Skip to content

Commit

Permalink
✨ (core) [DSDK-260]: Add sendCommand use case + GetOsVersion command (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman authored Apr 23, 2024
2 parents 641f0ac + 640df43 commit 95ec61a
Show file tree
Hide file tree
Showing 16 changed files with 501 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-insects-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-sdk-core": minor
---

Add SendCommand use case + GetOsVersion command
2 changes: 1 addition & 1 deletion packages/core/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { JestConfigWithTsJest } from "ts-jest";
const config: JestConfigWithTsJest = {
preset: "@ledgerhq/jest-config-dsdk",
setupFiles: ["<rootDir>/jest.setup.ts"],
testPathIgnorePatterns: ["<rootDir>/lib/"],
testPathIgnorePatterns: ["<rootDir>/lib/esm", "<rootDir>/lib/cjs"],
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.stub.ts",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/api/DeviceSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usbDiTypes } from "@internal/usb/di/usbDiTypes";
import pkg from "@root/package.json";
import { StubUseCase } from "@root/src/di.stub";

import { commandTypes } from "./command/di/commandTypes";
import { ConsoleLogger } from "./logger-subscriber/service/ConsoleLogger";
import { DeviceSdk } from "./DeviceSdk";

Expand Down Expand Up @@ -49,6 +50,10 @@ describe("DeviceSdk", () => {
it("should have getConnectedDevice method", () => {
expect(sdk.getConnectedDevice).toBeDefined();
});

it("should have sendCommand method", () => {
expect(sdk.sendCommand).toBeDefined();
});
});

describe("stubbed", () => {
Expand Down Expand Up @@ -78,6 +83,7 @@ describe("DeviceSdk", () => {
[discoveryTypes.StopDiscoveringUseCase],
[discoveryTypes.ConnectUseCase],
[sendTypes.SendApduUseCase],
[commandTypes.SendCommandUseCase],
[usbDiTypes.GetConnectedDeviceUseCase],
])("should have %p use case", (diSymbol) => {
const uc = sdk.container.get<StubUseCase>(diSymbol);
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/api/DeviceSdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Container } from "inversify";
import { Observable } from "rxjs";

import { commandTypes } from "@api/command/di/commandTypes";
import {
SendCommandUseCase,
SendCommandUseCaseArgs,
} from "@api/command/use-case/SendCommandUseCase";
import { ConnectedDevice } from "@api/usb/model/ConnectedDevice";
import { configTypes } from "@internal/config/di/configTypes";
import { GetSdkVersionUseCase } from "@internal/config/use-case/GetSdkVersionUseCase";
Expand Down Expand Up @@ -66,6 +71,12 @@ export class DeviceSdk {
.execute(args);
}

sendCommand<Params, T>(args: SendCommandUseCaseArgs<Params, T>): Promise<T> {
return this.container
.get<SendCommandUseCase>(commandTypes.SendCommandUseCase)
.execute(args);
}

getConnectedDevice(args: GetConnectedDeviceUseCaseArgs): ConnectedDevice {
return this.container
.get<GetConnectedDeviceUseCase>(usbDiTypes.GetConnectedDeviceUseCase)
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/api/command/Command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Apdu } from "@api/apdu/model/Apdu";
import { DeviceModelId } from "@internal/device-model/model/DeviceModel";
import { ApduResponse } from "@internal/device-session/model/ApduResponse";

export interface Command<Params, T> {
getApdu(params?: Params): Apdu;
parseResponse(responseApdu: Apdu): T;
parseResponse(responseApdu: ApduResponse, deviceModelId: DeviceModelId): T;
}
51 changes: 51 additions & 0 deletions packages/core/src/api/command/di/commandModule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Container } from "inversify";

import { SendCommandUseCase } from "@api/command/use-case/SendCommandUseCase";
import { deviceSessionModuleFactory } from "@internal/device-session/di/deviceSessionModule";
import { loggerModuleFactory } from "@internal/logger-publisher/di/loggerModule";
import { StubUseCase } from "@root/src/di.stub";

import { commandModuleFactory } from "./commandModule";
import { commandTypes } from "./commandTypes";

describe("commandModuleFactory", () => {
describe("Default", () => {
let container: Container;
let mod: ReturnType<typeof commandModuleFactory>;
beforeEach(() => {
mod = commandModuleFactory();
container = new Container();
container.load(mod, deviceSessionModuleFactory(), loggerModuleFactory());
});

it("should return the config module", () => {
expect(mod).toBeDefined();
});

it("should return non-stubbed sendCommand usecase", () => {
const sendCommandUseCase = container.get<SendCommandUseCase>(
commandTypes.SendCommandUseCase,
);
expect(sendCommandUseCase).toBeInstanceOf(SendCommandUseCase);
});
});

describe("Stubbed", () => {
let container: Container;
let mod: ReturnType<typeof commandModuleFactory>;
beforeEach(() => {
mod = commandModuleFactory({ stub: true });
container = new Container();
container.load(mod);
});

it("should return the config module", () => {
expect(mod).toBeDefined();
});

it("should return stubbed sendCommand usecase", () => {
const sendCommandUseCase = container.get(commandTypes.SendCommandUseCase);
expect(sendCommandUseCase).toBeInstanceOf(StubUseCase);
});
});
});
30 changes: 30 additions & 0 deletions packages/core/src/api/command/di/commandModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ContainerModule } from "inversify";

import { SendCommandUseCase } from "@api/command/use-case/SendCommandUseCase";
import { StubUseCase } from "@root/src/di.stub";

import { commandTypes } from "./commandTypes";

type CommandModuleArgs = Partial<{
stub: boolean;
}>;

export const commandModuleFactory = ({
stub = false,
}: CommandModuleArgs = {}) =>
new ContainerModule(
(
bind,
_unbind,
_isBound,
rebind,
_unbindAsync,
_onActivation,
_onDeactivation,
) => {
bind(commandTypes.SendCommandUseCase).to(SendCommandUseCase);
if (stub) {
rebind(commandTypes.SendCommandUseCase).to(StubUseCase);
}
},
);
3 changes: 3 additions & 0 deletions packages/core/src/api/command/di/commandTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const commandTypes = {
SendCommandUseCase: Symbol.for("SendCommandUseCase"),
};
136 changes: 136 additions & 0 deletions packages/core/src/api/command/os/GetOsVersionCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Command } from "@api/command/Command";
import { DeviceModelId } from "@api/types";
import { ApduResponse } from "@internal/device-session/model/ApduResponse";

import {
GetOsVersionCommand,
GetOsVersionResponse,
} from "./GetOsVersionCommand";

const GET_OS_VERSION_APDU = Uint8Array.from([0xe0, 0x01, 0x00, 0x00, 0x00]);

const LNX_RESPONSE_DATA_GOOD = Uint8Array.from([
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, 0x00, 0x01, 0x00, 0x90, 0x00,
]);
const LNX_RESPONSE_GOOD = new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: LNX_RESPONSE_DATA_GOOD,
});

