diff --git a/src/services/NfcService-alt.ts b/src/services/NfcService-alt.ts new file mode 100644 index 0000000..e3d459b --- /dev/null +++ b/src/services/NfcService-alt.ts @@ -0,0 +1,266 @@ +import { logger } from "../main/LogService"; +import { NFC } from "nfc-pcsc"; + +function formatHexBuffer(buff: Buffer): string { + console.log("LEN:", buff.length); + const str = ( + buff + .toString("hex") + .replace(/(.{2})/g, "$1 ") + .match(/.{1,48}/g) ?? [""] + ).join("\n"); + return str; +} + +function findIndexOfPattern(buff: Buffer, pattern: number[]): number { + if (pattern.length === 0) return -1; + + for (let i = 0; i <= buff.length - pattern.length; i++) { + let coincidencia = true; + + for (let j = 0; j < pattern.length; j++) { + if (buff[i + j] !== pattern[j]) { + coincidencia = false; + break; + } + } + + if (coincidencia) return i; // Retorna el índice de inicio + } + + return -1; // Retorna -1 si no encuentra nada +} + +function decodeRecord(buff: Buffer): number { + // Se suponde que el byte 0 del token es el inicio del PAN + // https://www.eftlab.com/knowledge-base/complete-list-of-emv-nfc-tags + const effectiveDate = [0x5f, 0x25]; + const pan = [0x5a]; + const panseq = [0x5f, 0x34]; + const usageControl = [0x9f, 0x07]; + const cdol = [0x8c]; + // ISO/IEC 7813 + const issuerData = [0x57]; + + if (buff.length < 164) { + return 1; + } + + // 1. byte de inicio del PAN + const inicio = findIndexOfPattern(buff, pan); + + if (inicio == undefined) return 1; + + // 2. 38 bytes desde ahi (offset de 2) por la instruccion + const inicioPAN = inicio + pan.length; + const finPan = inicioPAN + 1 + buff[inicioPAN]; + const fin = inicioPAN + 38; // 38 es supuestamente la longitud del public token + //console.log("IDXPAN", inicioPAN, finPan, buff[inicioPAN]); + + const completePAN = buff.subarray(inicioPAN, finPan); + console.log("PAN:\n", formatHexBuffer(completePAN)); + const complete = buff.subarray(inicioPAN, fin); + console.log("CODIGO:\n", formatHexBuffer(complete)); + + const inicioIDD = findIndexOfPattern(buff, issuerData); + if (inicioIDD == undefined) return 1; + const lenIDD = buff[inicioIDD + issuerData.length]; + const complpeteIDD = buff.subarray( + inicioIDD, + lenIDD + inicioIDD + issuerData.length + 1, + ); + console.log("INICIOIDD", inicioIDD, lenIDD); + console.log("IDD:", formatHexBuffer(complpeteIDD)); + + // Inmediatamente despues del PAN tiene el codigo 5f 34 (01) (00) que inica que + // solo hay una tarjeta con este PAN pero en la imagen de treezor hay un separador + // y la fecha de expiracion. + + /** + * Para leer el public token: + * + */ + return 0; +} + +function findPublicToken(buff: Buffer): string { + /** + 57 - EMV + 13 - LEN (19) + [3B] 54 69 23 - PAN BIN + [5B] 08 79 39 81 68 - PAN REMANDER + + [4B] + d - separador + 29 09 - fecha de expiración + 22 1 - codigo de servicio + + [7B] + 0 - separador + 00 00 06 1 - public token (7) + 97 5 - cvc + 00 - public token (2) + f - padding + */ + const BYTE_INICIO_TOKEN = 12; + const issuerData = [0x57]; + const emvOffset = issuerData.length + 1; + const inicio = findIndexOfPattern(buff, issuerData); + const finIndex = inicio + emvOffset + buff[inicio + issuerData.length]; + const issuerDataBuff = buff.subarray(inicio, finIndex); + const inicioData = emvOffset; // EMV + LEN + const inicioToken = inicioData + BYTE_INICIO_TOKEN; + const tokenbuff = issuerDataBuff.subarray(inicioToken); + const tokenstr = tokenbuff.toString("hex"); + // El primer cuarteto es un 0 de separacion el último es una f de padding + const token = tokenstr.substring(1, 8) + tokenstr.substring(11, 13); + console.log("Public token", tokenstr, " - ", token); + + return token; +} + +export type NfcEvent = + | { type: "tag"; uid: string } + | { type: "removed"; uid: string } + | { type: "reader"; name: string } + | { type: "error"; message: string }; + +// Estan hardcodeados pero en principio no deberia hacer falta más +const commands = { + app: [0x00, 0xa4, 0x04, 0x00, 0x05, 0xa0, 0x00, 0x00, 0x00, 0x04, 0x00], + options: [0x80, 0xa8, 0x00, 0x00, 0x02, 0x83, 0x00, 0x00], + record: [0x00, 0xb2, 0x01, 0x14, 0x00], +}; +/** PROVISIONAL */ +export class NfcServiceAlt { + private nfc: NFC; + private onEvent?: (event: NfcEvent) => void; + private currentReaderName: string = "OFFLINE"; + + constructor(onEvent?: (event: NfcEvent) => void) { + logger.info("NfcService: Constructor start"); + try { + logger.info("NfcService: Creating PCSC instance..."); + this.nfc = new NFC(console); + logger.info("NfcService: PCSC instance created."); + } catch (err) { + logger.error("NfcService: Failed to create PCSC instance", err); + throw err; + } + this.onEvent = onEvent; + this.init(); + logger.info("NfcService: Constructor end"); + } + + public getReaderName(): string { + return this.currentReaderName; + } + + private init(): void { + logger.info( + "NfcService: Starting initialization and attaching listeners...", + ); + this.nfc.on("reader", (reader) => { + console.log(`Lector detectado: ${reader.name}`); + logger.info(`NFC Reader detected: ${reader.name}`); + reader.aid = "a0000000041010"; + + this.currentReaderName = reader.name; + if (this.onEvent) { + this.onEvent({ type: "reader", name: reader.name }); + } + reader.autoProcessing = false; + + reader.on("card", async (card) => { + const send = async ( + cmd: number[], + comment: string | null = null, + responseMaxLength: number = 40, + ): Promise> => { + const b = Buffer.from(cmd); + /* + console.log( + (comment ? `[${comment}] ` : "") + `sending`, + b, + ); + */ + const data = await reader.transmit(b, responseMaxLength); + /* + console.log( + (comment ? `[${comment}] ` : "") + `received data \n`, + formatHexBuffer(data), + ); + */ + return data; + }; + + console.log(`Tarjeta detectada! UID: ${JSON.stringify(card)}`); + logger.info(`NFC Tag detected: ${JSON.stringify(card)}`); + + const res1 = await send( + commands.app, + "step 1 - select app", + 255, + ); + const res2 = await send( + commands.options, + "step 2 - read options", + 255, + ); + const res3 = await send( + commands.record, + "step 3 - read record 2 - 1", + 255, + ); + + //decodeRecord(res3); + const token = findPublicToken(res3); + if (this.onEvent) { + this.onEvent({ type: "tag", uid: token }); + } + }); + + reader.on("card.off", async (card) => { + console.log(`Tarjeta retirada: ${card.uid}`); + logger.info(`NFC Tag removed: ${card.uid}`); + if (this.onEvent) { + this.onEvent({ type: "removed", uid: card.uid }); + } + }); + + reader.on("error", (err: Error) => { + console.error(`Error en el lector ${reader.name}:`, err); + if (this.onEvent) { + this.onEvent({ + type: "error", + message: err.message || String(err), + }); + } + }); + + reader.on("end", () => { + console.log(`Lector desconectado: ${reader.name}`); + logger.info(`NFC Reader disconnected: ${reader.name}`); + this.currentReaderName = "OFFLINE"; + if (this.onEvent) { + this.onEvent({ type: "reader", name: "OFFLINE" }); + } + }); + }); + + this.nfc.on("error", (err: Error) => { + console.error("Error general de NFC:", err); + if (this.onEvent) { + this.onEvent({ + type: "error", + message: err.message || String(err), + }); + } + }); + logger.info("NfcService: Initialization sequence complete."); + } + + public stop(): void { + // La mayoría de los lectores se cierran solos al cerrar la app + } +}