import {Buffer} from "buffer"; import NfcManager, {NfcTech} from "react-native-nfc-manager"; import {performDecrypt} from "./iccard-crypto"; export enum ICType { Amusement = 1, Transport = 2 } export enum AmusementICVendor { Sega = 0x0078, Konami = 0x0068, BandaiNamcoEntertainment = 0x002A, BandaiNamcoGames = 0x003A, Nesica = 0x0079, Unknown = 0x0000 } type AmusementICCard = { type: ICType.Amusement, idm: string, accessCode: string, vendor: AmusementICVendor } type TransportICCard = { type: ICType.Transport, idm: string } export type ICCard = AmusementICCard | TransportICCard; /** * Converts an IC card IDm to the 20-digit "0008" number used by some networks * @param idm The IDm to convert */ export function icCard0008Number(idm: string): string { // 0008 is the numerical representation of the idm padded with zeros until it is 20 digits return BigInt(`0x${idm}`).toString().padStart(20, '0'); } /** * Reads a FeliCa card, returning access code information. */ export async function felicaCardReadAsync(): Promise { try { await NfcManager.requestTechnology(NfcTech.FelicaIOS, { isReaderModeEnabled: true, invalidateAfterFirstRead: false, alertMessage: "Place an Amusement IC or Transport card on your device." }); const tag = await NfcManager.getTag(); const idm = tag['idm'] as string; switch (tag['systemCode'] as string) { case "0003": return { type: ICType.Transport, idm }; case "88B4": return await readAmusementICCardContents(idm); default: return null; } } catch (ex) { return null; } finally { await NfcManager.cancelTechnologyRequest(); } } async function readAmusementICCardContents(idm: string): Promise { const idmBytes = Uint8Array.from(Buffer.from(idm, 'hex')); const readCommand = [ 0x06, // read ...idmBytes, 0x01, // service count (fixed) 0x0B, 0x00, // service code (000b, le) 0x02, // block count 0x80, 0x00, // S_PAD0 block 0x80, 0x82 // ID block ]; let response = await NfcManager.sendFelicaCommandIOS(readCommand); const offset = response.indexOf(0x07); // look for 0x07 as for some reason the response may not start at response[0] // response must have 44 bytes if (offset === -1 || response.length - offset !== 44) { return null; } else if (offset !== 0) { response = response.slice(offset); } // response format: // 0 - read response (0x07) // 1-8 - IDm // 9 - status flag 1 // 10 - status flag 2 // 11 - number of blocks (0x02) // 12-27 - S_PAD0 block // 28-43 - ID block (DFC is 35-36) if (response[9] !== 0x00 || response[11] !== 0x02) { return null; } const dfcValue = response[37] | (response[36] << 8); if (dfcValue in AmusementICVendor && dfcValue !== AmusementICVendor.Unknown) { const accessCode = performDecrypt(new Uint8Array(response.slice(12, 28))); return { type: ICType.Amusement, accessCode: [...accessCode.accessCodeBytes].map(x => x.toString(16).padStart(2, '0')).join(''), vendor: dfcValue, idm }; } return { type: ICType.Amusement, accessCode: icCard0008Number(idm), vendor: AmusementICVendor.Unknown, idm }; }