const LNSP_REPONSE_DATA_GOOD = Uint8Array.from([
0x33, 0x10, 0x00, 0x04, 0x05, 0x31, 0x2e, 0x31, 0x2e, 0x31, 0x04, 0xe6, 0x00,
0x00, 0x00, 0x04, 0x34, 0x2e, 0x30, 0x33, 0x04, 0x33, 0x2e, 0x31, 0x32, 0x01,
0x00, 0x01, 0x00, 0x90, 0x00,
]);
const LNSP_RESPONSE_GOOD = new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: LNSP_REPONSE_DATA_GOOD,
});

const STAX_RESPONSE_DATA_GOOD = Uint8Array.from([
0x33, 0x20, 0x00, 0x04, 0x05, 0x31, 0x2e, 0x33, 0x2e, 0x30, 0x04, 0xe6, 0x00,
0x00, 0x00, 0x04, 0x35, 0x2e, 0x32, 0x34, 0x04, 0x30, 0x2e, 0x34, 0x38, 0x01,
0x00, 0x01, 0x00, 0x90, 0x00,
]);
const STAX_RESPONSE_GOOD = new ApduResponse({
statusCode: Uint8Array.from([0x90, 0x00]),
data: STAX_RESPONSE_DATA_GOOD,
});

describe("GetOsVersionCommand", () => {
let command: Command<void, GetOsVersionResponse>;

beforeEach(() => {
command = new GetOsVersionCommand();
});

describe("getApdu", () => {
it("should return the GetOsVersion apdu", () => {
const apdu = command.getApdu();
expect(apdu.getRawApdu()).toStrictEqual(GET_OS_VERSION_APDU);
});
});

describe("parseResponse", () => {
describe("Nano X", () => {
it("should parse the LNX response", () => {
const parsed = command.parseResponse(
LNX_RESPONSE_GOOD,
DeviceModelId.NANO_X,
);

const expected = {
targetId: "33000004",
seVersion: "2.2.3",
seFlags: 3858759680,
mcuSephVersion: "2.30",
mcuBootloaderVersion: "1.16",
hwVersion: "00",
langId: "00",
recoverState: "00",
};

expect(parsed).toStrictEqual(expected);
});
});

describe("Nano S Plus", () => {
it("should parse the LNSP response", () => {
const parsed = command.parseResponse(
LNSP_RESPONSE_GOOD,
DeviceModelId.NANO_SP,
);

const expected = {
targetId: "33100004",
seVersion: "1.1.1",
seFlags: 3858759680,
mcuSephVersion: "4.03",
mcuBootloaderVersion: "3.12",
hwVersion: "00",
langId: "00",
recoverState: "00",
};

expect(parsed).toStrictEqual(expected);
});
});

describe("Stax", () => {
it("should parse the STAX response", () => {
const parsed = command.parseResponse(
STAX_RESPONSE_GOOD,
DeviceModelId.STAX,
);

const expected = {
targetId: "33200004",
seVersion: "1.3.0",
seFlags: 3858759680,
mcuSephVersion: "5.24",
mcuBootloaderVersion: "0.48",
hwVersion: "00",
langId: "00",
recoverState: "00",
};

expect(parsed).toStrictEqual(expected);
});
});

describe("Error handling", () => {
it("should throw an error if the response is not successful", () => {
const response = new ApduResponse({
statusCode: Uint8Array.from([0x6e, 0x80]),
data: Uint8Array.from([]),
});

expect(() =>
command.parseResponse(response, DeviceModelId.NANO_X),
).toThrow("Unexpected status word: 6e80");
});
});
});
});
74 changes: 74 additions & 0 deletions packages/core/src/api/command/os/GetOsVersionCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Apdu } from "@api/apdu/model/Apdu";
import { ApduBuilder } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
import { Command } from "@api/command/Command";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { DeviceModelId } from "@api/types";
import { ApduResponse } from "@internal/device-session/model/ApduResponse";

