Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (dmk) [DSDK-542]: Add API calls and models for secure channel #469

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rare-elephants-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/device-management-kit": patch
---

Add manager api calls for secure channel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { DeviceModelId } from "@api/device/DeviceModel";
import { ApduResponse } from "@api/device-session/ApduResponse";

import { getOsVersionCommandResponseStubBuilder } from "./__mocks__/GetOsVersionCommand";
import { GetOsVersionCommand } from "./GetOsVersionCommand";

const GET_OS_VERSION_APDU = Uint8Array.from([0xe0, 0x01, 0x00, 0x00, 0x00]);
Expand Down Expand Up @@ -62,16 +63,7 @@ describe("GetOsVersionCommand", () => {
);

const expected = CommandResultFactory({
data: {
targetId: "33000004",
seVersion: "2.2.3",
seFlags: 3858759680,
mcuSephVersion: "2.30",
mcuBootloaderVersion: "1.16",
hwVersion: "00",
langId: "00",
recoverState: "00",
},
data: getOsVersionCommandResponseStubBuilder(DeviceModelId.NANO_X),
});

expect(parsed).toStrictEqual(expected);
Expand All @@ -86,16 +78,7 @@ describe("GetOsVersionCommand", () => {
);

const expected = CommandResultFactory({
data: {
targetId: "33100004",
seVersion: "1.1.1",
seFlags: 3858759680,
mcuSephVersion: "4.03",
mcuBootloaderVersion: "3.12",
hwVersion: "00",
langId: "00",
recoverState: "00",
},
data: getOsVersionCommandResponseStubBuilder(DeviceModelId.NANO_SP),
});

expect(parsed).toStrictEqual(expected);
Expand All @@ -110,16 +93,7 @@ describe("GetOsVersionCommand", () => {
);

const expected = CommandResultFactory({
data: {
targetId: "33200004",
seVersion: "1.3.0",
seFlags: 3858759680,
mcuSephVersion: "5.24",
mcuBootloaderVersion: "0.48",
hwVersion: "00",
langId: "00",
recoverState: "00",
},
data: getOsVersionCommandResponseStubBuilder(DeviceModelId.STAX),
});

expect(parsed).toStrictEqual(expected);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type GetOsVersionResponse = {
/**
* Target identifier.
*/
readonly targetId: string;
readonly targetId: number;

/**
* Version of BOLOS on the secure element (SE).
Expand Down Expand Up @@ -91,7 +91,10 @@ export class GetOsVersionCommand implements Command<GetOsVersionResponse> {
}
const parser = new ApduParser(apduResponse);

const targetId = parser.encodeToHexaString(parser.extractFieldByLength(4));
const targetId = parseInt(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ASK] why parseInt then toString on a string ?

Copy link
Contributor Author

@jdabbech-ledger jdabbech-ledger Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To convert the hexastring to a decimal string, because the API only accept base 10 number for targetId.
The base 10 conversion has to be done on the whole number.

parser.encodeToHexaString(parser.extractFieldByLength(4)),
16,
);
const seVersion = parser.encodeToString(parser.extractFieldLVEncoded());
const seFlags = parseInt(
parser.encodeToHexaString(parser.extractFieldLVEncoded()).toString(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DeviceModelId } from "@api/device/DeviceModel";
import { type GetOsVersionResponse } from "@api/index";

export const getOsVersionCommandResponseStubBuilder = (
deviceModelId: DeviceModelId = DeviceModelId.NANO_SP,
props: Partial<GetOsVersionResponse> = {},
): GetOsVersionResponse =>
({
[DeviceModelId.NANO_SP]: {
targetId: 856686596,
seVersion: "1.1.1",
seFlags: 3858759680,
mcuSephVersion: "4.03",
mcuBootloaderVersion: "3.12",
hwVersion: "00",
langId: "00",
recoverState: "00",
...props,
},
[DeviceModelId.NANO_S]: {
targetId: 858783748,
seVersion: "1.1.1",
seFlags: 3858759680,
mcuSephVersion: "6.4.0",
mcuBootloaderVersion: "5.4.0",
hwVersion: "00",
langId: "00",
recoverState: "00",
...props,
},
[DeviceModelId.NANO_X]: {
targetId: 855638020,
seVersion: "2.2.3",
seFlags: 3858759680,
mcuSephVersion: "2.30",
mcuBootloaderVersion: "1.16",
hwVersion: "00",
langId: "00",
recoverState: "00",
},
[DeviceModelId.STAX]: {
targetId: 857735172,
seVersion: "1.3.0",
seFlags: 3858759680,
mcuSephVersion: "5.24",
mcuBootloaderVersion: "0.48",
hwVersion: "00",
langId: "00",
recoverState: "00",
...props,
},
[DeviceModelId.FLEX]: {
targetId: 858783748,
seVersion: "1.1.1",
seFlags: 3858759680,
mcuSephVersion: "6.4.0",
mcuBootloaderVersion: "5.4.0",
hwVersion: "00",
langId: "00",
recoverState: "00",
...props,
},
})[deviceModelId];
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppType } from "@internal/manager-api/model/ManagerApiType";
import { AppType } from "@internal/manager-api/model/Application";

export const BTC_APP = {
appEntryLength: 77,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
XStateDeviceAction,
} from "@api/device-action/xstate-utils/XStateDeviceAction";
import { type DeviceSessionState } from "@api/device-session/DeviceSessionState";
import { type Application } from "@internal/manager-api/model/Application";
import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { type Application } from "@internal/manager-api/model/ManagerApiType";

import {
type ListAppsWithMetadataDAError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
type ListAppsDAInput,
type ListAppsDAIntermediateValue,
} from "@api/device-action/os/ListApps/types";
import { type Application } from "@internal/manager-api/model/Application";
import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { type Application } from "@internal/manager-api/model/ManagerApiType";

export type ListAppsWithMetadataDAOutput = Array<Application | null>;
export type ListAppsWithMetadataDAInput = ListAppsDAInput;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type GetAppAndVersionResponse } from "@api/command/os/GetAppAndVersionCommand";
import { type BatteryStatusFlags } from "@api/command/os/GetBatteryStatusCommand";
import { type DeviceStatus } from "@api/device/DeviceStatus";
import { type Application } from "@internal/manager-api/model/ManagerApiType";
import { type Application } from "@internal/manager-api/model/Application";

/**
* The battery status of a device.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export class DeviceSession {
rawApdu,
options.triggersDisconnection,
);
console.log("errrorOrResponse", rawApdu);

return errorOrResponse.ifRight((response: ApduResponse) => {
if (CommandUtils.isLockedDeviceResponse(response)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import {
DEFAULT_MANAGER_API_BASE_URL,
DEFAULT_MOCK_SERVER_BASE_URL,
} from "@internal/manager-api//model/Const";
import { deviceVersionMockBuilder } from "@internal/manager-api/data/__mocks__/GetDeviceVersion";
import { firmwareVersionMockBuilder } from "@internal/manager-api/data/__mocks__/GetFirmwareVersion";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";

import { AxiosManagerApiDataSource } from "./AxiosManagerApiDataSource";

jest.mock("axios");

describe("AxiosManagerApiDataSource", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("getAppsByHash", () => {
describe("success cases", () => {
it("with BTC app, should return the metadata", async () => {
Expand Down Expand Up @@ -78,7 +83,8 @@ describe("AxiosManagerApiDataSource", () => {
});

describe("error cases", () => {
it("should throw an error if the request fails", async () => {
it("should throw an error if the request fails", () => {
// given
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
Expand All @@ -88,12 +94,82 @@ describe("AxiosManagerApiDataSource", () => {

const hashes = [BTC_APP.appFullHash];

try {
await api.getAppsByHash(hashes);
} catch (error) {
expect(error).toEqual(Left(new HttpFetchApiError(err)));
}
// when
const response = api.getAppsByHash(hashes);

// then
expect(response).resolves.toEqual(Left(new HttpFetchApiError(err)));
});
});
});

describe("getDeviceVersion", () => {
it("should return a complete device version", () => {
// given
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
const mockedDeviceVersion = deviceVersionMockBuilder();
jest
.spyOn(axios, "get")
.mockResolvedValueOnce({ data: mockedDeviceVersion });

// when
const response = api.getDeviceVersion("targetId", 42);

// then
expect(response).resolves.toEqual(Right(mockedDeviceVersion));
});
it("should return an error if the request fails", () => {
// given
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
const error = new Error("fetch error");
jest.spyOn(axios, "get").mockRejectedValue(error);

// when
const response = api.getDeviceVersion("targetId", 42);

// then
expect(response).resolves.toEqual(Left(new HttpFetchApiError(error)));
});
});

describe("getFirmwareVersion", () => {
it("should return a complete firmware version", () => {
// given
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
const mockedFirmwareVersion = firmwareVersionMockBuilder();
jest
.spyOn(axios, "get")
.mockResolvedValueOnce({ data: mockedFirmwareVersion });

// when
const response = api.getFirmwareVersion("versionName", 42, 21);

// then
expect(response).resolves.toEqual(Right(mockedFirmwareVersion));
});
it("should return an error if the request fails", () => {
// given
const api = new AxiosManagerApiDataSource({
managerApiUrl: DEFAULT_MANAGER_API_BASE_URL,
mockUrl: DEFAULT_MOCK_SERVER_BASE_URL,
});
const error = new Error("fetch error");
jest.spyOn(axios, "get").mockRejectedValue(error);

// when
const response = api.getFirmwareVersion("versionName", 42, 21);

// then
expect(response).resolves.toEqual(Left(new HttpFetchApiError(error)));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,58 @@ import { EitherAsync } from "purify-ts";

import { type DmkConfig } from "@api/DmkConfig";
import { managerApiTypes } from "@internal/manager-api/di/managerApiTypes";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import {
Application,
type Application,
AppType,
} from "@internal/manager-api/model/ManagerApiType";
} from "@internal/manager-api/model/Application";
import { type DeviceVersion } from "@internal/manager-api/model/Device";
import { HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { type FinalFirmware } from "@internal/manager-api/model/Firmware";

import { ManagerApiDataSource } from "./ManagerApiDataSource";
import { ApplicationDto, AppTypeDto } from "./ManagerApiDto";

@injectable()
export class AxiosManagerApiDataSource implements ManagerApiDataSource {
private readonly baseUrl: string;

constructor(@inject(managerApiTypes.DmkConfig) config: DmkConfig) {
this.baseUrl = config.managerApiUrl;
}

getDeviceVersion(
targetId: string,
provider: number,
): EitherAsync<HttpFetchApiError, DeviceVersion> {
return EitherAsync(() =>
axios.get<DeviceVersion>(`${this.baseUrl}/get_device_version`, {
params: {
target_id: targetId,
provider,
},
}),
)
.map((res) => res.data)
.mapLeft((error) => new HttpFetchApiError(error));
}
getFirmwareVersion(
version: string,
deviceId: number,
provider: number,
): EitherAsync<HttpFetchApiError, FinalFirmware> {
return EitherAsync(() =>
axios.get<FinalFirmware>(`${this.baseUrl}/get_firmware_version`, {
params: {
device_version: deviceId,
version_name: version,
provider,
},
}),
)
.map((res) => res.data)
.mapLeft((error) => new HttpFetchApiError(error));
}

private mapAppTypeDtoToAppType(appType: AppTypeDto): AppType {
switch (appType) {
case AppTypeDto.currency:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { type EitherAsync } from "purify-ts";

import { type Application } from "@internal/manager-api/model/Application";
import { type DeviceVersion } from "@internal/manager-api/model/Device";
import { type HttpFetchApiError } from "@internal/manager-api/model/Errors";
import { type Application } from "@internal/manager-api/model/ManagerApiType";
import { type FinalFirmware } from "@internal/manager-api/model/Firmware";

export interface ManagerApiDataSource {
getAppsByHash(
hashes: string[],
): EitherAsync<HttpFetchApiError, Array<Application | null>>;
getDeviceVersion(
targetId: string,
provider: number,
): EitherAsync<HttpFetchApiError, DeviceVersion>;
getFirmwareVersion(
version: string,
deviceId: number,
provider: number,
): EitherAsync<HttpFetchApiError, FinalFirmware>;
}
Loading