smartcardx
Version:
Backend library for communication with smartcards using system native PCSC interface. Plain Iso7816 + EMV + GlobalPlatform functionality.
385 lines (384 loc) • 15 kB
JavaScript
"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;