UNPKG

smartcardx

Version:

Backend library for communication with smartcards using system native PCSC interface. Plain Iso7816 + EMV + GlobalPlatform functionality.

385 lines (384 loc) 15 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Card = void 0; const events_1 = require("events"); const logger_1 = __importDefault(require("./logger")); const utils_1 = require("./utils"); const commandApdu_1 = __importDefault(require("./commandApdu")); const responseApdu_1 = __importDefault(require("./responseApdu")); const commands_1 = require("./iso7816/commands"); /** Response APDU max size(256 for data + 2 for status) */ const maxTrResLen = 258; class Card { constructor(device, atr, protocol) { this._eventEmitter = new events_1.EventEmitter(); this._decodedAtr = null; this._autoGetResponse = true; logger_1.default.trace(`Creating card instance for device "${device.name}"`); this._device = device; this._protocol = protocol; this._atr = new Uint8Array(atr.byteLength); try { (0, utils_1.importBinData)(atr, this._atr); this._decodedAtr = (0, utils_1.decodeAtr)(this._atr); } catch (error) { const err = new Error(`Error importing card ATR: ${error.message}`); logger_1.default.error(err); throw err; } this._atrHex = (0, utils_1.hexEncode)(this._atr); this._isBusy = false; logger_1.default.debug(`Card ATR: [${(0, utils_1.hexEncode)(atr)}]`); logger_1.default.debug(`Card protocol: ${protocol}`); } get protocol() { return this._protocol; } get atr() { return this._atr; } get atrHex() { return this._atrHex; } get decodedAtr() { return this._decodedAtr; } isBusy() { return this._isBusy; } toString() { return `Card(atr:0x${this.atrHex})`; } /** Function that transforms each command before sending; * Can be used to add secure session authentication */ setCommandTransformer(func) { if (typeof func === 'function') { logger_1.default.trace('Setting card command transformer'); } else { logger_1.default.trace('Removing card command transformer'); } this._commandTransformer = func; return this; } /** Function that transforms each command before sending; * Can be used to add secure session authentication */ get commandTransformer() { return this._commandTransformer; } _doCommandTransform(cmd) { if (typeof this._commandTransformer === 'undefined') { return cmd; } else { logger_1.default.trace('Applying command transformer'); return this._commandTransformer(cmd); } } /** Function that transforms each response before returning it; * Can be used to add secure session authentication */ setResponseTransformer(func) { if (typeof func === 'function') { logger_1.default.trace('Setting card response transformer'); } else { logger_1.default.trace('Removing card response transformer'); } this._responseTransformer = func; return this; } /** Function that transforms each response before returning it; * Can be used to add secure session authentication */ get responseTransformer() { return this._responseTransformer; } _doResponseTransform(rsp) { if (typeof this._responseTransformer === 'undefined') { return rsp; } else { logger_1.default.trace('Applying response transformer'); return this._responseTransformer(rsp); } } /** * If set to true(default), `GET_RESPONSE` APDU gets sent automatically upon receiving `0x61XX` response. * Also the command `Le` value gets corrected and commands is sent again upon receiving `0x6CXX` response. */ setAutoGetResponse(val = true) { logger_1.default.trace(`Setting autoGetResponse to "${val}"`); this._autoGetResponse = val; return this; } /** * If set to true(default), `GET_RESPONSE` APDU gets sent automatically upon receiving `0x61XX` response. * Also the command `Le` value gets corrected and commands is sent again upon receiving `0x6CXX` response. */ set autoGetResponse(val) { this.setAutoGetResponse(val); } /** Current state of the autoGetResponse feature */ get autoGetResponse() { return this._autoGetResponse; } _issueCmdInternal(cmd, callback) { this._isBusy = true; let doCommandTransform = true; const respAcc = new responseApdu_1.default(); // response accumulator let middleCallback; if (!this.autoGetResponse) { middleCallback = (err, respBuffer) => { if (err) { this._isBusy = false; callback(err, new responseApdu_1.default()); return; } if (respBuffer.byteLength < 2) { this._isBusy = false; callback(new Error(`Error response: [${(0, utils_1.hexEncode)(respBuffer)}]`), new responseApdu_1.default()); return; } let response; try { response = new responseApdu_1.default(respBuffer); } catch (error) { this._isBusy = false; callback(new Error(`Error response: [${(0, utils_1.hexEncode)(respBuffer)}]`), new responseApdu_1.default()); return; } this._eventEmitter.emit('response-received', { device: this._device, card: this, command: cmd, response, }); try { response = this._doResponseTransform(response); } catch (error) { this._isBusy = false; callback(new Error(`Error transformng response: ${error.message}`), new responseApdu_1.default()); return; } this._isBusy = false; callback(err, response); }; } else { middleCallback = (err, respBuffer) => { if (err) { this._isBusy = false; callback(err, new responseApdu_1.default()); return; } if (respBuffer.byteLength < 2) { this._isBusy = false; callback(new Error(`Error response: [${(0, utils_1.hexEncode)(respBuffer)}]`), new responseApdu_1.default()); return; } let response; try { response = new responseApdu_1.default(respBuffer); } catch (error) { this._isBusy = false; callback(new Error(`Error response: [${(0, utils_1.hexEncode)(respBuffer)}]`), new responseApdu_1.default()); return; } this._eventEmitter.emit('response-received', { device: this._device, card: this, command: cmd, response, }); try { response = this._doResponseTransform(response); } catch (error) { this._isBusy = false; callback(new Error(`Error transformng response: ${error.message}`), new responseApdu_1.default()); return; } const bytesToGet = response.availableResponseBytes; if (bytesToGet > 0) { if (response.dataLength > 0) respAcc.addData(response.data); let cmdToResend; switch (true) { case response.hasMoreBytesAvailable: logger_1.default.trace('Getting response automatically...'); cmdToResend = (0, commands_1.getResponse)(response.availableResponseBytes); doCommandTransform = false; break; case response.isWrongLe: logger_1.default.trace('Fixing command Le value...'); cmdToResend = new commandApdu_1.default(cmd).setLe(response.availableResponseBytes); break; default: break; } if (typeof cmdToResend === 'undefined') { this._isBusy = false; callback(err, respAcc .addData(response.data) .setStatus(response.status)); } else { if (doCommandTransform) { try { cmdToResend = this._doCommandTransform(cmdToResend); } catch (error) { this._isBusy = false; callback(new Error(`Error transformng command: ${error.message}`), new responseApdu_1.default()); return; } } else { doCommandTransform = true; } this._eventEmitter.emit('command-issued', { device: this._device, card: this, command: cmdToResend, }); try { this._device.transmit(cmdToResend.toByteArray(), maxTrResLen, this._protocol, middleCallback); } catch (error) { this._isBusy = false; callback(new Error(`Command transmission error: ${error.message}`), new responseApdu_1.default()); return; } } } else { this._isBusy = false; callback(err, respAcc .addData(response.data) .setStatus(response.status)); } }; } let tCmd = cmd; if (doCommandTransform) { try { tCmd = this._doCommandTransform(cmd); } catch (error) { this._isBusy = false; callback(new Error(`Error transformng command: ${error.message}`), new responseApdu_1.default()); return; } doCommandTransform = true; } this._eventEmitter.emit('command-issued', { device: this._device, card: this, command: tCmd, }); try { this._device.transmit(tCmd.toByteArray(), maxTrResLen, this._protocol, middleCallback); } catch (error) { this._isBusy = false; callback(new Error(`Command transmission error: ${error.message}`), new responseApdu_1.default()); return; } } issueCommand(command, callback) { logger_1.default.trace('Issuing command to card'); let cmd; if (command instanceof commandApdu_1.default) { cmd = command; } else { try { cmd = new commandApdu_1.default(command); } catch (error) { throw new Error(`Command APDU error: ${error.message}`); } } let checkingErr; if (cmd.byteLength < 4) { checkingErr = new Error(`Command too short; Min: 5 bytes; Received: ${cmd.byteLength} bytes; cmd: [${cmd.toString()}]`); } else if (cmd.byteLength === 6) { if (cmd.getLc() === 0) { checkingErr = new Error(`If Lc = 0, it should be omitted; cmd: [${cmd.toString()}]`); } checkingErr = new Error(`Lc or Data missing; cmd: [${cmd.toString()}]`); } else if (cmd.data.byteLength > commandApdu_1.default.MAX_DATA_BYTE_LENGTH) { checkingErr = new Error(`Command data too long; Max: ${commandApdu_1.default.MAX_DATA_BYTE_LENGTH} bytes; Received: ${cmd.data.byteLength} bytes; cmd: [${cmd.toString()}]`); } else if (cmd.getLc() !== cmd.getData().byteLength) { checkingErr = new Error(`Lc and actual data length discrepancy; Lc:${cmd.getLc()} actual: ${cmd.getData().byteLength}; cmd: [${cmd.toString()}]`); } if (callback) { if (checkingErr) { callback(checkingErr, new responseApdu_1.default([])); return; } try { this._issueCmdInternal(cmd, callback); } catch (error) { callback(new Error(`Error sending command to card: ${error.message}`), responseApdu_1.default.from([])); return; } return; } else { return new Promise((resolve, reject) => { if (checkingErr) { return reject(checkingErr); } const callback = (err, resp) => { if (err) { return reject(err); } else { return resolve(resp); } }; try { this._issueCmdInternal(cmd, callback); } catch (error) { return reject(new Error(`Error sending command to card: ${error.message}`)); } }); } } on(eventName, eventHandler) { this._eventEmitter.on(eventName, eventHandler); return this; } once(eventName, eventHandler) { this._eventEmitter.once(eventName, eventHandler); return this; } removeListener(eventName, eventHandler) { this._eventEmitter.removeListener(eventName, eventHandler); } off(eventName, eventHandler) { this.removeListener(eventName, eventHandler); } removeAllListeners(eventName) { this._eventEmitter.removeAllListeners(eventName); } } exports.Card = Card; exports.default = Card;