feathers-nfc-pcsc
Version:
Easy reading and writing NFC tags and cards
756 lines (527 loc) • 18.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.CONNECT_MODE_CARD = exports.CONNECT_MODE_DIRECT = exports.KEY_TYPE_B = exports.KEY_TYPE_A = exports.TAG_ISO_14443_4 = exports.TAG_ISO_14443_3 = undefined;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
var _errors = require('./errors');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
const TAG_ISO_14443_3 = exports.TAG_ISO_14443_3 = 'TAG_ISO_14443_3';
const TAG_ISO_14443_4 = exports.TAG_ISO_14443_4 = 'TAG_ISO_14443_4';
const KEY_TYPE_A = exports.KEY_TYPE_A = 0x60;
const KEY_TYPE_B = exports.KEY_TYPE_B = 0x61;
const CONNECT_MODE_DIRECT = exports.CONNECT_MODE_DIRECT = 'CONNECT_MODE_DIRECT';
const CONNECT_MODE_CARD = exports.CONNECT_MODE_CARD = 'CONNECT_MODE_CARD';
class Reader extends _events2.default {
static reverseBuffer(src) {
const buffer = new Buffer(src.length);
for (let i = 0, j = src.length - 1; i <= j; ++i, --j) {
buffer[i] = src[j];
buffer[j] = src[i];
}
return buffer;
}
static parseAid(str) {
const result = [];
for (let i = 0; i < str.length; i += 2) {
result.push(parseInt(str.substr(i, 2), 16));
}
return result;
}
static selectStandardByAtr(atr) {
// TODO: better detecting card types
if (atr[5] && atr[5] === 0x4f) {
return TAG_ISO_14443_3;
} else {
return TAG_ISO_14443_4;
}
}
get aid() {
return this._aid;
}
set aid(value) {
this.logger.info('Setting AID to', value);
this._aid = value;
const parsedAid = Reader.parseAid(value);
this.logger.info('AID parsed', parsedAid);
this._parsedAid = parsedAid;
}
get name() {
return this.reader.name;
}
constructor(reader, logger) {
var _this;
_this = super();
this.reader = null;
this.logger = null;
this.connection = null;
this.card = null;
this.autoProcessing = true;
this._aid = null;
this._parsedAid = null;
this.keyStorage = {
'0': null,
'1': null
};
this.pendingLoadAuthenticationKey = {};
this.reader = reader;
if (logger) {
this.logger = logger;
} else {
this.logger = {
log: function () {},
debug: function () {},
info: function () {},
warn: function () {},
error: function () {}
};
}
this.reader.on('error', err => {
this.logger.error(err);
this.emit('error', err);
});
this.reader.on('status', (() => {
var _ref = _asyncToGenerator(function* (status) {
_this.logger.debug('status', status);
// check what has changed
const changes = _this.reader.state ^ status.state;
_this.logger.debug('changes', changes);
if (changes) {
if (changes & _this.reader.SCARD_STATE_EMPTY && status.state & _this.reader.SCARD_STATE_EMPTY) {
_this.logger.info('card removed');
if (_this.card) {
_this.emit('card.off', _extends({}, _this.card));
}
try {
_this.card = null;
if (_this.connection) {
yield _this.disconnect();
}
} catch (err) {
_this.emit(err);
}
} else if (changes & _this.reader.SCARD_STATE_PRESENT && status.state & _this.reader.SCARD_STATE_PRESENT) {
const atr = status.atr;
_this.logger.info('card inserted', atr);
_this.card = {};
if (atr) {
_this.card.atr = atr;
_this.card.standard = Reader.selectStandardByAtr(atr);
_this.card.type = _this.card.standard;
}
try {
yield _this.connect();
if (!_this.autoProcessing) {
_this.emit('card', _this.card);
return;
}
_this.handleTag();
} catch (err) {
_this.emit(err);
}
}
}
});
return function (_x) {
return _ref.apply(this, arguments);
};
})());
this.reader.on('end', () => {
this.logger.info('reader removed');
this.emit('end');
});
}
connect(mode = CONNECT_MODE_CARD) {
const modes = {
[CONNECT_MODE_DIRECT]: this.reader.SCARD_SHARE_DIRECT,
[CONNECT_MODE_CARD]: this.reader.SCARD_SHARE_SHARED
};
if (!modes[mode]) {
throw new _errors.ConnectError('invalid_mode', 'Invalid mode');
}
this.logger.info('trying to connect', mode, modes[mode]);
return new Promise((resolve, reject) => {
// connect card
this.reader.connect({
share_mode: modes[mode]
//protocol: this.reader.SCARD_PROTOCOL_UNDEFINED
}, (err, protocol) => {
if (err) {
const error = new _errors.ConnectError(_errors.FAILURE, 'An error occurred while connecting.', err);
this.logger.error(error);
return reject(error);
}
this.connection = {
type: modes[mode],
protocol: protocol
};
this.logger.info('connected', this.connection);
return resolve(this.connection);
});
});
}
disconnect() {
if (!this.connection) {
throw new _errors.DisconnectError('not_connected', 'Reader in not connected. No need for disconnecting.');
}
this.logger.info('trying to disconnect', this.connection);
return new Promise((resolve, reject) => {
// disconnect removed
this.reader.disconnect(this.reader.SCARD_LEAVE_CARD, err => {
if (err) {
const error = new _errors.DisconnectError(_errors.FAILURE, 'An error occurred while disconnecting.', err);
this.logger.error(error);
return reject(error);
}
this.connection = null;
this.logger.info('disconnected');
return resolve(true);
});
});
}
transmit(data, responseMaxLength) {
if (!this.card || !this.connection) {
throw new _errors.TransmitError(_errors.CARD_NOT_CONNECTED, 'No card or connection available.');
}
return new Promise((resolve, reject) => {
this.logger.log('transmitting', data, responseMaxLength);
this.reader.transmit(data, responseMaxLength, this.connection.protocol, (err, response) => {
if (err) {
const error = new _errors.TransmitError(_errors.FAILURE, 'An error occurred while transmitting.', err);
return reject(error);
}
return resolve(response);
});
});
}
control(data, responseMaxLength) {
if (!this.connection) {
throw new _errors.ControlError('not_connected', 'No connection available.');
}
return new Promise((resolve, reject) => {
this.logger.log('transmitting control', data, responseMaxLength);
this.reader.control(data, this.reader.IOCTL_CCID_ESCAPE, responseMaxLength, (err, response) => {
if (err) {
const error = new _errors.ControlError(_errors.FAILURE, 'An error occurred while transmitting control.', err);
return reject(error);
}
return resolve(response);
});
});
}
loadAuthenticationKey(keyNumber, key) {
var _this2 = this;
return _asyncToGenerator(function* () {
if (!(keyNumber === 0 || keyNumber === 1)) {
throw new _errors.LoadAuthenticationKeyError('invalid_key_number');
}
const keyData = Reader.parseAid(key);
if (keyData.length !== 6) {
throw new _errors.LoadAuthenticationKeyError('invalid_key');
}
// CMD: Load Authentication Keys
const packet = new Buffer([0xff, // Class
0x82, // INS
0x00, // P1: Key Structure (0x00 = Key is loaded into the reader volatile memory.)
keyNumber, // P2: Key Number (00h ~ 01h = Key Location. The keys will disappear once the reader is disconnected from the PC)
0x06, // Lc
// Data In: Key (6 bytes)
...keyData]);
let response = null;
try {
response = yield _this2.transmit(packet, 2);
_this2.logger.info('response received', response);
} catch (err) {
throw new _errors.LoadAuthenticationKeyError(null, null, err);
}
const statusCode = response.readUInt16BE(0);
if (statusCode !== 0x9000) {
throw new _errors.LoadAuthenticationKeyError(_errors.OPERATION_FAILED, `Load authentication key operation failed: Status code: ${statusCode}`);
}
_this2.keyStorage[keyNumber] = key;
return keyNumber;
})();
}
// for PC/SC V2.01 use obsolete = true
// for PC/SC V2.07 use obsolete = false [default]
authenticate(blockNumber, keyType, key, obsolete = false) {
var _this3 = this;
return _asyncToGenerator(function* () {
let keyNumber = Object.keys(_this3.keyStorage).find(function (n) {
return _this3.keyStorage[n] === key;
});
// key is not in the storage
if (!keyNumber) {
// is not being written now?
if (_this3.pendingLoadAuthenticationKey[key]) {
try {
keyNumber = yield _this3.pendingLoadAuthenticationKey[key];
} catch (err) {
throw new _errors.AuthenticationError('unable_to_load_key', 'Could not load authentication key into reader.', err);
}
} else {
// set key number to first
keyNumber = Object.keys(_this3.keyStorage)[0];
// if this number is not free
if (_this3.keyStorage[keyNumber] !== null) {
// try to find any free number
const freeNumber = Object.keys(_this3.keyStorage).find(function (n) {
return _this3.keyStorage[n] === null;
});
// if we find, we use it, otherwise the first will be used and rewritten
if (freeNumber) {
keyNumber = freeNumber;
}
}
try {
_this3.pendingLoadAuthenticationKey[key] = _this3.loadAuthenticationKey(parseInt(keyNumber), key);
yield _this3.pendingLoadAuthenticationKey[key];
} catch (err) {
throw new _errors.AuthenticationError('unable_to_load_key', 'Could not load authentication key into reader.', err);
}
}
}
const packet = !obsolete ?
// CMD: Authentication
new Buffer([0xff, // Class
0x86, // INS
0x00, // P1
0x00, // P2
0x05, // Lc
// Data In: Authenticate Data Bytes (5 bytes)
0x01, // Byte 1: Version
0x00, // Byte 2
blockNumber, // Byte 3: Block Number
keyType, // Byte 4: Key Type
keyNumber]) :
// CMD: Authentication (obsolete)
new Buffer([0xff, // Class
0x88, // INS
0x00, // P1
blockNumber, // P2: Block Number
keyType, // P3: Key Type
keyNumber // Data In: Key Number
]);
let response = null;
try {
response = yield _this3.transmit(packet, 2);
_this3.logger.info('response received', response);
} catch (err) {
throw new _errors.AuthenticationError(null, null, err);
}
const statusCode = response.readUInt16BE(0);
if (statusCode !== 0x9000) {
_this3.logger.error('[authentication operation failed][request packet]', packet);
throw new _errors.AuthenticationError(_errors.OPERATION_FAILED, `Authentication operation failed: Status code: 0x${statusCode.toString(16)}`);
}
return true;
})();
}
read(blockNumber, length, blockSize = 4, packetSize = 16) {
var _this4 = this;
return _asyncToGenerator(function* () {
if (!_this4.card) {
throw new _errors.ReadError(_errors.CARD_NOT_CONNECTED);
}
_this4.logger.info('reading data from card', _this4.card);
if (length > packetSize) {
const p = Math.ceil(length / packetSize);
const commands = [];
for (let i = 0; i < p; i++) {
const block = blockNumber + i * packetSize / blockSize;
const size = (i + 1) * packetSize < length ? packetSize : length - i * packetSize;
// console.log(i, block, size);
commands.push(_this4.read(block, size, blockSize, packetSize));
}
return Promise.all(commands).then(function (values) {
// console.log(values);
return Buffer.concat(values, length);
});
}
// APDU CMD: Read Binary Blocks
const packet = new Buffer([0xff, // Class
0xb0, // Ins
0x00, // P1
blockNumber, // P2: Block Number
length // Le: Number of Bytes to Read (Maximum 16 bytes)
]);
let response = null;
try {
response = yield _this4.transmit(packet, length + 2);
_this4.logger.info('response received', response);
} catch (err) {
throw new _errors.ReadError(null, null, err);
}
const statusCode = response.slice(-2).readUInt16BE(0);
if (statusCode !== 0x9000) {
throw new _errors.ReadError(_errors.OPERATION_FAILED, `Read operation failed: Status code: 0x${statusCode.toString(16)}`);
}
const data = response.slice(0, -2);
_this4.logger.info('data', data);
return data;
})();
}
write(blockNumber, data, blockSize = 4) {
var _this5 = this;
return _asyncToGenerator(function* () {
if (!_this5.card) {
throw new _errors.WriteError(_errors.CARD_NOT_CONNECTED);
}
_this5.logger.info('writing data to card', _this5.card);
if (data.length < blockSize || data.length % blockSize !== 0) {
throw new _errors.WriteError('invalid_data_length', 'Invalid data length. You can only update the entire data block(s).');
}
if (data.length > blockSize) {
const p = data.length / blockSize;
const commands = [];
for (let i = 0; i < p; i++) {
const block = blockNumber + i;
const start = i * blockSize;
const end = (i + 1) * blockSize;
const part = data.slice(start, end);
// console.log(i, block, start, end, part);
commands.push(_this5.write(block, part, blockSize));
}
return Promise.all(commands).then(function (values) {
// console.log(values);
return values;
});
}
// APDU CMD: Update Binary Block
const packetHeader = new Buffer([0xff, // Class
0xd6, // Ins
0x00, // P1
blockNumber, // P2: Block Number
blockSize]);
const packet = Buffer.concat([packetHeader, data]);
let response = null;
try {
response = yield _this5.transmit(packet, 2);
_this5.logger.info('response received', response);
} catch (err) {
throw new _errors.WriteError(null, null, err);
}
const statusCode = response.readUInt16BE(0);
if (statusCode !== 0x9000) {
throw new _errors.WriteError(_errors.OPERATION_FAILED, `Write operation failed: Status code: 0x${statusCode.toString(16)}`);
}
return true;
})();
}
handleTag() {
if (!this.card) {
return false;
}
this.logger.info('handling tag', this.card);
switch (this.card.standard) {
case TAG_ISO_14443_3:
return this.handle_Iso_14443_3_Tag();
case TAG_ISO_14443_4:
return this.handle_Iso_14443_4_Tag();
default:
return this.handle_Iso_14443_3_Tag();
}
}
// TODO: improve error handling and debugging
handle_Iso_14443_3_Tag() {
var _this6 = this;
return _asyncToGenerator(function* () {
if (!_this6.card || !_this6.connection) {
return false;
}
_this6.logger.info('processing ISO 14443-3 tag', _this6.card);
// APDU CMD: Get Data
const packet = new Buffer([0xff, // Class
0xca, // INS
0x00, // P1: Get current card UID
0x00, // P2
0x00 // Le: Full Length of UID
]);
try {
const response = yield _this6.transmit(packet, 12);
if (response.length < 2) {
const error = new _errors.GetUIDError('invalid_response', `Invalid response length ${response.length}. Expected minimal length was 2 bytes.`);
_this6.emit('error', error);
return;
}
// last 2 bytes are the status code
const statusCode = response.slice(-2).readUInt16BE(0);
// an error occurred
if (statusCode !== 0x9000) {
const error = new _errors.GetUIDError(_errors.OPERATION_FAILED, 'Could not get card UID.');
_this6.emit('error', error);
return;
}
// strip out the status code (the rest is UID)
const uid = response.slice(0, -2).toString('hex');
// const uidReverse = Reader.reverseBuffer(response.slice(0, -2)).toString('hex');
_this6.card.uid = uid;
_this6.emit('card', _extends({}, _this6.card));
} catch (err) {
const error = new _errors.GetUIDError(null, null, err);
_this6.emit('error', error);
}
})();
}
// TODO: improve error handling and debugging
handle_Iso_14443_4_Tag() {
var _this7 = this;
return _asyncToGenerator(function* () {
if (!_this7.card || !_this7.connection) {
return false;
}
_this7.logger.info('processing ISO 14443-4 tag', _this7.card);
if (!_this7._parsedAid) {
const err = new Error('Cannot process ISO 14443-4 tag because AID was not set.');
_this7.emit('error', err);
return;
}
// APDU CMD: Select Apdu
const packetHeader = Buffer.from([0x00, // Class
0xa4, // INS
0x04, // P1
0x00, // P2
0x05 // Le
]);
const aid = Buffer.from(_this7._parsedAid);
const packet = Buffer.concat([packetHeader, aid]);
try {
const response = yield _this7.transmit(packet, 40);
if (response.length === 2 && response.readUInt16BE(0) === 0x6a82) {
const err = new Error(`Not found response. Tag not compatible with AID ${_this7._aid}.`);
_this7.emit('error', err);
return;
}
if (response.length < 2) {
const err = new Error(`Invalid response length ${response.length}. Expected minimal length was 2 bytes.`);
_this7.emit('error', err);
return;
}
// another possibility const statusCode = parseInt(response.slice(-2).toString('hex'), 16)
const statusCode = response.slice(-2).readUInt16BE(0);
// an error occurred
if (statusCode !== 0x9000) {
const err = new Error(`Response status error.`);
_this7.emit('error', err);
return;
}
// strip out the status code
const data = response.slice(0, -2);
_this7.logger.info('Data cropped', data);
_this7.emit('card', _extends({}, _this7.card, {
data: data
}));
} catch (err) {
const error = new _errors.GetUIDError(null, null, err);
_this7.emit('error', error);
}
})();
}
close() {
this.reader.close();
}
}
exports.default = Reader;