UNPKG

je_nfc_sdk

Version:

A comprehensive React Native SDK for NFC-based device control and communication

565 lines (564 loc) 24.2 kB
/** * NFC Frame Server * * Handles framing and processing of NFC communication frames * according to the specified protocol. * Enhanced to handle ISO 7816 status words and APDU responses. * Includes state machine for handling frame errors and processing status. */ // Import required libraries import NfcManager from 'react-native-nfc-manager'; import LoggerService from '../utils/LoggerService'; class FrameServer { constructor() { // Constants this.MAX_FRAME_SIZE = 1024; // Maximum frame size in bytes this.RECORD_SEPARATOR = 0x1E; // Error codes this.ERROR_CODES = { NONE: 0x00, PROCESSING: 0xFF, INVALID_COMMAND_ID: 0x01, COMMAND_ID_INDEX: 0x02, INVALID_DATA_ARGUMENTS: 0x03, INVALID_FRAME_PAYLOAD_SIZE: 0x04, INVALID_STOP_FRAME_SIZE: 0x05, EXCEED_MAX_PAYLOAD_SIZE: 0x06 }; // ISO 7816 Status Words this.STATUS_WORDS = { SUCCESS: [0x90, 0x00], COMMAND_NOT_ALLOWED: [0x69, 0x86], WRONG_PARAMETERS: [0x6A, 0x80], FILE_NOT_FOUND: [0x6A, 0x82], SECURITY_STATUS_NOT_SATISFIED: [0x69, 0x82] }; // Timeout for operations in milliseconds this.DEFAULT_TIMEOUT = 5000; } /** * Creates a properly formatted frame for sending to NFC device * Frame format: [Header(4)] [PayloadLen(1)] [CmdId(1)] [IdIndex(1)] [DataLen(1)] [Data(n)] [ErrorCode(1)] [Separator(1)] * * @param {number} commandId - Command ID for the frame * @param {number} idIndex - ID Index for tracking related frames * @param {Array|Uint8Array} data - Data payload to include in the frame * @param {number} instruction - Instruction byte (0xB2 for read, 0xD6 for write) * @returns {Uint8Array} - Properly formatted frame */ createSendFrame(commandId, idIndex, data = [], instruction = 0xD6) { // Validate inputs if (typeof commandId !== 'number' || commandId < 0 || commandId > 255) { throw new Error('Invalid commandId: must be 0-255'); } if (typeof idIndex !== 'number' || idIndex < 0 || idIndex > 255) { throw new Error('Invalid idIndex: must be 0-255'); } if (!Array.isArray(data) && !(data instanceof Uint8Array)) { throw new Error('Data must be an array or Uint8Array'); } // Convert data to array for easier handling const dataArray = Array.from(data); const dataLength = dataArray.length; // Check data size if (dataLength > 255) { throw new Error('Data length cannot exceed 255 bytes'); } // Calculate payload length: cmdId(1) + idIndex(1) + dataLength(1) + data(n) + error byte + stopper const payloadLength = 1 + 1 + 1 + dataLength + 1 + 1; // Check total frame size const totalFrameSize = 4 + 1 + payloadLength; // header + payloadLen + payload + errorCode + separator if (totalFrameSize > this.MAX_FRAME_SIZE) { throw new Error(`Frame exceeds maximum size (${this.MAX_FRAME_SIZE} bytes)`); } // Create frame buffer const frame = new Uint8Array(totalFrameSize); let index = 0; // Set header (4 bytes) frame[index++] = 0x00; // Class frame[index++] = instruction; // Instruction (0xB2 for read, 0xD6 for write) frame[index++] = 0x00; // P1 frame[index++] = 0x00; // P2 // Set payload length (1 byte) frame[index++] = payloadLength; // Set payload data frame[index++] = commandId; // Command ID frame[index++] = idIndex; // ID Index frame[index++] = dataLength; // Data length // Set actual data for (let i = 0; i < dataLength; i++) { frame[index++] = dataArray[i]; } // Set error byte to 0xFF (PROCESSING) for all outgoing frames frame[index++] = this.ERROR_CODES.PROCESSING; // Set record separator frame[index++] = this.RECORD_SEPARATOR; LoggerService.info(`Created send Frame:${frame}`); return frame; } /** * Create a read request frame * * @param {number} commandId - Command ID for the read operation * @param {number} idIndex - ID Index for tracking * @param {Array|Uint8Array} parameters - Optional parameters for the read request * @returns {Uint8Array} - Properly formatted read frame */ createReadFrame(commandId, idIndex, parameters = []) { return this.createSendFrame(commandId, idIndex, parameters, 0xB2); } /** * Create a write request frame * * @param {number} commandId - Command ID for the write operation * @param {number} idIndex - ID Index for tracking * @param {Array|Uint8Array} data - Data to write * @returns {Uint8Array} - Properly formatted write frame */ createWriteFrame(commandId, idIndex, data) { return this.createSendFrame(commandId, idIndex, data, 0xD6); } /** * Parse a received frame from the NFC device * Frame format: [Header(4)] [PayloadLen(1)] [CmdId(1)] [IdIndex(1)] [DataLen(1)] [Data(n)] [ErrorCode(1)] [Separator(1)] [StatusWord(2)] * * @param {Uint8Array} frameData - Raw frame data received from NFC * @returns {ParsedFrame} - Parsed frame with command, index, payload, and error information */ parseFrame(frameData) { if (!(frameData instanceof Uint8Array) && !Array.isArray(frameData)) { throw new Error('Frame data must be a Uint8Array or array'); } const frame = Array.from(frameData); if (frame.length < 3) { // Minimum: header(4) + payloadLen(1) + cmdId(1) + idIndex(1) + dataLen(1) LoggerService.info(`Recived Response frame length:${frame.length}`); throw new Error('Frame too small to be valid'); } // Parse header //const header = frame.slice(0, 4); // [0x00, 0xB2, 0x00, 0x00] const payloadLength = frame[0]; // // Validate payload length // if (frame.length < 2 + payloadLength + 1 + 1) { // header + payloadLen + payload + errorCode + separator (minimum) // throw new Error('Frame length does not match payload length'); // } // Parse payload const commandId = frame[1]; const idIndex = frame[2]; const dataLength = frame[3]; // Validate data length within payload const expectedPayloadLength = 1 + 1 + 1 + dataLength + 1 + 1; // cmdId + idIndex + dataLength + data if (expectedPayloadLength !== payloadLength) { throw new Error(`Payload length mismatch: expected ${expectedPayloadLength}, got ${payloadLength}`); } // Extract actual data const dataStartIndex = 4; const actualData = new Uint8Array(frame.slice(dataStartIndex, dataStartIndex + dataLength)); // Parse error code and separator const errorCodeIndex = payloadLength - 1; const separatorIndex = payloadLength; if (separatorIndex >= frame.length) { throw new Error('Frame missing error code or separator'); } const errorCode = frame[errorCodeIndex]; const separator = frame[separatorIndex]; if (separator !== this.RECORD_SEPARATOR) { throw new Error(`Invalid frame: expected separator 0x${this.RECORD_SEPARATOR.toString(16)}, got 0x${separator.toString(16)}`); } // Parse status word (if present) let statusWord = null; const statusWordStartIndex = separatorIndex + 1; if (frame.length >= statusWordStartIndex + 2) { statusWord = [ frame[statusWordStartIndex], frame[statusWordStartIndex + 1] ]; } // Create payload for compatibility const payload = new Uint8Array([ commandId, idIndex, dataLength, ...actualData ]); const apduSuccess = this.isSuccessStatusWord(statusWord); const isProcessing = errorCode === this.ERROR_CODES.PROCESSING; return { commandId, idIndex, payload, actualData, dataLength, errorCode, isValid: errorCode === this.ERROR_CODES.NONE, statusWord, apduSuccess, isProcessing }; } /** * Write data to NFC device with proper framing * Only checks SW1 and SW2 status words for success/failure * * @param {number} commandId - Command ID for the operation * @param {number} idIndex - ID Index for tracking * @param {Array|Uint8Array} data - Data to write * @returns {Promise<ResponseResult>} - Result of the write operation */ async writeData(commandId, idIndex, data) { try { // Create the write frame const frame = this.createWriteFrame(commandId, idIndex, data); LoggerService.info(`Write frame: [${Array.from(frame).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')}]`); // Use NfcManager to transceive const response = await this.transceiveWithTimeout(frame, this.DEFAULT_TIMEOUT); LoggerService.success(`Write response: [${Array.from(response).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')}]`); // For write operations, we only check the status word (SW1 SW2) // Assume response is just the status word (2 bytes: SW1 SW2) if (response.length >= 2) { const sw1 = response[response.length - 2]; const sw2 = response[response.length - 1]; const statusWord = [sw1, sw2]; const isSuccess = this.isSuccessStatusWord(statusWord); const statusMessage = this.getStatusWordMessage(statusWord); LoggerService.success(`Write status: SW1=0x${sw1.toString(16).padStart(2, '0')} SW2=0x${sw2.toString(16).padStart(2, '0')} - ${statusMessage}`); if (isSuccess) { return { success: true, data: response, statusWord: statusWord, statusMessage: statusMessage }; } else { return { success: false, errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, errorMessage: `Write failed with status word: 0x${sw1.toString(16).padStart(2, '0')} 0x${sw2.toString(16).padStart(2, '0')}`, statusWord: statusWord, statusMessage: statusMessage, data: response }; } } else { return { success: false, errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, errorMessage: 'Invalid response: missing status word', data: response }; } } catch (error) { console.error(`Write operation failed: ${error instanceof Error ? error.message : String(error)}`); return { success: false, errorMessage: error instanceof Error ? error.message : String(error), errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, data: null }; } } /** * Create a read request frame and send it to the NFC device * * @param {number} commandId - Command ID for the read operation * @param {number} idIndex - ID Index for tracking * @param {Array|Uint8Array} parameters - Optional parameters for the read request * @returns {Promise<ResponseResult>} - Result of the read operation */ async readReq(commandId, idIndex, parameters = []) { try { // Create the read frame const frame = this.createReadFrame(commandId, idIndex, parameters); LoggerService.info(`Read frame: [${Array.from(frame).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')}]`); // Use NfcManager to transceive const response = await this.transceiveWithTimeout(frame, this.DEFAULT_TIMEOUT); LoggerService.success(`Read response: [${Array.from(response).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')}]`); LoggerService.info(`Read response length: ${response.length}`); // Process and validate the response const result = this.processResponse(response); LoggerService.success(`Processed read response: ${JSON.stringify(result, null, 2)}`); return result; } catch (error) { console.error(`Read operation failed: ${error instanceof Error ? error.message : String(error)}`); return { success: false, errorMessage: error instanceof Error ? error.message : String(error), errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, data: null }; } } /** * Process a response from the NFC device * Enhanced to first check status words in payload, then apply state machine logic * * @param {Uint8Array} responseData - Raw response data from NFC * @returns {ResponseResult} - Processed response with parsed data */ processResponse(responseData) { try { const parsedFrame = this.parseFrame(responseData); // Check status word first (if present) if (parsedFrame.statusWord) { const sw = parsedFrame.statusWord; if (!parsedFrame.apduSuccess) { const swHex = sw.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '); return { success: false, errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, errorMessage: `APDU command failed with status word: ${swHex}`, statusWord: sw, statusMessage: this.getStatusWordMessage(sw), data: parsedFrame, }; } } // Handle processing state if (parsedFrame.isProcessing) { return { success: false, errorCode: this.ERROR_CODES.PROCESSING, errorMessage: 'Operation still processing', isProcessing: true, data: parsedFrame, }; } // Handle other non-zero error codes if (parsedFrame.errorCode !== this.ERROR_CODES.NONE) { return { success: false, errorCode: parsedFrame.errorCode, errorMessage: this.getErrorMessage(parsedFrame.errorCode), data: parsedFrame, }; } // ✅ Success case: no error, valid status word return { success: true, data: parsedFrame, statusWord: parsedFrame.statusWord || undefined, statusMessage: parsedFrame.statusWord ? this.getStatusWordMessage(parsedFrame.statusWord) : undefined, }; } catch (error) { return { success: false, errorMessage: error instanceof Error ? error.message : String(error), errorCode: this.ERROR_CODES.INVALID_DATA_ARGUMENTS, data: null, }; } } /** * Check if a status word indicates success (0x90 0x00) * * @param {Array|Uint8Array} statusWord - Status word to check * @returns {boolean} - True if status word indicates success */ isSuccessStatusWord(statusWord) { if (!statusWord || statusWord.length !== 2) { return false; } return statusWord[0] === this.STATUS_WORDS.SUCCESS[0] && statusWord[1] === this.STATUS_WORDS.SUCCESS[1]; } /** * Get human-readable error message for an error code * * @param {number} errorCode - Error code from NFC device * @returns {string} - Human-readable error message */ getErrorMessage(errorCode) { switch (errorCode) { case this.ERROR_CODES.NONE: return 'No error'; case this.ERROR_CODES.PROCESSING: return 'Operation still processing'; case this.ERROR_CODES.INVALID_COMMAND_ID: return 'Invalid command ID'; case this.ERROR_CODES.COMMAND_ID_INDEX: return 'Invalid command ID index'; case this.ERROR_CODES.INVALID_DATA_ARGUMENTS: return 'Invalid data arguments'; case this.ERROR_CODES.INVALID_FRAME_PAYLOAD_SIZE: return 'Invalid frame payload size'; case this.ERROR_CODES.INVALID_STOP_FRAME_SIZE: return 'Invalid stop frame size'; case this.ERROR_CODES.EXCEED_MAX_PAYLOAD_SIZE: return 'Exceeded maximum payload size'; default: return `Unknown error code: 0x${errorCode.toString(16).padStart(2, '0')}`; } } /** * Get human-readable message for ISO 7816 status word * * @param {Array|Uint8Array} statusWord - Status word from ISO 7816 response * @returns {string} - Human-readable message */ getStatusWordMessage(statusWord) { if (!statusWord || statusWord.length !== 2) { return 'Invalid status word'; } const sw1 = statusWord[0]; const sw2 = statusWord[1]; // Check against known status words if (sw1 === 0x90 && sw2 === 0x00) { return 'Success'; } else if (sw1 === 0x69 && sw2 === 0x86) { return 'Command not allowed (no current EF)'; } else if (sw1 === 0x6A && sw2 === 0x80) { return 'Wrong parameters'; } else if (sw1 === 0x6A && sw2 === 0x82) { return 'File not found'; } else if (sw1 === 0x69 && sw2 === 0x82) { return 'Security status not satisfied'; } else if (sw1 === 0x61) { return `Success (${sw2} bytes still available)`; } else if (sw1 === 0x6C) { return `Wrong length (expected ${sw2})`; } return `Unknown status word: 0x${sw1.toString(16).padStart(2, '0')} 0x${sw2.toString(16).padStart(2, '0')}`; } /** * Utility to send APDU with timeout * * @param {Array|Uint8Array} apduCommand - APDU command to send * @param {number} timeout - Timeout in milliseconds * @returns {Promise<Uint8Array>} - Response from NFC device */ async transceiveWithTimeout(apduCommand, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error('NFC operation timed out')); }, timeout); NfcManager.transceive(Array.from(apduCommand)) .then(response => { clearTimeout(timeoutId); resolve(new Uint8Array(response)); }) .catch(error => { clearTimeout(timeoutId); reject(error); }); }); } async hexFrameToDecimal(hexFrame, dataLength) { let value = 0; for (let i = 0; i < dataLength; i++) { value = (value << 8) + hexFrame[i]; } return value; } /** * Poll for operation completion when receiving PROCESSING state * * @param {number} commandId - Command ID to poll * @param {number} idIndex - ID Index to poll * @param {number} maxAttempts - Maximum number of polling attempts * @param {number} pollInterval - Time between polls in milliseconds * @returns {Promise<ResponseResult>} - Final result after polling */ async pollUntilComplete(commandId, idIndex, maxAttempts = 10, pollInterval = 500) { let attempts = 0; while (attempts < maxAttempts) { // Send status check command const result = await this.readReq(commandId, idIndex); // Check if operation is complete if (!result.isProcessing) { // Process completed (either success or error) return result; } // Operation still processing, wait and try again attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, pollInterval)); } } // If we reach here, polling timed out return { success: false, errorCode: this.ERROR_CODES.PROCESSING, errorMessage: 'Operation timed out while processing', isProcessing: true, data: null }; } /** * Helper method to format byte array for logging * * @param {Array|Uint8Array} bytes - Byte array to format * @returns {string} - Formatted hex string */ formatBytes(bytes) { return Array.from(bytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '); } /** * Validate frame structure for debugging * * @param {Array|Uint8Array} frame - Frame to validate * @returns {Object} - Validation result with details */ validateFrame(frame) { const errors = []; const frameArray = Array.from(frame); // Check minimum length if (frameArray.length < 8) { errors.push('Frame too short (minimum 8 bytes)'); return { isValid: false, errors, details: { length: frameArray.length } }; } // Check header if (frameArray[0] !== 0x00) { errors.push(`Invalid class byte: expected 0x00, got 0x${frameArray[0].toString(16)}`); } if (frameArray[1] !== 0xB2 && frameArray[1] !== 0xD6) { errors.push(`Invalid instruction byte: expected 0xB2 or 0xD6, got 0x${frameArray[1].toString(16)}`); } if (frameArray[2] !== 0x00 || frameArray[3] !== 0x00) { errors.push('Invalid P1/P2 bytes: expected 0x00 0x00'); } // Check payload length consistency const payloadLength = frameArray[4]; const expectedMinLength = 5 + payloadLength + 1 + 1; // header + payloadLen + payload + errorCode + separator if (frameArray.length < expectedMinLength) { errors.push(`Frame length ${frameArray.length} is less than expected minimum ${expectedMinLength}`); } // Check data length consistency within payload if (payloadLength >= 3) { const dataLength = frameArray[7]; const expectedPayloadLength = 3 + dataLength; // cmdId + idIndex + dataLength + data if (expectedPayloadLength !== payloadLength) { errors.push(`Payload length mismatch: payload claims ${payloadLength}, calculated ${expectedPayloadLength}`); } } // Check separator const separatorIndex = 5 + payloadLength + 1; if (separatorIndex < frameArray.length && frameArray[separatorIndex] !== this.RECORD_SEPARATOR) { errors.push(`Invalid separator: expected 0x${this.RECORD_SEPARATOR.toString(16)}, got 0x${frameArray[separatorIndex].toString(16)}`); } return { isValid: errors.length === 0, errors, details: { length: frameArray.length, payloadLength: payloadLength, separatorIndex: separatorIndex, hasStatusWord: frameArray.length >= separatorIndex + 3 } }; } } export default new FrameServer();