UNPKG

@iotize/tap

Version:

IoTize Device client for Javascript

1,489 lines (1,468 loc) 76.8 kB
import { typedArrayToBuffer, bufferToHexString } from '@iotize/common/byte-converter'; import { KaitaiStreamReader, KaitaiStreamWriter } from '@iotize/common/byte-stream'; import { createDebugger } from '@iotize/common/debug'; import { CodeError } from '@iotize/common/error'; import { TapRequestFrame, TapApduRequest, ApduResponse, ResultCode } from '@iotize/tap/client/api'; import { Subject, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; import { Buffer } from 'buffer'; import { TLV } from '@iotize/common/tlv'; const prefix = '@iotize/tap/client/impl'; const debug = createDebugger(prefix); class ConverterError extends CodeError { constructor(code, msg, cause) { super(msg, code); this.cause = cause; } static unexpectedBufferSize(expected, buffer) { return new ConverterError(ConverterError.Code.UnexpectedBufferSizeError, 'Expected buffer of ' + expected + ' byte(s) but found only ' + buffer.length + ' byte(s)'); } static nullBufferError(msg) { return new ConverterError(ConverterError.Code.NullBufferError, msg); } static valueTooBigError(value, maxValue) { return new ConverterError(ConverterError.Code.ValueTooBigError, `Value ${value} is not acceptable. Maximum authorized value is ${maxValue}.`); } } (function (ConverterError) { let Code; (function (Code) { Code["UnexpectedBufferSizeError"] = "UnexpectedBufferSizeError"; Code["NullBufferError"] = "NullBufferError"; Code["ValueTooBigError"] = "ConverterErrorValueTooBigError"; })(Code = ConverterError.Code || (ConverterError.Code = {})); })(ConverterError || (ConverterError = {})); class NumberConverter { constructor(options) { this.options = options; } /** * Number as unsigned 8 bits converter * @param lsb true for least significant bit first */ static uint8(lsb = false) { return new NumberConverter({ signed: false, sizeOf: 8, leastSignificantBitFirst: lsb, }); } /** * Number as unsigned 16 bits converter * @param lsb true for least significant bit first */ static uint16(lsb = false) { return new NumberConverter({ signed: false, sizeOf: 16, leastSignificantBitFirst: lsb, }); } /** * Number as unsigned 32 bits converter * @param lsb true for least significant bit first */ static uint32(lsb = false) { return new NumberConverter({ signed: false, sizeOf: 32, leastSignificantBitFirst: lsb, }); } /** * Number as signed 8 bits converter * @param lsb true for least significant bit first */ static int8(lsb = false) { return new NumberConverter({ signed: true, sizeOf: 8, leastSignificantBitFirst: lsb, }); } /** * Number as signed 16 bits converter * @param lsb true for least significant bit first */ static int16(lsb = false) { return new NumberConverter({ signed: true, sizeOf: 16, leastSignificantBitFirst: lsb, }); } /** * Number as signed 32 bits converter * @param lsb true for least significant bit first */ static int32(lsb = false) { return new NumberConverter({ signed: true, sizeOf: 32, leastSignificantBitFirst: lsb, }); } decode(body) { if (body.length * 8 < this.options.sizeOf) { throw ConverterError.unexpectedBufferSize(this.options.sizeOf / 8, body); } return NumberConverter.fromBytes(body, this.options.sizeOf / 8, this.options.signed, this.options.leastSignificantBitFirst); } encode(value) { const maxValue = Math.pow(2, this.options.sizeOf) - 1; if (value > maxValue) { throw ConverterError.valueTooBigError(value, maxValue); } const result = new Uint8Array(this.options.sizeOf >> 3); for (var i = 0; i < result.length; i++) { const decalage = (result.length - i - 1) * 8; result[i] = (value >> decalage) & 0xff; } if (this.options.leastSignificantBitFirst) { result.reverse(); } return result; } static toOpaqueMsb(input, sizeOf) { const maxValue = Math.pow(2, sizeOf) - 1; if (input > maxValue) { throw ConverterError.valueTooBigError(input, maxValue); } if (sizeOf === 8) { return new Uint8Array([input & 0xff]); } else if (sizeOf === 16) { return new Uint8Array([(input >> 8) & 0xff, input & 0xff]); } else if (sizeOf === 32) { return new Uint8Array([ (input >> 24) & 0xff, (input >> 16) & 0xff, (input >> 8) & 0xff, input & 0xff, ]); } else { throw new Error(`Invalid size ${sizeOf}. Must be 8, 16 or 32`); } } static fromOpaqueMsb(data, sizeOf) { if (!sizeOf) { sizeOf = data.length * 8; } if (sizeOf === 8) { return data[0] & 0xff; } if (sizeOf === 16) { return (data[1] & 0xff) + ((data[0] & 0xff) << 8); } if (sizeOf === 32) { return ((data[3] & 0xff) + ((data[2] & 0xff) << 8) + ((data[1] & 0xff) << 16) + ((data[0] & 0xff) << 24)); } throw new Error(`Invalid size ${sizeOf}. Must be 8, 16 or 32`); } /** * @param size size in bytes */ static fromBytes(data, size, signed, lsb) { if (lsb) { data = data.reverse(); } var result = 0; var last = 0; var decalage; for (var i = 0; i < size; i++) { decalage = (size - i - 1) * 8; result += ((data[i] & 0xff) << decalage) >>> 0; if (last > result && !signed) { throw new Error('Overflow. Last value: ' + last + ' but new is ' + result); } last = result; } if (signed && (data[0] & 0x80) === 0x80) { result = -(Math.pow(2, 8 * size) - result); } return result; } } let _mpeg2Instance; class CRC { constructor(polynomial) { this.polynomial = polynomial; } static mpeg2() { if (!_mpeg2Instance) { _mpeg2Instance = new CRC(CRC.POLYNOMIAL_CRC32_MPEG2); } return _mpeg2Instance; } compute(buffer, firstValue = CRC.FIRST_VALUE) { if (buffer instanceof Uint32Array) { return CRC.fromWord(buffer, firstValue, this.polynomial); } else { return CRC.fromBytes(buffer, firstValue, this.polynomial); } } static fromBytes(data, crc = CRC.FIRST_VALUE, polynomial = CRC.POLYNOMIAL_CRC32_MPEG2) { if (data.length % CRC.MODULO !== 0) { throw new Error(`Cannot compute CRC, input size must be a multiple of 4. Length is ${data.length} % ${CRC.MODULO} = ${data.length % CRC.MODULO}`); } const uint32Data = new Uint32Array(data.length / 4); for (let offset = 0; offset < uint32Data.byteLength; offset++) { uint32Data[offset] = NumberConverter.fromOpaqueMsb(data.slice(offset * 4, (offset + 1) * 4), 32); } return CRC.fromWord(uint32Data, crc, polynomial); } // crc 32 calculation by word. static fromWord(buffer, crc = CRC.FIRST_VALUE, polynomial = CRC.POLYNOMIAL_CRC32_MPEG2) { // don't follow null pointers if (!buffer) { throw new Error('Buffer is null'); } // don't accept buffers larger than 16MB if (buffer.byteLength > 16 * 1024 * 1024) { throw new Error('Buffers larger than 16MB'); } let myword; for (let i = 0; i < buffer.length; i++) { myword = buffer[i]; // Get next word. crc = CRC.appendCrcNumber(crc, myword, polynomial); } return crc; } static appendCrcNumber(crc, data, polynomial) { crc = crc ^ data; for (let i = 0; i < 32; i++) { if (crc & 0x80000000) { crc = ((crc << 1) ^ polynomial) >>> 0; } else { crc = (crc << 1) >>> 0; } } return crc; } } CRC.POLYNOMIAL_CRC32_MPEG2 = 0x4c11db7; CRC.MODULO = 4; CRC.FIRST_VALUE = 0xffffffff; function enumKeyOrValueToNumber(key, mapping) { if (typeof key === 'string') { // if (!(key in mapping)) { // throw new Error(``) // } return mapping[key]; } else { return key; } } class TapStreamReader extends KaitaiStreamReader { static fromArray(array) { const buffer = typedArrayToBuffer(array); return new TapStreamReader(buffer); } static create(packet) { return packet instanceof Uint8Array ? TapStreamReader.fromArray(packet) : packet; } readStr(n) { return String.fromCharCode.apply(null, this.readBytes(n)); } /** * Read given byte size as string * @param n number of byte to read */ readString(n = this.byteLeft) { return this.readStr(n); } /** * Read string until terminator character is reached */ readStringTerminator(terminator) { let result = ''; while (true) { const lastByte = this.readU1(); const lastChar = String.fromCharCode(lastByte); if (lastChar !== terminator) { result += lastChar; } else { break; } } return result; } readU2() { return this._isBigEndian ? this.readU2be() : this.readU2le(); } readU4() { return this._isBigEndian ? this.readU4be() : this.readU4le(); } readF4() { return this._isBigEndian ? this.readF4be() : this.readF4le(); } subStream(length) { return TapStreamReader.fromArray(this.readBytes(length)); } } class TapStreamWriter extends KaitaiStreamWriter { constructor(lengthOrBuffer = 512, byteOffset, isBigEndian = true) { if (typeof lengthOrBuffer === 'number') { lengthOrBuffer = new ArrayBuffer(lengthOrBuffer); } super(lengthOrBuffer, byteOffset, isBigEndian); } static create(size) { return new TapStreamWriter(new ArrayBuffer(size)); } writeFunction(type, options, fieldValue, fieldSize) { switch (type) { case 'crc32': return this.addCRC(options); case 'padding': if (fieldValue) { debug('Padding is already set'); return this.writeBytes(fieldValue); } else { if (typeof fieldSize == undefined) { throw new Error(`Padding function expect padding size as a second argument. Args: ${JSON.stringify(arguments)}`); } return this.addPadding(fieldSize); } default: throw new Error(`Invalid writer function type: ${type}`); } } addPadding(padSize) { this.writeBytes(new Uint8Array(padSize)); return this; } addCRC(options) { let buffer = this.toBytes; if (options && options.offset) { buffer = buffer.slice(options.offset); } const crc = CRC.fromBytes(buffer); this.writeU4(crc); // console.info(`TODO REMOVE Compute CRC from ${options ? options.offset : 0}:${buffer.length} 0x${bufferToHexString(buffer)} => 0x${crc.toString(16)}`) return this; } writeUnsigned(value, length) { this.writeU(value, length); return this; } writeU4(value) { this.writeU(value, 4); return this; } writeU2(value) { this.writeU(value, 2); return this; } writeU1(value) { this.writeU(value, 1); return this; } writeF4(value) { this.writeF(value, 4); return this; } writeFloat(value, length) { this.writeF(value, length); return this; } writeSigned(value, length) { this.writeSigned(value, length); return this; } writeString(value, length = (value === null || value === void 0 ? void 0 : value.length) || 0) { return this.writeStr(value || '', length); } writeStr(value, length = value.length) { let i = 1; const stringWriteLength = Math.min(value.length, length); for (; i <= stringWriteLength; i++) { this.writeByte(value.charCodeAt(i - 1)); } const remaining = length - stringWriteLength; if (remaining > 0) { this.writeBytes(new Uint8Array(remaining)); } return this; } writeBitsInt(value, n) { this.writeB(value, n); return this; } writeBits(value, n) { this.writeB(value, n); return this; } writeBoolean(value, n) { this.writeBits(value ? 1 : 0, n); return this; } } /** * Generated file. Do not edit */ TapStreamReader.prototype.readApduRequest = function () { const model = {}; model.header = this.readApduRequestHeader(); model.data = this.readBytes(model.header.lc); return model; }; TapStreamReader.prototype.readApduRequestHeader = function () { const model = {}; model.cla = this.readUnsigned(1); model.ins = this.readUnsigned(1); model.p1 = this.readUnsigned(1); model.p2 = this.readUnsigned(1); model.lc = this.readUnsigned(1); return model; }; TapStreamReader.prototype.readApduResponse = function () { const model = {}; model.data = this.readBytes(this.getStreamSize() - 2); model.status = this.readUnsigned(2); return model; }; TapStreamReader.prototype.readTapRequestEncrypted = function () { const model = {}; model.header = this.readApduRequestHeader(); model.request = this.readTapRequestFrame(); return model; }; TapStreamReader.prototype.readTapApduRequest = function () { const model = {}; return model; }; TapStreamReader.prototype.readTapEncryptedFrame = function () { const model = {}; model.id = this.readUnsigned(2); model.len = this.readUnsigned(2); model.payload = this.readBytes(model.len); model.padding = this.readBytes((16 - ((2 + 2 + (model.len === undefined ? 0 : model.len) + 4) % 16)) % 16); model.crc = this.readUnsigned(4); return model; }; TapStreamReader.prototype.readTapRequestFrame = function () { const model = {}; model.header = this.readTapRequestFrameHeader(); model.payload = this.readBytes(); return model; }; TapStreamReader.prototype.readTapRequestFrameHeader = function () { const model = {}; model.methodType = this.readUnsigned(1); model.path = this.readTapRequestFramePath(); return model; }; TapStreamReader.prototype.readTapRequestFramePath = function () { const model = {}; model.objectId = this.readUnsigned(2); model.objectInstanceId = this.readUnsigned(2); model.resourceId = this.readUnsigned(2); return model; }; TapStreamReader.prototype.readTapResponseFrame = function () { const model = {}; model.status = this.readUnsigned(1); model.data = this.readBytes(); return model; }; // TapStreamWriter.prototype.write(model: ApduRequest) : TapStreamWriter{ // return this.writeApduRequest(model) // } TapStreamWriter.prototype.writeApduRequest = function (model) { this.writeApduRequestHeader(model.header); this.writeBytes(model.data, model.header.lc); return this; }; TapStreamWriter.prototype.writeApduRequestHeader = function (model) { this.writeUnsigned(model.cla, 1); this.writeUnsigned(model.ins, 1); this.writeUnsigned(model.p1, 1); this.writeUnsigned(model.p2, 1); this.writeUnsigned(model.lc, 1); return this; }; // TapStreamWriter.prototype.write(model: ApduResponse) : TapStreamWriter{ // return this.writeApduResponse(model) // } TapStreamWriter.prototype.writeApduResponse = function (model) { this.writeBytes(model.data, undefined); this.writeUnsigned(model.status, 2); return this; }; // TapStreamWriter.prototype.write(model: TapRequestEncrypted) : TapStreamWriter{ // return this.writeTapRequestEncrypted(model) // } TapStreamWriter.prototype.writeTapRequestEncrypted = function (model) { this.writeApduRequestHeader(model.header); this.writeTapRequestFrame(model.request); return this; }; // TapStreamWriter.prototype.write(model: TapApduRequest) : TapStreamWriter{ // return this.writeTapApduRequest(model) // } TapStreamWriter.prototype.writeTapApduRequest = function (model) { return this; }; // TapStreamWriter.prototype.write(model: TapEncryptedFrame) : TapStreamWriter{ // return this.writeTapEncryptedFrame(model) // } TapStreamWriter.prototype.writeTapEncryptedFrame = function (model) { this.writeUnsigned(model.id, 2); this.writeUnsigned(model.len, 2); this.writeBytes(model.payload, model.len); this.writeFunction('padding', undefined, model.padding, (16 - ((2 + 2 + (model.len === undefined ? 0 : model.len) + 4) % 16)) % 16); this.writeFunction('crc32', undefined, model.crc, 4); return this; }; // TapStreamWriter.prototype.write(model: TapRequestFrame) : TapStreamWriter{ // return this.writeTapRequestFrame(model) // } TapStreamWriter.prototype.writeTapRequestFrame = function (model) { this.writeTapRequestFrameHeader(model.header); this.writeBytes(model.payload); return this; }; TapStreamWriter.prototype.writeTapRequestFrameHeader = function (model) { this.writeBitsInt(enumKeyOrValueToNumber(model.methodType, TapRequestFrame.MethodType), 8); this.writeTapRequestFramePath(model.path); return this; }; TapStreamWriter.prototype.writeTapRequestFramePath = function (model) { this.writeUnsigned(model.objectId, 2); this.writeUnsigned(model.objectInstanceId, 2); this.writeUnsigned(model.resourceId, 2); return this; }; // TapStreamWriter.prototype.write(model: TapResponseFrame) : TapStreamWriter{ // return this.writeTapResponseFrame(model) // } TapStreamWriter.prototype.writeTapResponseFrame = function (model) { this.writeUnsigned(model.status, 1); this.writeBytes(model.data); return this; }; /** * Generated file. Do not edit */ class ApduRequestConverter { encode(model, stream = new TapStreamWriter()) { stream.writeApduRequest(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readApduRequest(); } } class ApduResponseConverter { encode(model, stream = new TapStreamWriter()) { stream.writeApduResponse(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readApduResponse(); } } class TapRequestEncryptedConverter { encode(model, stream = new TapStreamWriter()) { stream.writeTapRequestEncrypted(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readTapRequestEncrypted(); } } class TapApduRequestConverter$1 { encode(model, stream = new TapStreamWriter()) { stream.writeTapApduRequest(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readTapApduRequest(); } } class TapEncryptedFrameConverter { encode(model, stream = new TapStreamWriter()) { stream.writeTapEncryptedFrame(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readTapEncryptedFrame(); } } class TapRequestFrameConverter { encode(model, stream = new TapStreamWriter()) { stream.writeTapRequestFrame(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readTapRequestFrame(); } } class TapResponseFrameConverter { encode(model, stream = new TapStreamWriter()) { stream.writeTapResponseFrame(model); return stream.toBytes; } decode(data) { const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data); return stream.readTapResponseFrame(); } } const generatedWriteApduRequestHeader = TapStreamWriter.prototype.writeApduRequestHeader; TapStreamWriter.prototype.writeApduRequestHeader = function (model) { if (model.lc > 0xff) { this.writeUnsigned(model.cla, 1); this.writeUnsigned(model.ins, 1); this.writeUnsigned(model.p1, 1); this.writeUnsigned(model.p2, 1); this.writeUnsigned(0, 1); this.writeUnsigned(model.lc, 2); return this; } else { return generatedWriteApduRequestHeader.apply(this, [model]); } }; function createApduFromTapRequest(request) { const data = new TapRequestFrameConverter().encode(request); // TODO // TODO check if size > 255 ? let ins; switch (request.header.methodType) { case TapRequestFrame.MethodType.GET: ins = TapApduRequest.MethodType.GET; break; case TapRequestFrame.MethodType.PUT: case TapRequestFrame.MethodType.POST: ins = TapApduRequest.MethodType.PUT_OR_POST; break; default: throw new Error(`Internal error: request type "${request.header.methodType}" is invalid`); } return { header: { cla: TapApduRequest.Default.CLA, ins, p1: 0, p2: 0, lc: data.length, }, data, }; } class TapRequestFrameBuilder { // public static fromBytes(bytes: Uint8Array): Header { // const path = TapStreamReader.fromArray(bytes).readTapRequestPath(); // const header = new Header(path); // if (bytes.length == 8) { // // What is that ??? // header.resourceInstance = // ((bytes[6] << 8) & Header.MAX_ID) + (bytes[7] & 0xff); // header.resourceInstance &= Header.MAX_ID; // } // // return header; // return header; // } static parsePath(path) { if (!path || path.length === 0) { throw new Error('Missing LWM2M path, it must not be empty'); // TODO better error } path = path.trim(); const result = path.match(TapRequestFrameBuilder.PATH_EXPRESSION); if (!result || result.length === 0) { throw new Error(`Invalid LWM2M path "${path}", it does not match expected format`); // TODO better error } const header = { objectId: this.validateId(parseInt(result[1])), objectInstanceId: result[2] ? parseInt(result[2]) : 0xffff, resourceId: parseInt(result[3]), }; if (result[4]) { header['resourceInstanceId'] = parseInt(result[4], 10); } return header; } static GET(url, body) { return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.GET, url, body); } static PUT(url, body) { return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.PUT, url, body); } static POST(url, body) { return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.POST, url, body); } static create(method, url, data) { return { header: { path: TapRequestFrameBuilder.parsePath(url), methodType: method, }, payload: data || new Uint8Array(), }; } static validateId(id) { if (id < 0 || id > TapRequestFrameBuilder.MAX_ID) { throw new Error(`Invalid identifier "${id}". It must be a number between ${id} and ${TapRequestFrameBuilder.MAX_ID}`); } return id; } } TapRequestFrameBuilder.MAX_ID = 0xffff; (function (TapRequestFrameBuilder) { TapRequestFrameBuilder.PATH_EXPRESSION = /^\/?([0-9]+)\/([0-9]+)?\/([0-9]+)\/?([0-9]+)?$/; })(TapRequestFrameBuilder || (TapRequestFrameBuilder = {})); class TapRequestHelper { // static toString(request: TapRequestFrame) { // return `${TapRequestFrame.MethodType[request.header.methodType]} ${TapRequestHelper.pathToString(request.header.path)}`; // } static toString(request, options) { let result = `${TapRequestFrame.MethodType[request.header.methodType]} ${TapRequestHelper.pathToString(request.header.path, options)}`; if (request.payload && request.payload.length > 0) { result += ` 0x${bufferToHexString(request.payload)}`; } return result; } static pathToString(path, options) { const resourceInstanceId = path['resourceInstanceId']; return ('/' + path.objectId + '/' + (path.objectInstanceId === TapRequestFrameBuilder.MAX_ID ? (options === null || options === void 0 ? void 0 : options.printMaxId) ? path.objectInstanceId : '' : path.objectInstanceId) + '/' + path.resourceId + (resourceInstanceId ? '/' + resourceInstanceId : '')); } } /** * Generated file. Do not edit */ const ResultCodeTranslation = { /** * OK * decimal: 0 */ 0x0: 'The request was successful', /** * CREATED * decimal: 65 */ 0x41: 'Create request was successful', /** * DELETED * decimal: 66 */ 0x42: 'Delete request was successful', /** * CHANGED * decimal: 68 */ 0x44: 'Update request was successful', /** * CONTENT * decimal: 69 */ 0x45: 'The server has fulfilled the request and sent a response', /** * BUSY * decimal: 70 */ 0x46: 'The server is busy', /** * BAD_REQUEST * decimal: 128 */ 0x80: 'Bad request. Meaning device cannot understand your request.', /** * UNAUTHORIZED * decimal: 129 */ 0x81: 'Current user is not authorized to access this ressource', /** * NOT_FOUND * decimal: 132 */ 0x84: 'The server has not found anything matching the Request', /** * METHOD_NOT_ALLOWED * decimal: 133 */ 0x85: 'Current user is not authorized to access this ressource', /** * NOT_ACCEPTABLE * decimal: 134 */ 0x86: 'Given parameters are not acceptable', /** * RESOURCE_LOCKED * decimal: 135 */ 0x87: 'Given resource is not available with the current TAP configuration.', /** * INTERNAL_SERVER_ERROR * decimal: 160 */ 0xa0: 'Internal server error', /** * NOT_IMPLEMENTED * decimal: 161 */ 0xa1: 'This resource has not been implemented yet', /** * SERVICE_UNAVAILABLE * decimal: 163 */ 0xa3: 'Service is currently unavailable. Try again later.', /** * NVM_ERROR * decimal: 164 */ 0xa4: 'Writing non volatible memory failed', /** * UNABLE_TO_CONNECT_TO_TARGET * decimal: 165 */ 0xa5: 'Tap was not able to connect to the target', /** * TARGET_POWER_FAILURE * decimal: 166 */ 0xa6: 'Target power failure', /** * NVM_FULL * decimal: 167 */ 0xa7: 'Non volatile memory is full', /** * DEVICE_UNAVAILABLE * decimal: 168 */ 0xa8: "It means we can't access this resource because someone is already connected to this device and he has the priority over us.", /** * RESPONSE_TOO_LARGE * decimal: 169 */ 0xa9: 'Response is bigger than you current protocol limit', /** * TARGET_PROTOCOL_ERROR * decimal: 176 */ 0xb0: 'Target protocol error', /** * TARGET_PROTOCOL_BUSY * decimal: 177 */ 0xb1: 'Target protocol is busy', /** * TARGET_PROTOCOL_REAL * decimal: 178 */ 0xb2: 'Target protocol error', /** * TARGET_PROTOCOL_WRONG_PARAM * decimal: 179 */ 0xb3: 'Target protocol parameters are not valid', /** * TARGET_PROTOCOL_FORBIDDEN * decimal: 180 */ 0xb4: 'Target protocol is forbidden', /** * TARGET_PROTOCOL_DATA * decimal: 181 */ 0xb5: 'Target protocol data are not the one expected. For example a CRC error.', /** * TARGET_PROTOCOL_COM * decimal: 182 */ 0xb6: 'Target protocol communication error', /** * TARGET_PROTOCOL_LOST_COM * decimal: 183 */ 0xb7: 'Target protocol lost communication', /** * TARGET_PROTOCOL_PARAM * decimal: 184 */ 0xb8: 'Target protocol invalid parameter', /** * TARGET_PROTOCOL_INT * decimal: 185 */ 0xb9: 'Internal error with target communication', /** * TARGET_PROTOCOL_ABORT * decimal: 186 */ 0xba: 'Operation aborted because one of the command did not return the expected result code.', /** * TARGET_PROTOCOL_NOT_IMPLEMENTED * decimal: 187 */ 0xbb: 'Target protocol is not implemented', }; class TapClientError extends CodeError { constructor(code, message, cause) { super(message, code); this.cause = cause; } static illegalArgument(msg) { return new TapClientError(TapClientError.Code.IllegalArgumentError, msg); } static illegalStateError(msg) { return new TapClientError(TapClientError.Code.IllegalStateError, msg); } static encodeRequestError(command, cause) { return new TapClientError(TapClientError.Code.EncodeRequestError, 'Cannot encode this request: ' + command + '. Cause: ' + cause, cause); } static decodeResponseError(cause, frameOrCommand) { return new TapClientError(TapClientError.Code.DecodeResponseError, `Cannot decode response to command ${frameOrCommand instanceof Uint8Array ? `0x${bufferToHexString(frameOrCommand)}` : TapRequestHelper.toString(frameOrCommand)}. Cause: ` + cause, cause); } static notConnectedError() { return new TapClientError(TapClientError.Code.NotConnectedError, 'Trying to execute command but device is not connected'); } static cannotDecodeResponseError(err, bufferData) { return new TapClientError(TapClientError.Code.UnexpectedTapResponse, `Received from device an unexpected response that cannot be decoded (${err.message}). Frame: 0x${bufferToHexString(bufferData)}`); } static unexpectedApduStatusCode(apduResponse) { return new TapClientError(TapClientError.Code.UnexpectedApduResponseStatus, `Received from device an unexpected response with an invalid APDU status code 0x${apduResponse.status.toString(16)} but expecting 0x${ApduResponse.Status.OK.toString(16)}`); } static cannotEncodeRequest(err, tapRequest) { return new TapClientError(TapClientError.Code.CannotEncodeRequest, `Cannot encode request ${tapRequest}: ${err.message}`); } } (function (TapClientError) { let Code; (function (Code) { Code["NotConnectedError"] = "TapClientErrorNotConnected"; Code["EncodeRequestError"] = "TapClientErrorEncodeRequest"; Code["IllegalArgumentError"] = "TapClientErrorIllegalArgument"; Code["IllegalStateError"] = "TapClientErrorIllegalState"; Code["DecodeResponseError"] = "TapClientErrorDecodeResponse"; Code["UnexpectedTapResponse"] = "TapClientErrorUnexpectedTapResponse"; Code["UnexpectedApduResponseStatus"] = "TapClientErrorUnexpectedApduResponseStatus"; Code["CannotEncodeRequest"] = "TapClientErrorCannotEncodeRequest"; Code["TapClientResponseStatusError"] = "TapClientErrorResponseStatus"; })(Code = TapClientError.Code || (TapClientError.Code = {})); })(TapClientError || (TapClientError = {})); // export const TapResponseErrorCode = 'TapResponseError'; class TapClientResponseStatusError extends TapClientError { // public code: string; constructor(response, request) { super(TapClientError.Code.TapClientResponseStatusError, TapClientResponseStatusError.createErrorMessage(response, request)); this.response = response; this.request = request; } static createErrorMessage(response, request) { const codeRet = response.status; let msg = 'Tap request failed. '; if (request) { msg = `Tap request ${TapRequestHelper.toString(request)} failed. `; } const errorExplained = codeRet in ResultCodeTranslation ? ResultCodeTranslation[codeRet] : 'an unknown error'; msg += `${errorExplained} (code=0x${codeRet.toString(16)})`; return msg; } } class TapApduRequestConverter { encode(tapRequest) { try { const stream = new TapStreamWriter(5 + 1 + tapRequest.payload.length + 1); stream.writeApduRequest(createApduFromTapRequest(tapRequest)); return stream.toBytes; } catch (err) { if (!(err instanceof TapClientError)) { throw TapClientError.cannotEncodeRequest(err, tapRequest); } else { throw err; } } } decode(bufferData) { const stream = bufferData instanceof TapStreamReader ? bufferData : TapStreamReader.create(bufferData); const apduHeader = stream.readApduRequestHeader(); const tapRequestFrame = stream.readTapRequestFrame(); return tapRequestFrame; } } class TapApduResponseConverter { encode(tapResponseFrame) { const stream = new TapStreamWriter(); const tapRequestStream = new TapStreamWriter(); tapRequestStream.writeTapResponseFrame(tapResponseFrame); stream.writeApduResponse({ data: tapRequestStream.toBytes, status: ApduResponse.Status.OK, }); return stream.toBytes; } decode(bufferData) { try { const stream = bufferData instanceof TapStreamReader ? bufferData : TapStreamReader.create(bufferData); const apduResponse = stream.readApduResponse(); if (apduResponse.status !== ApduResponse.Status.OK) { throw TapClientError.unexpectedApduStatusCode(apduResponse); } return TapStreamReader.fromArray(apduResponse.data).readTapResponseFrame(); } catch (err) { if (!(err instanceof TapClientError)) { throw TapClientError.cannotDecodeResponseError(err, bufferData instanceof TapStreamReader ? bufferData.toBytes : bufferData); } else { throw err; } } } } const TAP_REQUEST_FRAME_CONVERTER = new TapApduRequestConverter(); const TAP_RESPONSE_FRAME_CONVERTER = new TapApduResponseConverter(); class InterceptorChain { constructor(target) { this.target = target; this.interceptors = []; } addInterceptor(interceptor) { this.interceptors.push(typeof interceptor === 'function' ? { intercept: interceptor, } : interceptor); return this; } execute(context) { const runner = { index: 0, run: (context) => { if (runner.index < this.interceptors.length) { runner.index++; return this.interceptors[runner.index - 1].intercept(context, { handle: (context) => { return runner.run(context); }, }); } else { return this.target(context); } }, }; return runner.run(context); } } const TAG$1 = 'Client'; class TapClient { constructor(requestEncoder, responseDecoder) { this._onProtocolChange = new Subject(); this._commandEncoder = requestEncoder; this._responseDecoder = responseDecoder; this.protocols = {}; this._interceptorChain = new InterceptorChain((context) => { try { const command = context.request; // const bodyDecoder = context.bodyDecoder; return this._command(command); } catch (err) { return throwError(err); } }); } static create(protocol) { const client = new TapClient(TAP_REQUEST_FRAME_CONVERTER, TAP_RESPONSE_FRAME_CONVERTER); if (protocol) { client.addComProtocol(protocol); } return client; } // public isEncryptionEnabled(): boolean { // return this._options.encryption; // } // public get cryptedFrameConverter() { // return this._cryptedFrameConverter; // } get interceptorChain() { return this._interceptorChain; } addInterceptor(interceptor) { this.interceptorChain.addInterceptor(interceptor); return this; } // /** // * Enable/Disable encrytion when communicating with a device // * @param value true if requests must be encrypted // * @param algo optional if you have already provided the algo with @{link setEncryptionAlgo(algo)} method // */ // public enableEncryption(value: boolean, algo?: EncryptionAlgo): Promise<any> { // if (value) { // algo = algo || this._encryptionAlgo; // if (!algo) { // return Promise.reject( // TapClientError.illegalArgument( // 'Illegal argument, encryption algo must be set' // ) // ); // } // return this._startEncryption(algo); // } // else { // return this._stopEncryption(); // } // } isConnected() { return (this.hasProtocol(this.currentProtocolId) && this.getCurrentProtocol().isConnected()); } /** * see {@link getCurrentProtocol} */ get protocol() { return this.getCurrentProtocol(); } get commandEncoder() { return this._commandEncoder; } get responseDecoder() { return this._responseDecoder; } /** * Get current protocol or throw an error if no protocol */ getCurrentProtocol() { if (!this.currentProtocolId) { throw TapClientError.illegalStateError('Tap client protocol is required'); } if (!this.hasProtocol(this.currentProtocolId)) { throw TapClientError.illegalArgument('Protocol ' + this.currentProtocolId + ' does not exist'); } return this.protocols[this.currentProtocolId]; } /** * Register/replace a communication protocol with given id * If no communication protocol is selected yet, it will select it. * @param newProtocol * @param id */ addComProtocol(newProtocol, id = 'default') { this.protocols[id] = newProtocol; if (!this.currentProtocolId) { this.currentProtocolId = id; } return this; } /** * Switch to given communication protocol identifier * @throws if protocol identifier does not exist * @param name */ switchProtocol(name) { if (!this.hasProtocol(name)) { throw TapClientError.illegalArgument('Unkonwn protocol: ' + name); } const oldProtocol = this.getCurrentProtocol(); this.currentProtocolId = name; debug(TAG$1, `Changing protocol to: ${name}`); this._onProtocolChange.next({ newProtocol: this.getCurrentProtocol(), oldProtocol, }); return this; } /** * Change communication protocol * @throws if protocol identifier does not exist * @param protocol can either be the communication protocol instance or a string identifier */ useComProtocol(protocol) { let protocolId; if (typeof protocol == 'string') { protocolId = protocol; } else { protocolId = protocol.constructor.name; this.addComProtocol(protocol, protocolId); } this.switchProtocol(protocolId); return this; } /** * Return true if protocol identifier is registered * @param id */ hasProtocol(id) { return id in this.protocols; } connect() { try { return this.getCurrentProtocol().connect(); } catch (err) { return throwError(err); } } disconnect() { try { return this.getCurrentProtocol().disconnect(); } catch (err) { return throwError(err); } } /** * Send request * @param request * @param bodyDecoder */ request(request) { return this._interceptorChain.execute({ // bodyDecoder, request, client: this, }); } /** * Send raw request * No interceptors/No encryption will be triggered * @param dataIn request bytes * @param bodyDecoder optional body decoder to use */ send(dataIn) { try { return this.getCurrentProtocol() .send(dataIn) .pipe(map((result) => { // debug( // TAG, // 'Received result: ', // bufferToHexString(result), // 'DECODER: ', // bodyDecoder ? bodyDecoder.constructor.name : 'NO' // ); try { return this._responseDecoder.decode(result); } catch (err) { throw TapClientError.decodeResponseError(err, dataIn); } })); } catch (err) { return throwError(err); } } /** * Listen to connection state change event * We have to resubscribe when protocol change.. */ onConnectionStateChange() { try { return this.getCurrentProtocol().onConnectionStateChange(); } catch (err) { return throwError(err); } } onProtocolChange() { try { return this._onProtocolChange.asObservable(); } catch (err) { return throwError(err); } } _command(command) { try { if (!this.isConnected()) { return throwError(TapClientError.notConnectedError()); } let dataIn; try { dataIn = this._commandEncoder.encode(command); } catch (e) { return throwError(TapClientError.encodeRequestError(command, e)); } debug(TAG$1, `Sending ${TapRequestHelper.toString(command)} (${dataIn.length} bytes 0x${bufferToHexString(dataIn)})`); return this.getCurrentProtocol() .send(dataIn) .pipe(map((result) => { // debug( // TAG, // 'Received result: ', // bufferToHexString(result), // 'DECODER: ', // bodyDecoder ? bodyDecoder.constructor.name : 'NO' // ); try { const response = this._responseDecoder.decode(result); // if (bodyDecoder) { // response.setBodyDecoder(bodyDecoder); // } return response; } catch (err) { throw TapClientError.decodeResponseError(err, command); } })); } catch (err) { return throwError(err); } } } class TapResponseFrameBuilder { static SUCCESS(data) { return TapResponseFrameBuilder.create(ResultCode.OK, data); } static ERROR(status = ResultCode.BAD_REQUEST, data) { return TapResponseFrameBuilder.create(status, data); } static create(status, data) { return { status, data: data || new Uint8Array(), }; } } function tapResponseStatusToString(status) { return ResultCodeTranslation[status] ? ResultCodeTranslation[status] : 'Unknown tap response status'; } class ArrayConverter { constructor(_itemConverter, options) { this._itemConverter = _itemConverter; this.options = options; } get itemConverter() { return this._itemConverter; } decode(bytes) { let items = []; let sizeOfItem = this.options.sizeOfItem; for (let offset = 0; offset < bytes.length; offset += sizeOfItem) { let slice = bytes.subarray(offset, offset + sizeOfItem); let item = this._itemConverter.decode(slice); items.push(item); } return items; } encode(items) { let result = new Uint8Array(this.options.sizeOfItem * items.length); let offset = 0; for (let item of items) { let encodedItem = this._itemConverter.encode(item); // TODO if (encodedItem.length > this.options.sizeOfItem) { throw new Error(`Encoded item size overflow: ${encodedItem.length} (max is ${this.options.sizeOfItem})`); } result.set(encodedItem, offset); offset += this.options.sizeOfItem; } return result; } } class BooleanConverter { constructor(_mask = 0x1) { this._mask = _mask; } decode(data) { var value = NumberConverter.fromBytes(data, data.length, false, BooleanConverter.IS_LEAST_SIGNIFICANT_BIT_FIRST); return BooleanConverter.decodeFromNumber(this._mask, value); } encode(value) { return Uint8Array.from([value ? this._mask : 0]); } static decodeFromNumber(mask, value) { return (mask == 0 && value == 0) || (value & mask) != 0; } static instanceBit0() { if (!BooleanConverter._instance) { BooleanConverter._instance = new BooleanConverter(0b1); } return BooleanConverter._instance; } } BooleanConverter.IS_LEAST_SIGNIFICANT_BIT_FIRST = true; BooleanConverter._instance = undefined; const BYTE_SWAP_ORDER_REGEX = /^(B[0-9]+)(_B[0-9]+)*$/; class ByteSwapConverter { constructor(order) { this.order = order; } encode(input) { return swapBytes(input, this.order); } decode(input) { return this.encode(input); } } /** * Swap byte position * B0 represents the first byte from the right * BN represents the N byte from the right * * This function is symetric * * Example: * ```typescript * const input = Uint8Array.from([1,2,3,4]); * expect(swapBytes(input, 'B2_B3_B0_B1')).to.be.deep.eq(Uint8Array.from([2,1,4,3])); * ``` * * @param input array * @param order string that defined how to swap bytes * @return a new array with byte swap */ function swapBytes(input, order) { if (!order.match(BYTE_SWAP_ORDER_REGEX)) { throw new Error(`swatpBytes error, invalid byte order "${order}". Should match regex ${BYTE_SWAP_ORDER_REGEX}`); } const parts = order.split('_'); // // C