Files
ppc_amnet/amnet-native/lib/iccard.ts
T
2025-02-05 08:43:49 +00:00

133 lines
3.6 KiB
TypeScript

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<ICCard | null> {
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<AmusementICCard | null> {
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
};
}