From 777412b204de5993e2975ad20091080b9e0e088e Mon Sep 17 00:00:00 2001 From: IoTPlumber Date: Mon, 15 Apr 2024 19:57:19 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20(core):=20Add=20ApduParser=20to?= =?UTF-8?q?=20core=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/apdu/utils/ApduParser.test.ts | 443 ++++++++++++++++++ .../core/src/api/apdu/utils/ApduParser.ts | 131 ++++++ 2 files changed, 574 insertions(+) create mode 100644 packages/core/src/api/apdu/utils/ApduParser.test.ts create mode 100644 packages/core/src/api/apdu/utils/ApduParser.ts diff --git a/packages/core/src/api/apdu/utils/ApduParser.test.ts b/packages/core/src/api/apdu/utils/ApduParser.test.ts new file mode 100644 index 000000000..e8ad086f4 --- /dev/null +++ b/packages/core/src/api/apdu/utils/ApduParser.test.ts @@ -0,0 +1,443 @@ +import { ApduResponse } from "@internal/device-session/model/ApduResponse"; + +import { ApduParser } from "./ApduParser"; + +const STATUS_WORD_SUCCESS = new Uint8Array([0x90, 0x00]); +const RESPONSE_ONE_BYTE = new Uint8Array([0x01]); +const RESPONSE_LV_ZERO = new Uint8Array([0x00]); +const RESPONSE_TWO_BYTES = new Uint8Array([0x01, 0x01]); +const RESPONSE_TLV_ZERO = new Uint8Array([0xab, 0x00]); +const RESPONSE_ALL_BYTES = new Uint8Array([ + 0x01, + 0x02, + 0x03, + ...Array(253).fill(0xaa), +]); + +/* +Type : 33 00 00 04 -> nanoX +Version SE (LV): 2.2.3 +Flag: E600000000 + PIN OK + Factory init Ok + Onboarding done +Version MCU(LV): 2.30 +Version BootLoader(LV): 1.16 +HW rev: 0 +Language(LV): Fra & Eng +Recover state (LV): 1 +*/ +const DEVICE_TYPE = "33000004"; +const DEVICE_FLAGS = "0xe6000000"; +const NUMERIC_FLAGS = 0xe6000000; +const VERSION_FW_SE = "2.2.3"; +const VERSION_FW_MCU = "2.30"; +const VERSION_FW_BL = "1.16"; +const HARDWARE_REV = 0; +const LANGUAGE_PACK = 1; +const RECOVER_STATE = 0; +const RESPONSE_GET_VERSION = new Uint8Array([ + 0x33, 0x00, 0x00, 0x04, 0x05, 0x32, 0x2e, 0x32, 0x2e, 0x33, 0x04, 0xe6, 0x00, + 0x00, 0x00, 0x04, 0x32, 0x2e, 0x33, 0x30, 0x04, 0x31, 0x2e, 0x31, 0x36, 0x01, + 0x00, 0x01, 0x01, 0x01, 0x00, +]); + +/* +Format version: 1 +Name: BOLOS +Version: 2.2.3 +*/ +const DASHBOARD_HEX = new Uint8Array([0x42, 0x4f, 0x4c, 0x4f, 0x53]); +const DASHBOARD_NAME = "BOLOS"; +const RESPONSE_GET_APP_VERSION = new Uint8Array([ + 0x01, 0x05, 0x42, 0x4f, 0x4c, 0x4f, 0x53, 0x05, 0x32, 0x2e, 0x32, 0x2e, 0x33, +]); + +let parser: ApduParser; +let response: ApduResponse = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_ONE_BYTE, +}); + +describe("ApduParser", () => { + describe("clean", () => { + it("should create an instance", () => { + parser = new ApduParser(response); + expect(parser).toBeDefined(); + expect(parser).toBeInstanceOf(ApduParser); + }); + + it("Extract a single byte", () => { + parser = new ApduParser(response); + expect(parser.extract8BitUint()).toBe(0x01); + expect(parser.getCurrentIndex()).toBe(1); + expect(parser.getUnparsedRemainingLength()).toBe(0); + }); + + it("Extract one byte", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_ALL_BYTES, + }); + parser = new ApduParser(response); + let index = 0; + let length = RESPONSE_ALL_BYTES.length; + + expect(length).toBe(256); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + index++; + length--; + + expect(parser.extract8BitUint()).toBe(0x01); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + index++; + length--; + + expect(parser.extract8BitUint()).toBe(0x02); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + index++; + length--; + + expect(parser.extract8BitUint()).toBe(0x03); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + index++; + length--; + + while (length != 0) { + expect(parser.extract8BitUint()).toBe(0xaa); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + index++; + length--; + } + }); + + it("Extract 16-bit & 32-bit number", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_ALL_BYTES, + }); + parser = new ApduParser(response); + let index = 0; + let length = RESPONSE_ALL_BYTES.length; + + expect(length).toBe(256); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract16BitUInt()).toBe(0x0102); + index += 2; + length -= 2; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract16BitUInt()).toBe(0x03aa); + index += 2; + length -= 2; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + parser.resetIndex(); + index = 0; + length = RESPONSE_ALL_BYTES.length; + + expect(parser.extract32BitUInt()).toBe(0x010203aa); + index += 4; + length -= 4; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract32BitUInt()).toBe(0xaaaaaaaa); + index += 4; + length -= 4; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + }); + + it("Parse a GetAppVersion response", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_GET_APP_VERSION, + }); + parser = new ApduParser(response); + let index = 0; + let length = RESPONSE_GET_APP_VERSION.length; + + // Parse the response considering the first field to be the format field + expect(length).toBe(13); + + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + const value = parser.extract8BitUint(); + index++; + length--; + expect(value).toBe(1); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + let array = parser.extractFieldLVEncoded(); + expect(array).toStrictEqual(DASHBOARD_HEX); + expect(parser.encodeToString(array)).toBe(DASHBOARD_NAME); + index += 6; + length -= 6; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.encodeToString(array)).toBe(VERSION_FW_SE); + index += 6; + length -= 6; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + // Reparse the response considering the first field to be the TLV formatted + parser.resetIndex(); + index = 0; + length = RESPONSE_GET_APP_VERSION.length; + + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + const field = parser.extractFieldTLVEncoded(); + expect(field?.tag).toBe(0x01); + expect(field?.value).toStrictEqual(DASHBOARD_HEX); + expect(parser.encodeToString(field?.value)).toBe(DASHBOARD_NAME); + index += 7; + length -= 7; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.encodeToString(array)).toBe(VERSION_FW_SE); + index += 6; + length -= 6; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + }); + + it("Parse a GetVersion response", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_GET_VERSION, + }); + parser = new ApduParser(response); + let index = 0; + let length = RESPONSE_GET_VERSION.length; + + expect(length).toBe(31); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + expect(parser.testMinimalLength(25)).toBe(true); + + let array = parser.extractFieldDirect(4); + expect(parser.encodeToHexaString(array)).toBe(DEVICE_TYPE); + index += 4; + length -= 4; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.encodeToString(array)).toBe(VERSION_FW_SE); + index += 6; + length -= 6; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + const flags = parser.encodeToHexaString(array, true); + expect(flags).toBe(DEVICE_FLAGS); + expect(parseInt(flags, 16)).toBe(NUMERIC_FLAGS); + index += 5; + length -= 5; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.encodeToString(array)).toBe(VERSION_FW_MCU); + index += 5; + length -= 5; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.encodeToString(array)).toBe(VERSION_FW_BL); + index += 5; + length -= 5; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(array?.at(0)).toBe(HARDWARE_REV); + index += 2; + length -= 2; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(array?.at(0)).toBe(LANGUAGE_PACK); + index += 2; + length -= 2; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(array?.at(0)).toBe(RECOVER_STATE); + index += 2; + length -= 2; + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + }); + }); + + describe("errors", () => { + it("no response", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: new Uint8Array(), + }); + parser = new ApduParser(response); + const index = 0; + const length = 0; + + expect(parser.testMinimalLength(1)).toBe(false); + + expect(parser.extract8BitUint()).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract16BitUInt()).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract32BitUInt()).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + let array = parser.extractFieldDirect(2); + expect(array).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(array).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + const field = parser.extractFieldTLVEncoded(); + expect(field).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + }); + + it("length error", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_ONE_BYTE, + }); + parser = new ApduParser(response); + const index = 0; + const length = RESPONSE_ONE_BYTE.length; + + expect(length).toBe(1); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract16BitUInt()).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + expect(parser.extract32BitUInt()).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + let array = parser.extractFieldDirect(2); + expect(array).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(array).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + let field = parser.extractFieldTLVEncoded(); + expect(field).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_TWO_BYTES, + }); + parser = new ApduParser(response); + + field = parser.extractFieldTLVEncoded(); + expect(field).toBe(undefined); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe( + RESPONSE_TWO_BYTES.length, + ); + }); + + it("Test zero length", () => { + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_LV_ZERO, + }); + parser = new ApduParser(response); + const zero = new Uint8Array(); + + const index = 0; + let length = RESPONSE_LV_ZERO.length; + + expect(length).toBe(1); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + const value = parser.extract8BitUint(); + expect(value).toBe(0); + expect(parser.getCurrentIndex()).toBe(1); + expect(parser.getUnparsedRemainingLength()).toBe(0); + + parser.resetIndex(); + + let array = parser.extractFieldDirect(0); + expect(array).toStrictEqual(zero); + expect(parser.encodeToString(array)).toBe(""); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + array = parser.extractFieldLVEncoded(); + expect(parser.getCurrentIndex()).toBe(1); + expect(parser.getUnparsedRemainingLength()).toBe(0); + expect(array).toStrictEqual(zero); + expect(parser.encodeToString(array)).toBe(""); + + response = new ApduResponse({ + statusCode: STATUS_WORD_SUCCESS, + data: RESPONSE_TLV_ZERO, + }); + parser = new ApduParser(response); + length = RESPONSE_TLV_ZERO.length; + + expect(length).toBe(2); + expect(parser.getCurrentIndex()).toBe(index); + expect(parser.getUnparsedRemainingLength()).toBe(length); + + const field = parser.extractFieldTLVEncoded(); + expect(field?.tag).toBe(0xab); + expect(field?.value).toStrictEqual(zero); + expect(parser.encodeToString(field?.value)).toBe(""); + expect(parser.getCurrentIndex()).toBe(2); + expect(parser.getUnparsedRemainingLength()).toBe(0); + + expect(parser.encodeToHexaString()).toBe(""); + expect(parser.encodeToString()).toBe(""); + }); + }); +}); diff --git a/packages/core/src/api/apdu/utils/ApduParser.ts b/packages/core/src/api/apdu/utils/ApduParser.ts new file mode 100644 index 000000000..745bd1020 --- /dev/null +++ b/packages/core/src/api/apdu/utils/ApduParser.ts @@ -0,0 +1,131 @@ +import { ApduResponse } from "@internal/device-session/model/ApduResponse"; + +export type TaggedField = { + tag: number; + value: Uint8Array; +}; + +export class ApduParser { + private _index: number; + private _response: Uint8Array; + + constructor(response: ApduResponse) { + this._index = 0; + this._response = response.data; + } + + // Public API + testMinimalLength = (length: number) => { + if (length > this._response.length) return false; + return true; + }; + + extract8BitUint = () => { + if (!this.testLength(1)) return undefined; + return this._response[this._index++]; + }; + + extract16BitUInt = () => { + if (!this.testLength(2)) return undefined; + let msb = this.extract8BitUint(); + if (!msb) return undefined; + const lsb = this.extract8BitUint(); + if (!lsb) return undefined; + msb *= 0x100; + return msb + lsb; + }; + + extract32BitUInt = () => { + if (!this.testLength(4)) return undefined; + let msw = this.extract16BitUInt(); + if (!msw) return undefined; + const lsw = this.extract16BitUInt(); + if (!lsw) return undefined; + msw *= 0x10000; + return msw + lsw; + }; + + extractFieldDirect = (length: number) => { + if (!this.testLength(length)) return undefined; + if (length == 0) return new Uint8Array(); + const field = this._response.slice(this._index, this._index + length); + this._index += length; + return field; + }; + + extractFieldLVEncoded = () => { + // extract Length field + const length = this.extract8BitUint(); + if (length == 0) return new Uint8Array(); + if (!length) return undefined; + const field = this.extractFieldDirect(length); + + // if the field is inconsistent then roll back to the initial point + if (!field) this._index--; + return field; + }; + + extractFieldTLVEncoded = () => { + if (!this.testLength(2)) return undefined; + + // extract the tag field + const tag = this.extract8BitUint(); + const value = this.extractFieldLVEncoded(); + + // if the field is inconsistent then roll back to the initial point + if (!value) { + this._index--; + return undefined; + } + return { tag, value }; + }; + + encodeToHexaString = (value?: Uint8Array, preamble?: boolean) => { + let result = ""; + let index = 0; + + if (!value) return result; + + if (preamble) result += "0x"; + + while (index <= value.length) { + const item = value.at(index)?.toString(16); + if (item) result += item.length < 2 ? "0" + item : item; + index++; + } + return result; + }; + + encodeToString = (value?: Uint8Array) => { + let result = ""; + let index = 0; + + if (!value) return result; + + while (index <= value.length) { + const item = value.at(index); + if (item) result += String.fromCharCode(item); + index++; + } + + return result; + }; + + getCurrentIndex = () => { + return this._index; + }; + + resetIndex = () => { + this._index = 0; + }; + + getUnparsedRemainingLength = () => { + return this._response.length - this._index; + }; + + // Private API + private testLength = (length: number) => { + if (this._index + length > this._response.length) return false; + return true; + }; +} From 37a2573901d912a5c7b67b21d92dbbd5bc109cbe Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Tue, 16 Apr 2024 15:59:46 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20(core):=20ApduParser=20+=20A?= =?UTF-8?q?pduBuilder=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/apdu/utils/ApduBuilder.test.ts | 18 +-- .../core/src/api/apdu/utils/ApduBuilder.ts | 114 +++++++++++++++--- .../src/api/apdu/utils/ApduParser.test.ts | 32 ++--- .../core/src/api/apdu/utils/ApduParser.ts | 108 +++++++++++++---- 4 files changed, 206 insertions(+), 66 deletions(-) diff --git a/packages/core/src/api/apdu/utils/ApduBuilder.test.ts b/packages/core/src/api/apdu/utils/ApduBuilder.test.ts index 9e874a35a..3345231c7 100644 --- a/packages/core/src/api/apdu/utils/ApduBuilder.test.ts +++ b/packages/core/src/api/apdu/utils/ApduBuilder.test.ts @@ -84,13 +84,13 @@ describe("ApduBuilder", () => { }); it("should serialize with an single byte body", () => { - builder.addByteToData(0x01); + builder.add8BitUintToData(0x01); expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_SINGLE); expect(builder.getErrors()).toEqual([]); }); it("should serialize with an 2 byte body", () => { - builder.addShortToData(0x3302); + builder.add16BitUintToData(0x3302); expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_TWO); expect(builder.getErrors()).toEqual([]); }); @@ -148,11 +148,11 @@ describe("ApduBuilder", () => { it("should serialize with all previous field", () => { let available = APDU_MAX_PAYLOAD; - builder.addByteToData(0x01); + builder.add8BitUintToData(0x01); available--; expect(builder.getAvailablePayloadLength()).toBe(available); - builder.addShortToData(0x3302); + builder.add16BitUintToData(0x3302); available -= 2; expect(builder.getAvailablePayloadLength()).toBe(available); @@ -187,14 +187,14 @@ describe("ApduBuilder", () => { }); it("error to undefined value", () => { - builder.addByteToData(undefined); + builder.add8BitUintToData(undefined); expect(builder.build().getRawApdu()).toEqual(COMMAND_NO_BODY); expect(builder.getAvailablePayloadLength()).toBe(APDU_MAX_PAYLOAD); expect(builder.getErrors()).toEqual([new InvalidValueError("byte")]); }); it("error due value greater than 8-bit integer", () => { - builder.addByteToData(0x100); + builder.add8BitUintToData(0x100); expect(builder.build().getRawApdu()).toEqual(COMMAND_NO_BODY); expect(builder.getAvailablePayloadLength()).toBe(APDU_MAX_PAYLOAD); expect(builder.getErrors()).toEqual([ @@ -203,7 +203,7 @@ describe("ApduBuilder", () => { }); it("error due value greater than 16-bit integer", () => { - builder.addShortToData(0x10000); + builder.add16BitUintToData(0x10000); expect(builder.build().getRawApdu()).toEqual(COMMAND_NO_BODY); expect(builder.getAvailablePayloadLength()).toBe(APDU_MAX_PAYLOAD); expect(builder.getErrors()).toEqual([ @@ -248,7 +248,7 @@ describe("ApduBuilder", () => { expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_MAX); expect(builder.getAvailablePayloadLength()).toBe(0); - builder.addByteToData(0); + builder.add8BitUintToData(0); expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_MAX); expect(builder.getErrors()).toEqual([new DataOverflowError("0")]); }); @@ -263,7 +263,7 @@ describe("ApduBuilder", () => { expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_MAX); expect(builder.getAvailablePayloadLength()).toBe(0); - builder.addShortToData(0); + builder.add16BitUintToData(0); expect(builder.build().getRawApdu()).toEqual(COMMAND_BODY_MAX); expect(builder.getAvailablePayloadLength()).toBe(0); expect(builder.getErrors()).toEqual([new DataOverflowError("0")]); diff --git a/packages/core/src/api/apdu/utils/ApduBuilder.ts b/packages/core/src/api/apdu/utils/ApduBuilder.ts index 044a0f0dc..7c2a893bf 100644 --- a/packages/core/src/api/apdu/utils/ApduBuilder.ts +++ b/packages/core/src/api/apdu/utils/ApduBuilder.ts @@ -12,7 +12,7 @@ export const HEADER_LENGTH = 5; export const APDU_MAX_PAYLOAD = 255; export const APDU_MAX_SIZE = APDU_MAX_PAYLOAD + 5; -type ApduBuilderArgs = { +export type ApduBuilderArgs = { ins: number; cla: number; p1: number; @@ -20,6 +20,11 @@ type ApduBuilderArgs = { offset?: number; }; +/** + * ApduBuilder is a utility class to help build APDU commands + * It allows to easily add data to the data field of the APDU command + * and to encode data in different formats + */ export class ApduBuilder { private _ins: number; private _cla: number; @@ -35,11 +40,22 @@ export class ApduBuilder { this.p2 = p2 & 0xff; } + // ========== // Public API + // ========== + /** + * Build a new Apdu instance with the current state of the builder + * @returns {Apdu} - Returns a new Apdu instance + */ build = () => new Apdu(this._cla, this._ins, this._p1, this.p2, this.data); - addByteToData = (value?: number) => { + /** + * Add a 8-bit unsigned integer to the data field (max value 0xff = 255) + * @param value?: number - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ + add8BitUintToData = (value?: number) => { if (typeof value === "undefined" || isNaN(value)) { this.errors?.push(new InvalidValueError("byte", value?.toString())); return this; @@ -61,7 +77,12 @@ export class ApduBuilder { return this; }; - addShortToData = (value: number) => { + /** + * Add a 16-bit unsigned integer to the data field (max value 0xffff = 65535) + * @param value: number - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ + add16BitUintToData = (value: number) => { if (value > 0xffff) { this.errors?.push(new ValueOverflowError(value.toString(), 65535)); return this; @@ -72,11 +93,16 @@ export class ApduBuilder { return this; } - this.addByteToData((value >>> 8) & 0xff); - this.addByteToData(value & 0xff); + this.add8BitUintToData((value >>> 8) & 0xff); + this.add8BitUintToData(value & 0xff); return this; }; + /** + * Add a Uint8Array to the data field if it has enough remaining space + * @param value: Uint8Array - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ addBufferToData = (value: Uint8Array) => { if (!this.hasEnoughLengthRemaining(value)) { this.errors?.push(new DataOverflowError(value.toString())); @@ -84,11 +110,17 @@ export class ApduBuilder { } for (const byte of value) { - this.addByteToData(byte); + this.add8BitUintToData(byte); } return this; }; + /** + * Add a string to the data field if it has enough remaining space + * and it can be formatted as a hexadecimal string + * @param value: string - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ addHexaStringToData = (value: string) => { const result = this.getHexaString(value); if (!result.length) { @@ -99,6 +131,11 @@ export class ApduBuilder { return this; }; + /** + * Add an ascii string to the data field if it has enough remaining space + * @param value: string - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ addAsciiStringToData = (value: string) => { let hexa = 0; @@ -109,12 +146,19 @@ export class ApduBuilder { for (const char of value) { hexa = char.charCodeAt(0); - this.addByteToData(hexa); + this.add8BitUintToData(hexa); } return this; }; + /** + * Add a Length-Value encoded hexadecimal string to the data field if it has enough remaining space + * Length-Value encoding is a way to encode data in a binary format with the first byte + * being the length of the data and the following bytes being the data itself + * @param value: string - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ encodeInLVFromHexa = (value: string) => { const result: number[] = this.getHexaString(value); @@ -129,39 +173,60 @@ export class ApduBuilder { } // values are always being well formatted at this point // therefore no status test is needed - this.addByteToData(result.length); + this.add8BitUintToData(result.length); this.addNumbers(result); return this; }; + /** + * Add a Length-Value encoded buffer to the data field if it has enough remaining space + * Length-Value encoding is a way to encode data in a binary format with the first byte + * being the length of the data and the following bytes being the data itself + * @param value: Uint8Array - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ encodeInLVFromBuffer = (value: Uint8Array) => { if (!this.hasEnoughLengthRemaining(value, true)) { this.errors?.push(new DataOverflowError(value.toString())); return this; } - // values are always being well formatted at this point - // therefore no status test is needed - this.addByteToData(value.length); + + this.add8BitUintToData(value.length); this.addBufferToData(value); return this; }; + /** + * Add a Length-Value encoded ascii string to the data field if it has enough remaining space + * Length-Value encoding is a way to encode data in a binary format with the first byte + * being the length of the data and the following bytes being the data itself + * @param value: string - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ encodeInLVFromAscii = (value: string) => { if (!this.hasEnoughLengthRemaining(value, true)) { this.errors?.push(new DataOverflowError(value)); return this; } - // values are always being well formatted at this point - // therefore no status test is needed - this.addByteToData(value.length); + + this.add8BitUintToData(value.length); this.addAsciiStringToData(value); return this; }; + /** + * Returns the remaining payload length + * @returns {number} + */ getAvailablePayloadLength = () => { return APDU_MAX_SIZE - (HEADER_LENGTH + (this.data?.length ?? 0)); }; + /** + * Returns the hexadecimal representation of a string + * @param value: string - The value to convert to hexadecimal + * @returns {number[]} - Returns an array of numbers representing the hexadecimal value + */ getHexaString = (value: string) => { const table: number[] = []; @@ -196,10 +261,22 @@ export class ApduBuilder { return table; }; + /** + * Returns the current errors + * @returns {AppBuilderError[]} - Returns an array of errors + */ getErrors = () => this.errors; + // =========== // Private API - + // =========== + + /** + * Check if there is enough space to add a value to the data field + * @param value {string | Uint8Array | number[]} - Value to add to the data + * @param hasLv {boolean} - Length-Value encoding flag + * @returns {boolean} - Returns true if there is enough space to add the value + */ private hasEnoughLengthRemaining = ( value: string | Uint8Array | number[], hasLv = false, @@ -213,6 +290,11 @@ export class ApduBuilder { ); }; + /** + * Add an array of numbers to the data field if it has enough remaining space + * @param value: number[] - The value to add to the data + * @returns {ApduBuilder} - Returns the current instance of ApduBuilder + */ private addNumbers = (value: number[]) => { if (!this.hasEnoughLengthRemaining(value)) { this.errors?.push(new DataOverflowError(value.toString())); @@ -220,7 +302,7 @@ export class ApduBuilder { } for (const byte of value) { - this.addByteToData(byte); + this.add8BitUintToData(byte); } return this; diff --git a/packages/core/src/api/apdu/utils/ApduParser.test.ts b/packages/core/src/api/apdu/utils/ApduParser.test.ts index e8ad086f4..54033d244 100644 --- a/packages/core/src/api/apdu/utils/ApduParser.test.ts +++ b/packages/core/src/api/apdu/utils/ApduParser.test.ts @@ -234,7 +234,7 @@ describe("ApduParser", () => { expect(parser.getUnparsedRemainingLength()).toBe(length); expect(parser.testMinimalLength(25)).toBe(true); - let array = parser.extractFieldDirect(4); + let array = parser.extractFieldByLength(4); expect(parser.encodeToHexaString(array)).toBe(DEVICE_TYPE); index += 4; length -= 4; @@ -306,30 +306,30 @@ describe("ApduParser", () => { expect(parser.testMinimalLength(1)).toBe(false); - expect(parser.extract8BitUint()).toBe(undefined); + expect(parser.extract8BitUint()).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - expect(parser.extract16BitUInt()).toBe(undefined); + expect(parser.extract16BitUInt()).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - expect(parser.extract32BitUInt()).toBe(undefined); + expect(parser.extract32BitUInt()).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - let array = parser.extractFieldDirect(2); - expect(array).toBe(undefined); + let array = parser.extractFieldByLength(2); + expect(array).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); array = parser.extractFieldLVEncoded(); - expect(array).toBe(undefined); + expect(array).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); const field = parser.extractFieldTLVEncoded(); - expect(field).toBe(undefined); + expect(field).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); }); @@ -347,26 +347,26 @@ describe("ApduParser", () => { expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - expect(parser.extract16BitUInt()).toBe(undefined); + expect(parser.extract16BitUInt()).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - expect(parser.extract32BitUInt()).toBe(undefined); + expect(parser.extract32BitUInt()).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); - let array = parser.extractFieldDirect(2); - expect(array).toBe(undefined); + let array = parser.extractFieldByLength(2); + expect(array).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); array = parser.extractFieldLVEncoded(); - expect(array).toBe(undefined); + expect(array).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); let field = parser.extractFieldTLVEncoded(); - expect(field).toBe(undefined); + expect(field).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe(length); @@ -377,7 +377,7 @@ describe("ApduParser", () => { parser = new ApduParser(response); field = parser.extractFieldTLVEncoded(); - expect(field).toBe(undefined); + expect(field).toBeUndefined(); expect(parser.getCurrentIndex()).toBe(index); expect(parser.getUnparsedRemainingLength()).toBe( RESPONSE_TWO_BYTES.length, @@ -406,7 +406,7 @@ describe("ApduParser", () => { parser.resetIndex(); - let array = parser.extractFieldDirect(0); + let array = parser.extractFieldByLength(0); expect(array).toStrictEqual(zero); expect(parser.encodeToString(array)).toBe(""); expect(parser.getCurrentIndex()).toBe(index); diff --git a/packages/core/src/api/apdu/utils/ApduParser.ts b/packages/core/src/api/apdu/utils/ApduParser.ts index 745bd1020..525fd4502 100644 --- a/packages/core/src/api/apdu/utils/ApduParser.ts +++ b/packages/core/src/api/apdu/utils/ApduParser.ts @@ -14,59 +14,89 @@ export class ApduParser { this._response = response.data; } + // ========== // Public API - testMinimalLength = (length: number) => { - if (length > this._response.length) return false; - return true; - }; - + // ========== + + /** + * Test if the length is greater than the response length + * @param length: number + * @returns {boolean} - Returns false if the length is greater than the response length + */ + testMinimalLength = (length: number) => !(length > this._response.length); + + /** + * Extract a single byte from the response + * @returns {number | undefined} - Returns the byte extracted from the response + */ extract8BitUint = () => { - if (!this.testLength(1)) return undefined; + if (!this.testLength(1)) return; return this._response[this._index++]; }; + /** + * Extract a 16-bit unsigned integer (Big Endian coding) from the response + * @returns {number | undefined} - Returns the 16-bit unsigned integer extracted from the response + */ extract16BitUInt = () => { - if (!this.testLength(2)) return undefined; + if (!this.testLength(2)) return; let msb = this.extract8BitUint(); - if (!msb) return undefined; + if (!msb) return; const lsb = this.extract8BitUint(); - if (!lsb) return undefined; + if (!lsb) return; msb *= 0x100; return msb + lsb; }; + /** + * Extract a 32-bit unsigned integer (Big Endian coding) from the response + * @returns {number | undefined} - Returns the 32-bit unsigned integer extracted from the response + */ extract32BitUInt = () => { - if (!this.testLength(4)) return undefined; + if (!this.testLength(4)) return; let msw = this.extract16BitUInt(); - if (!msw) return undefined; + if (!msw) return; const lsw = this.extract16BitUInt(); - if (!lsw) return undefined; + if (!lsw) return; msw *= 0x10000; return msw + lsw; }; - extractFieldDirect = (length: number) => { - if (!this.testLength(length)) return undefined; + /** + * Extract a field of a specified length from the response + * @param length: number - The length of the field to extract + * @returns {Uint8Array | undefined} - Returns the field extracted from the response + */ + extractFieldByLength = (length: number) => { + if (!this.testLength(length)) return; if (length == 0) return new Uint8Array(); const field = this._response.slice(this._index, this._index + length); this._index += length; return field; }; + /** + * Extract a field from the response that is length-value encoded + * @returns {Uint8Array | undefined} - Returns the field extracted from the response + */ extractFieldLVEncoded = () => { // extract Length field const length = this.extract8BitUint(); if (length == 0) return new Uint8Array(); - if (!length) return undefined; - const field = this.extractFieldDirect(length); + if (!length) return; + const field = this.extractFieldByLength(length); // if the field is inconsistent then roll back to the initial point if (!field) this._index--; return field; }; + /** + * Extract a field from the response that is tag-length-value encoded + * @returns {TaggedField | undefined} - Returns the field extracted from the response + */ extractFieldTLVEncoded = () => { - if (!this.testLength(2)) return undefined; + if (!this.testLength(2)) return; // extract the tag field const tag = this.extract8BitUint(); @@ -75,18 +105,24 @@ export class ApduParser { // if the field is inconsistent then roll back to the initial point if (!value) { this._index--; - return undefined; + return; } - return { tag, value }; + return { tag, value } as TaggedField; }; - encodeToHexaString = (value?: Uint8Array, preamble?: boolean) => { + /** + * Encode a value to a hexadecimal string + * @param value {Uint8Array} - The value to encode + * @param prefix {boolean} - Whether to add a prefix to the encoded value + * @returns {string} - The encoded value as a hexadecimal string + */ + encodeToHexaString = (value?: Uint8Array, prefix?: boolean) => { let result = ""; let index = 0; if (!value) return result; - if (preamble) result += "0x"; + if (prefix) result += "0x"; while (index <= value.length) { const item = value.at(index)?.toString(16); @@ -96,6 +132,11 @@ export class ApduParser { return result; }; + /** + * Encode a value to an ASCII string + * @param value {Uint8Array} - The value to encode + * @returns {string} - The encoded value as an ASCII string + */ encodeToString = (value?: Uint8Array) => { let result = ""; let index = 0; @@ -111,21 +152,38 @@ export class ApduParser { return result; }; + /** + * Get the current index of the parser + * @returns {number} - The current index of the parser + */ getCurrentIndex = () => { return this._index; }; + /** + * Reset the index of the parser to 0 + */ resetIndex = () => { this._index = 0; }; + /** + * Get the remaining length of the response + * @returns {number} - The remaining length of the response + */ getUnparsedRemainingLength = () => { return this._response.length - this._index; }; + // =========== // Private API - private testLength = (length: number) => { - if (this._index + length > this._response.length) return false; - return true; - }; + // =========== + + /** + * Test if the length is greater than the response length + * @param length: number + * @returns {boolean} - Returns false if the length is greater than the response length + */ + private testLength = (length: number) => + !(this._index + length > this._response.length); }