Skip to content

Commit

Permalink
✨ (dmk): Add API calls and models for secure channel
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Nov 22, 2024
1 parent 8c2c05d commit 307b674
Show file tree
Hide file tree
Showing 20 changed files with 426 additions and 51 deletions.
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(
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 @@ -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>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type ManagerApiDataSource } from "@internal/manager-api/data/ManagerApiDataSource";

export class AxiosManagerApiDataSource implements ManagerApiDataSource {
getDeviceVersion = jest.fn();
getFirmwareVersion = jest.fn();
getAppsByHash = jest.fn();
}
Loading

0 comments on commit 307b674

Please sign in to comment.