export type GetOsVersionResponse = {
targetId: string;
seVersion: string;
seFlags: number;
mcuSephVersion: string;
mcuBootloaderVersion: string;
hwVersion: string;
langId: string;
recoverState: string;
};

export class GetOsVersionCommand
implements Command<void, GetOsVersionResponse>
{
getApdu = (): Apdu =>
new ApduBuilder({
cla: 0xe0,
ins: 0x01,
p1: 0x00,
p2: 0x00,
}).build();

parseResponse(responseApdu: ApduResponse, deviceModelId: DeviceModelId) {
const parser = new ApduParser(responseApdu);
if (!CommandUtils.isSuccessResponse(responseApdu)) {
// [ASK] How de we handle unsuccessful responses?
throw new Error(
`Unexpected status word: ${parser.encodeToHexaString(responseApdu.statusCode)}`,
);
}

const targetId = parser.encodeToHexaString(parser.extractFieldByLength(4));
const seVersion = parser.encodeToString(parser.extractFieldLVEncoded());
const seFlags = parseInt(
parser.encodeToHexaString(parser.extractFieldLVEncoded()).toString(),
16,
);
const mcuSephVersion = parser.encodeToString(
parser.extractFieldLVEncoded(),
);
const mcuBootloaderVersion = parser.encodeToString(
parser.extractFieldLVEncoded(),
);

let hwVersion = "00";
if (deviceModelId === DeviceModelId.NANO_X) {
hwVersion = parser.encodeToHexaString(parser.extractFieldLVEncoded());
}

const langId = parser.encodeToHexaString(parser.extractFieldLVEncoded());
const recoverState = parser.encodeToHexaString(
parser.extractFieldLVEncoded(),
);

return {
targetId,
seVersion,
seFlags,
mcuSephVersion,
mcuBootloaderVersion,
hwVersion,
langId,
recoverState: recoverState ? recoverState : "0",
};
}
}
Loading

0 comments on commit 95ec61a

Please sign in to comment.