je_nfc_sdk
Version:
A comprehensive React Native SDK for NFC-based device control and communication
565 lines (564 loc) • 24.2 kB
JavaScript
/**
* 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();