diff --git a/packages/core/src/internal/device-session/data/ApduResponseConst.ts b/packages/core/src/internal/device-session/data/ApduResponseConst.ts new file mode 100644 index 000000000..0b8428a0d --- /dev/null +++ b/packages/core/src/internal/device-session/data/ApduResponseConst.ts @@ -0,0 +1 @@ +export const APDU_RESPONSE_STATUS_CODE_LENGTH = 2; diff --git a/packages/core/src/internal/device-session/data/FramerConst.ts b/packages/core/src/internal/device-session/data/FramerConst.ts index c95f66951..17588db3f 100644 --- a/packages/core/src/internal/device-session/data/FramerConst.ts +++ b/packages/core/src/internal/device-session/data/FramerConst.ts @@ -1,5 +1,5 @@ export const HEAD_TAG = 0x05; -export const HEAD_TAG_SIZE = 1; -export const CHANNEL_SIZE = 2; -export const INDEX_SIZE = 2; -export const APDU_DATA_SIZE = 2; +export const HEAD_TAG_LENGTH = 1; +export const CHANNEL_LENGTH = 2; +export const INDEX_LENGTH = 2; +export const APDU_DATA_LENGTH_LENGTH = 2; diff --git a/packages/core/src/internal/device-session/model/ApduResponse.ts b/packages/core/src/internal/device-session/model/ApduResponse.ts new file mode 100644 index 000000000..63d617ff0 --- /dev/null +++ b/packages/core/src/internal/device-session/model/ApduResponse.ts @@ -0,0 +1,22 @@ +type ApduResponseConstructorArgs = { + statusCode: Uint8Array; + data: Uint8Array; +}; + +export class ApduResponse { + protected _statusCode: Uint8Array; + protected _data: Uint8Array; + + constructor({ statusCode, data }: ApduResponseConstructorArgs) { + this._statusCode = statusCode; + this._data = data; + } + + public getStatusCode() { + return this._statusCode; + } + + public getData() { + return this._data; + } +} diff --git a/packages/core/src/internal/device-session/model/Errors.ts b/packages/core/src/internal/device-session/model/Errors.ts index 33b4b6b05..835ac143c 100644 --- a/packages/core/src/internal/device-session/model/Errors.ts +++ b/packages/core/src/internal/device-session/model/Errors.ts @@ -19,6 +19,15 @@ export class FramerApduError implements SdkError { } } +export class ReceiverApduError implements SdkError { + readonly _tag = "ReceiverApduError"; + originalError: Error; + + constructor(message?: string) { + this.originalError = new Error(message ?? "Unable to parse apdu"); + } +} + export class DeviceSessionNotFound implements SdkError { readonly _tag = "DeviceSessionNotFound"; originalError?: Error; diff --git a/packages/core/src/internal/device-session/model/Frame.ts b/packages/core/src/internal/device-session/model/Frame.ts index e327168b4..353de03b1 100644 --- a/packages/core/src/internal/device-session/model/Frame.ts +++ b/packages/core/src/internal/device-session/model/Frame.ts @@ -8,10 +8,12 @@ type FrameConstructorArgs = { export class Frame { protected _header: FrameHeader; protected _data: Uint8Array; + constructor({ header, data }: FrameConstructorArgs) { this._header = header; this._data = data; } + toString(): string { return JSON.stringify( { @@ -22,6 +24,7 @@ export class Frame { 2, ); } + getRawData(): Uint8Array { const headerRaw = this._header.getRawData(); const raw = new Uint8Array(headerRaw.length + this._data.length); @@ -30,4 +33,12 @@ export class Frame { raw.set(this._data, headerRaw.length); return raw; } + + getHeader(): FrameHeader { + return this._header; + } + + getData(): Uint8Array { + return this._data; + } } diff --git a/packages/core/src/internal/device-session/model/FrameHeader.ts b/packages/core/src/internal/device-session/model/FrameHeader.ts index a386a9923..8cd260a79 100644 --- a/packages/core/src/internal/device-session/model/FrameHeader.ts +++ b/packages/core/src/internal/device-session/model/FrameHeader.ts @@ -1,5 +1,7 @@ import { Maybe } from "purify-ts"; +import { FramerUtils } from "@internal/device-session/utils/FramerUtils"; + type FrameHeaderConstructorArgs = { uuid: string; channel: Maybe; @@ -15,7 +17,7 @@ export class FrameHeader { protected _headTag: Uint8Array; protected _index: Uint8Array; protected _length: number; - protected _dataSize: Maybe; + protected _dataLength: Maybe; constructor({ uuid, dataSize, @@ -25,14 +27,17 @@ export class FrameHeader { channel, }: FrameHeaderConstructorArgs) { this._uuid = uuid; - this._dataSize = dataSize; + this._dataLength = dataSize; this._index = index; this._headTag = headTag; this._length = length; this._channel = channel; } + getDataLength(): Maybe { + return this._dataLength.map((value) => FramerUtils.bytesToNumber(value)); + } setDataSize(dataSize: Maybe): FrameHeader { - this._dataSize = dataSize; + this._dataLength = dataSize; return this; } getLength(): number { @@ -41,7 +46,7 @@ export class FrameHeader { toString(): string { return JSON.stringify({ uuid: this._uuid.toString(), - dataSize: this._dataSize.extract()?.toString(), + dataSize: this._dataLength.extract()?.toString(), index: this._index.toString(), headTag: this._headTag.toString(), length: this._length.toString(), @@ -56,7 +61,7 @@ export class FrameHeader { }), ...this._headTag, ...this._index, - ...this._dataSize.caseOf({ + ...this._dataLength.caseOf({ Just: (dataSize) => [...dataSize], Nothing: () => [], }), diff --git a/packages/core/src/internal/device-session/service/ApduReceiverService.ts b/packages/core/src/internal/device-session/service/ApduReceiverService.ts new file mode 100644 index 000000000..42ba0d86e --- /dev/null +++ b/packages/core/src/internal/device-session/service/ApduReceiverService.ts @@ -0,0 +1,8 @@ +import { Either, Maybe } from "purify-ts"; + +import { ApduResponse } from "@internal/device-session/model/ApduResponse"; +import { ReceiverApduError } from "@internal/device-session/model/Errors"; + +export interface ApduReceiverService { + handleFrame(apdu: Uint8Array): Either>; +} diff --git a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts b/packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts new file mode 100644 index 000000000..d8a603273 --- /dev/null +++ b/packages/core/src/internal/device-session/service/DefaultApduReceiverService.test.ts @@ -0,0 +1,331 @@ +import * as uuid from "uuid"; +jest.mock("uuid"); + +import { Just, Left, Maybe, Nothing, Right } from "purify-ts"; + +import { ApduResponse } from "@internal/device-session/model/ApduResponse"; +import { ReceiverApduError } from "@internal/device-session/model/Errors"; +import { DefaultLoggerPublisherService } from "@internal/logger-publisher/service/DefaultLoggerPublisherService"; + +import { ApduReceiverService } from "./ApduReceiverService"; +import { DefaultApduReceiverService } from "./DefaultApduReceiverService"; + +const loggerService = new DefaultLoggerPublisherService([], "frame"); + +const RESPONSE_GET_VERSION = new Uint8Array([ + 0xaa, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x21, 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, 0x01, 0x01, 0x00, 0x01, 0x00, 0x90, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +const RESPONSE_LOCKED_DEVICE = new Uint8Array([ + 0xaa, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x02, 0x55, 0x15, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); + +const RESPONSE_LIST_APPS = [ + new Uint8Array([ + 0xaa, 0xaa, 0x05, 0x00, 0x00, 0x00, 0x9e, 0x01, 0x4d, 0x00, 0x13, 0xca, + 0x50, 0xfa, 0xa1, 0x91, 0x40, 0x9a, 0x6b, 0xfa, 0x6c, 0x0f, 0xbb, 0xb2, + 0xe7, 0xc4, 0xa9, 0xcf, 0xe5, 0x57, 0x41, 0x00, 0x5d, 0xbd, 0x84, 0xab, + 0x9a, 0xbd, 0x66, 0xc7, 0x6c, 0x90, 0xdd, 0x08, 0x79, 0x0d, 0x08, 0x47, + 0xb9, 0x3a, 0x8f, 0xa7, 0x6f, 0x60, 0x33, 0xae, 0xd3, 0x25, 0xd7, 0xb1, + 0xe5, 0x7c, 0xeb, 0xd7, + ]), + new Uint8Array([ + 0xaa, 0xaa, 0x05, 0x00, 0x01, 0x4b, 0x2e, 0x2c, 0x9f, 0xb4, 0x46, 0x78, + 0xde, 0x05, 0x5f, 0x9e, 0x80, 0x0a, 0x07, 0x42, 0x69, 0x74, 0x63, 0x6f, + 0x69, 0x6e, 0x4e, 0x00, 0x15, 0xca, 0x40, 0x06, 0x03, 0x28, 0xf8, 0x8f, + 0xc6, 0xd6, 0x42, 0x98, 0xd0, 0x49, 0x00, 0xc7, 0x04, 0x98, 0x19, 0x1b, + 0x6c, 0xeb, 0xed, 0xd8, 0xcb, 0x84, 0x5d, 0xf5, 0x4b, 0xe3, 0xbd, 0xbb, + 0x25, 0x7a, 0x3f, 0x6f, + ]), + new Uint8Array([ + 0xaa, 0xaa, 0x05, 0x00, 0x02, 0x68, 0x8f, 0x54, 0xef, 0x7f, 0xaa, 0xc4, + 0x22, 0xaa, 0x54, 0xe7, 0xb8, 0x0a, 0xc8, 0xa3, 0x2f, 0x96, 0xe5, 0x5e, + 0x43, 0x2d, 0xf3, 0xa3, 0x45, 0x8d, 0x8e, 0xaa, 0xf1, 0x4e, 0xd1, 0x1e, + 0x08, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x90, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ]), +]; + +describe("DefaultApduReceiverService", () => { + let service: ApduReceiverService; + + beforeAll(() => { + jest.spyOn(uuid, "v4").mockReturnValue("42"); + }); + + describe("without dataSize", () => { + beforeEach(() => { + service = new DefaultApduReceiverService( + { channel: Just(new Uint8Array([0xaa, 0xaa])) }, + () => loggerService, + ); + }); + + it("should return a left error when the first frame has no dataSize", () => { + //given + const frame = RESPONSE_LIST_APPS[1]!; + + //when + const apdu = service.handleFrame(frame); + + //then + expect(apdu).toEqual(Left(new ReceiverApduError())); + }); + }); + + describe("[USB] With padding and channel", () => { + beforeEach(() => { + service = new DefaultApduReceiverService( + { channel: Just(new Uint8Array([0xaa, 0xaa])) }, + () => loggerService, + ); + }); + + it("should return a response directly when a frame is complete", () => { + // given + const frame = RESPONSE_GET_VERSION; + + // when + const apdu = service.handleFrame(frame); + + // then + expect(apdu).toEqual( + Right( + Just( + new ApduResponse({ + data: RESPONSE_GET_VERSION.slice(7, 38), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ), + ); + }); + + it("should return a response on a third frame when the two first are not complete", () => { + //given + const apdus = [...RESPONSE_LIST_APPS]; + + //when + let response: Maybe = Nothing; + const responses: Maybe[] = []; + while (response.isNothing()) { + const apdu = apdus.shift()!; + const either = service.handleFrame(apdu); + + either.map((value) => { + response = value; + responses.push(response); + }); + } + + //then + expect(responses).toEqual([ + Nothing, + Nothing, + Just( + new ApduResponse({ + data: new Uint8Array([ + ...Array.from(RESPONSE_LIST_APPS[0]!.slice(7)), + ...Array.from(RESPONSE_LIST_APPS[1]!.slice(5)), + ...Array.from(RESPONSE_LIST_APPS[2]!).slice(5, 45), + ]), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ]); + }); + + it("should return two response directly when each frame is complete", () => { + // given + const firstFrame = RESPONSE_LOCKED_DEVICE; + const secondFrame = RESPONSE_GET_VERSION; + + // when + const firstResponse = service.handleFrame(firstFrame); + const secondResponse = service.handleFrame(secondFrame); + + // then + expect(firstResponse).toEqual( + Right( + Just( + new ApduResponse({ + data: new Uint8Array([]), + statusCode: new Uint8Array([0x55, 0x15]), + }), + ), + ), + ); + expect(secondResponse).toEqual( + Right( + Just( + new ApduResponse({ + data: RESPONSE_GET_VERSION.slice(7, 38), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ), + ); + }); + + it("should return an error if the frame is without index", () => { + //given + //frame with channelId, headTag, and partial Index only + const frame = RESPONSE_LIST_APPS[0]!.slice(0, 4); + + //when + const apdu = service.handleFrame(frame); + + //then + expect(apdu).toEqual( + Left(new ReceiverApduError("Unable to parse header from apdu")), + ); + }); + + it("should return an error if the frame is without datasize", () => { + //given + //frame with channelId, headTag, and Index and partial dataSize only + const frame = RESPONSE_LIST_APPS[0]!.slice(0, 6); + + //when + const apdu = service.handleFrame(frame); + + //then + expect(apdu).toEqual( + Left(new ReceiverApduError("Unable to parse header from apdu")), + ); + }); + }); + + describe("[BLE] Without padding nor channel", () => { + beforeEach(() => { + service = new DefaultApduReceiverService({}, () => loggerService); + }); + + it("should return a response directly when a frame is complete", () => { + // given + const frame = RESPONSE_GET_VERSION.slice(2, 40); + + // when + const apdu = service.handleFrame(frame); + + // then + expect(apdu).toEqual( + Right( + Just( + new ApduResponse({ + data: RESPONSE_GET_VERSION.slice(7, 38), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ), + ); + }); + + it("should return a response on a third frame when the two first are not complete", () => { + //given + const apdus = [ + RESPONSE_LIST_APPS[0]!.slice(2), + RESPONSE_LIST_APPS[1]!.slice(2), + RESPONSE_LIST_APPS[2]!.slice(2, 47), + ]; + + //when + let response: Maybe = Nothing; + const responses: Maybe[] = []; + while (response.isNothing()) { + const apdu = apdus.shift()!; + const either = service.handleFrame(apdu); + + either.map((value) => { + response = value; + responses.push(response); + }); + } + + //then + expect(responses).toEqual([ + Nothing, + Nothing, + Just( + new ApduResponse({ + data: new Uint8Array([ + ...Array.from(RESPONSE_LIST_APPS[0]!.slice(7)), + ...Array.from(RESPONSE_LIST_APPS[1]!.slice(5)), + ...Array.from(RESPONSE_LIST_APPS[2]!).slice(5, 45), + ]), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ]); + }); + + it("should return two response directly when each frame is complete", () => { + // given + const firstFrame = RESPONSE_LOCKED_DEVICE.slice(2, 9); + const secondFrame = RESPONSE_GET_VERSION.slice(2, 40); + + // when + const firstResponse = service.handleFrame(firstFrame); + const secondResponse = service.handleFrame(secondFrame); + + // then + expect(firstResponse).toEqual( + Right( + Just( + new ApduResponse({ + data: new Uint8Array([]), + statusCode: new Uint8Array([0x55, 0x15]), + }), + ), + ), + ); + expect(secondResponse).toEqual( + Right( + Just( + new ApduResponse({ + data: RESPONSE_GET_VERSION.slice(7, 38), + statusCode: new Uint8Array([0x90, 0x00]), + }), + ), + ), + ); + }); + + it("should return an error if the frame is without index", () => { + //given + //frame with channelId, headTag, and partial Index only + const frame = RESPONSE_LIST_APPS[0]!.slice(2, 4); + + //when + const apdu = service.handleFrame(frame); + + //then + expect(apdu).toEqual( + Left(new ReceiverApduError("Unable to parse header from apdu")), + ); + }); + + it("should return an error if the frame is without datasize", () => { + //given + //frame with channelId, headTag, and Index and partial dataSize only + const frame = RESPONSE_LIST_APPS[0]!.slice(2, 6); + + //when + const apdu = service.handleFrame(frame); + + //then + expect(apdu).toEqual( + Left(new ReceiverApduError("Unable to parse header from apdu")), + ); + }); + }); +}); diff --git a/packages/core/src/internal/device-session/service/DefaultApduReceiverService.ts b/packages/core/src/internal/device-session/service/DefaultApduReceiverService.ts new file mode 100644 index 000000000..8cd95ead5 --- /dev/null +++ b/packages/core/src/internal/device-session/service/DefaultApduReceiverService.ts @@ -0,0 +1,179 @@ +import { inject, injectable } from "inversify"; +import { Either, Just, Left, Maybe, Nothing, Right } from "purify-ts"; +import { v4 } from "uuid"; + +import { APDU_RESPONSE_STATUS_CODE_LENGTH } from "@internal/device-session/data/ApduResponseConst"; +import { + APDU_DATA_LENGTH_LENGTH, + CHANNEL_LENGTH, + HEAD_TAG_LENGTH, + INDEX_LENGTH, +} from "@internal/device-session/data/FramerConst"; +import { ApduResponse } from "@internal/device-session/model/ApduResponse"; +import { ReceiverApduError } from "@internal/device-session/model/Errors"; +import { Frame } from "@internal/device-session/model/Frame"; +import { FrameHeader } from "@internal/device-session/model/FrameHeader"; +import { FramerUtils } from "@internal/device-session/utils/FramerUtils"; +import { loggerTypes } from "@internal/logger-publisher/di/loggerTypes"; +import { LoggerPublisherService } from "@internal/logger-publisher/service/LoggerPublisherService"; + +import { ApduReceiverService } from "./ApduReceiverService"; + +type DefaultReceiverConstructorArgs = { + channel?: Maybe; +}; + +@injectable() +export class DefaultApduReceiverService implements ApduReceiverService { + private _channel: Maybe; + private _logger: LoggerPublisherService; + private _pendingFrames: Frame[]; + + constructor( + { channel = Maybe.zero() }: DefaultReceiverConstructorArgs, + @inject(loggerTypes.LoggerPublisherServiceFactory) + loggerModuleFactory: (tag: string) => LoggerPublisherService, + ) { + this._channel = channel; + this._logger = loggerModuleFactory("receiver"); + this._pendingFrames = []; + } + + /* + * Return + * - A complete ApduResponse + * - Or a Nothing if not all the data has been received + * - Or a ReceiverApduError if the apdu is not formatted correctly + * + * @param Uint8Array + */ + public handleFrame( + apdu: Uint8Array, + ): Either> { + const frame = this.apduToFrame(apdu); + + return frame.map((value) => { + this._logger.debug("handle frame", { data: { frame } }); + this._pendingFrames.push(value); + + const dataSize = this._pendingFrames[0]!.getHeader().getDataLength(); + return this.getCompleteFrame(dataSize); + }); + } + + /* + * Return + * - A complete ApduResponse + * - Or a Nothing if not all the data has been received + * + * @param Maybe + */ + private getCompleteFrame(dataSize: Maybe): Maybe { + return dataSize.chain((value) => { + if (!this.isComplete(value)) { + this._logger.debug("frame is not complete, waiting for more"); + return Nothing; + } + + const concatenatedFramesData = FramerUtils.getFirstBytesFrom( + this.concatFrames(this._pendingFrames), + value, + ); + const data = FramerUtils.getFirstBytesFrom( + concatenatedFramesData, + concatenatedFramesData.length - APDU_RESPONSE_STATUS_CODE_LENGTH, + ); + const statusCode = FramerUtils.getLastBytesFrom( + concatenatedFramesData, + APDU_RESPONSE_STATUS_CODE_LENGTH, + ); + + this._pendingFrames = []; + + return Just( + new ApduResponse({ + data: data, + statusCode, + }), + ); + }); + } + + /* + * Parse an Uint8Array to a Frame + * Return an error if the frame is not formatted correctly + * + * @param Uint8Array + */ + private apduToFrame(apdu: Uint8Array): Either { + const channelSize = this._channel.caseOf({ + Just: () => CHANNEL_LENGTH, + Nothing: () => 0, + }); + + const headTag = apdu.slice(channelSize, channelSize + HEAD_TAG_LENGTH); + const index = apdu.slice( + channelSize + HEAD_TAG_LENGTH, + channelSize + HEAD_TAG_LENGTH + INDEX_LENGTH, + ); + + const isFirstIndex = index.reduce((curr, val) => curr + val, 0) === 0; + + if (!isFirstIndex && this._pendingFrames.length === 0) { + return Left(new ReceiverApduError()); + } + + const dataSizeIndex = channelSize + HEAD_TAG_LENGTH + INDEX_LENGTH; + const dataSizeLength = isFirstIndex ? APDU_DATA_LENGTH_LENGTH : 0; + + if ( + apdu.length < + channelSize + HEAD_TAG_LENGTH + INDEX_LENGTH + dataSizeLength + ) { + return Left(new ReceiverApduError("Unable to parse header from apdu")); + } + + const dataSize = isFirstIndex + ? Just(apdu.slice(dataSizeIndex, dataSizeIndex + dataSizeLength)) + : Nothing; + + const dataIndex = dataSizeIndex + dataSizeLength; + const data = apdu.slice(dataIndex); + + const frame = new Frame({ + header: new FrameHeader({ + uuid: v4(), + channel: this._channel, + dataSize, + headTag, + index, + length: channelSize + HEAD_TAG_LENGTH + INDEX_LENGTH + dataSizeLength, + }), + data, + }); + + return Right(frame); + } + + /* + * Return true if all the datas has been received + * + * @param number + */ + private isComplete(dataSize: number): boolean { + const totalReceiveLength = this._pendingFrames.reduce( + (prev: number, curr: Frame) => prev + curr.getData().length, + 0, + ); + + return totalReceiveLength >= dataSize; + } + + private concatFrames(frames: Frame[]): Uint8Array { + return frames.reduce( + (prev: Uint8Array, curr: Frame) => + Uint8Array.from([...prev, ...curr.getData()]), + new Uint8Array(0), + ); + } +} diff --git a/packages/core/src/internal/device-session/service/DefaultFramerService.ts b/packages/core/src/internal/device-session/service/DefaultFramerService.ts index 201b73352..feb7ec894 100644 --- a/packages/core/src/internal/device-session/service/DefaultFramerService.ts +++ b/packages/core/src/internal/device-session/service/DefaultFramerService.ts @@ -3,11 +3,11 @@ import { Either, Left, Maybe, Right } from "purify-ts"; import { v4 } from "uuid"; import { - APDU_DATA_SIZE, - CHANNEL_SIZE, + APDU_DATA_LENGTH_LENGTH, + CHANNEL_LENGTH, HEAD_TAG, - HEAD_TAG_SIZE, - INDEX_SIZE, + HEAD_TAG_LENGTH, + INDEX_LENGTH, } from "@internal/device-session/data/FramerConst"; import { FramerApduError, @@ -128,7 +128,7 @@ export class DefaultFramerService implements FramerService { const header = new FrameHeader({ uuid: v4(), channel: this._channel.map((channel) => - FramerUtils.getLastBytesFrom(channel, CHANNEL_SIZE), + FramerUtils.getLastBytesFrom(channel, CHANNEL_LENGTH), ), headTag: new Uint8Array([HEAD_TAG]), index: new Uint8Array([Math.floor(frameIndex / 0xff), frameIndex & 0xff]), @@ -170,12 +170,12 @@ export class DefaultFramerService implements FramerService { private getFrameHeaderSizeFromIndex(frameIndex: number): number { return ( this._channel.caseOf({ - Just: () => CHANNEL_SIZE, + Just: () => CHANNEL_LENGTH, Nothing: () => 0, }) + - INDEX_SIZE + - HEAD_TAG_SIZE + - (frameIndex === 0 ? APDU_DATA_SIZE : 0) + INDEX_LENGTH + + HEAD_TAG_LENGTH + + (frameIndex === 0 ? APDU_DATA_LENGTH_LENGTH : 0) ); } } diff --git a/packages/core/src/internal/device-session/utils/FramerUtils.test.ts b/packages/core/src/internal/device-session/utils/FramerUtils.test.ts index 8517d9c4c..e71eb307d 100644 --- a/packages/core/src/internal/device-session/utils/FramerUtils.test.ts +++ b/packages/core/src/internal/device-session/utils/FramerUtils.test.ts @@ -30,4 +30,53 @@ describe("FramerUtils", () => { expect(result).toEqual(new Uint8Array([])); }); }); + + describe("getLastBytesFrom", () => { + it("should return 2 same bytes of Uint8Array", () => { + // Arrange + const array = new Uint8Array([0x67, 0x89]); + const size = 2; + // Act + const result = FramerUtils.getFirstBytesFrom(array, size); + // Assert + expect(result).toEqual(new Uint8Array([0x67, 0x89])); + }); + it("should return 2 first bytes of Uint8Array", () => { + // Arrange + const array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + const size = 2; + // Act + const result = FramerUtils.getFirstBytesFrom(array, size); + // Assert + expect(result).toEqual(new Uint8Array([1, 2])); + }); + it("should return empty Uint8Array", () => { + // Arrange + const array = new Uint8Array([]); + const size = 2; + // Act + const result = FramerUtils.getLastBytesFrom(array, size); + // Assert + expect(result).toEqual(new Uint8Array([])); + }); + }); + + describe("bytesToNumber", () => { + it("should return a correct number", () => { + // Arrange + const array = new Uint8Array([0x67, 0x89]); + // Act + const result = FramerUtils.bytesToNumber(array); + // Assert + expect(result).toEqual(26505); + }); + it("should return 0 when array is empty", () => { + // Arrange + const array = new Uint8Array([]); + // Act + const result = FramerUtils.bytesToNumber(array); + // Assert + expect(result).toEqual(0); + }); + }); }); diff --git a/packages/core/src/internal/device-session/utils/FramerUtils.ts b/packages/core/src/internal/device-session/utils/FramerUtils.ts index 6d55b6105..b8bacb434 100644 --- a/packages/core/src/internal/device-session/utils/FramerUtils.ts +++ b/packages/core/src/internal/device-session/utils/FramerUtils.ts @@ -7,4 +7,26 @@ export const FramerUtils = { getLastBytesFrom(array: Uint8Array, size: number): Uint8Array { return new Uint8Array(array.slice(-size)); }, + + /* + * Get first bytes of Uint8Array + * + * @param Uint8Array + */ + getFirstBytesFrom(array: Uint8Array, size: number): Uint8Array { + return new Uint8Array(array.slice(0, size)); + }, + + /* + * Get number from Uint8Array + * + * @param Uint8Array + */ + bytesToNumber(array: Uint8Array) { + return array.reduce( + (acc, val, index) => + acc + val * Math.pow(0x100, array.length - 1 - index), + 0, + ); + }, };