@ledgerhq/device-core 0.2.1-next.1 → 0.3.0-nightly.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +9 -9
  3. package/lib/commands/entities/AppStorageInfo.d.ts +8 -0
  4. package/lib/commands/entities/AppStorageInfo.d.ts.map +1 -0
  5. package/lib/commands/entities/AppStorageInfo.js +3 -0
  6. package/lib/commands/entities/AppStorageInfo.js.map +1 -0
  7. package/lib/commands/use-cases/app-backup/backupAppStorage.d.ts +18 -0
  8. package/lib/commands/use-cases/app-backup/backupAppStorage.d.ts.map +1 -0
  9. package/lib/commands/use-cases/app-backup/backupAppStorage.js +98 -0
  10. package/lib/commands/use-cases/app-backup/backupAppStorage.js.map +1 -0
  11. package/lib/commands/use-cases/app-backup/backupAppStorage.test.d.ts +2 -0
  12. package/lib/commands/use-cases/app-backup/backupAppStorage.test.d.ts.map +1 -0
  13. package/lib/commands/use-cases/app-backup/backupAppStorage.test.js +56 -0
  14. package/lib/commands/use-cases/app-backup/backupAppStorage.test.js.map +1 -0
  15. package/lib/commands/use-cases/app-backup/getAppStorageInfo.d.ts +20 -0
  16. package/lib/commands/use-cases/app-backup/getAppStorageInfo.d.ts.map +1 -0
  17. package/lib/commands/use-cases/app-backup/getAppStorageInfo.js +104 -0
  18. package/lib/commands/use-cases/app-backup/getAppStorageInfo.js.map +1 -0
  19. package/lib/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts +2 -0
  20. package/lib/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts.map +1 -0
  21. package/lib/commands/use-cases/app-backup/getAppStorageInfo.test.js +59 -0
  22. package/lib/commands/use-cases/app-backup/getAppStorageInfo.test.js.map +1 -0
  23. package/lib/commands/use-cases/app-backup/restoreAppStorage.d.ts +12 -0
  24. package/lib/commands/use-cases/app-backup/restoreAppStorage.d.ts.map +1 -0
  25. package/lib/commands/use-cases/app-backup/restoreAppStorage.js +93 -0
  26. package/lib/commands/use-cases/app-backup/restoreAppStorage.js.map +1 -0
  27. package/lib/commands/use-cases/app-backup/restoreAppStorage.test.d.ts +2 -0
  28. package/lib/commands/use-cases/app-backup/restoreAppStorage.test.d.ts.map +1 -0
  29. package/lib/commands/use-cases/app-backup/restoreAppStorage.test.js +54 -0
  30. package/lib/commands/use-cases/app-backup/restoreAppStorage.test.js.map +1 -0
  31. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts +11 -0
  32. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts.map +1 -0
  33. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.js +83 -0
  34. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.js.map +1 -0
  35. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts +2 -0
  36. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts.map +1 -0
  37. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.test.js +49 -0
  38. package/lib/commands/use-cases/app-backup/restoreAppStorageCommit.test.js.map +1 -0
  39. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.d.ts +13 -0
  40. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.d.ts.map +1 -0
  41. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.js +99 -0
  42. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.js.map +1 -0
  43. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts +2 -0
  44. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts.map +1 -0
  45. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.test.js +52 -0
  46. package/lib/commands/use-cases/app-backup/restoreAppStorageInit.test.js.map +1 -0
  47. package/lib/commands/use-cases/getDeviceName.d.ts.map +1 -1
  48. package/lib/commands/use-cases/getDeviceName.js +15 -0
  49. package/lib/commands/use-cases/getDeviceName.js.map +1 -1
  50. package/lib/errors.d.ts +40 -0
  51. package/lib/errors.d.ts.map +1 -0
  52. package/lib/errors.js +30 -0
  53. package/lib/errors.js.map +1 -0
  54. package/lib/index.d.ts +6 -0
  55. package/lib/index.d.ts.map +1 -1
  56. package/lib/index.js +11 -1
  57. package/lib/index.js.map +1 -1
  58. package/lib-es/commands/entities/AppStorageInfo.d.ts +8 -0
  59. package/lib-es/commands/entities/AppStorageInfo.d.ts.map +1 -0
  60. package/lib-es/commands/entities/AppStorageInfo.js +2 -0
  61. package/lib-es/commands/entities/AppStorageInfo.js.map +1 -0
  62. package/lib-es/commands/use-cases/app-backup/backupAppStorage.d.ts +18 -0
  63. package/lib-es/commands/use-cases/app-backup/backupAppStorage.d.ts.map +1 -0
  64. package/lib-es/commands/use-cases/app-backup/backupAppStorage.js +93 -0
  65. package/lib-es/commands/use-cases/app-backup/backupAppStorage.js.map +1 -0
  66. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.d.ts +2 -0
  67. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.d.ts.map +1 -0
  68. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.js +54 -0
  69. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.js.map +1 -0
  70. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.d.ts +20 -0
  71. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.d.ts.map +1 -0
  72. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.js +99 -0
  73. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.js.map +1 -0
  74. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts +2 -0
  75. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts.map +1 -0
  76. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.js +57 -0
  77. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.js.map +1 -0
  78. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.d.ts +12 -0
  79. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.d.ts.map +1 -0
  80. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.js +88 -0
  81. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.js.map +1 -0
  82. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.d.ts +2 -0
  83. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.d.ts.map +1 -0
  84. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.js +52 -0
  85. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.js.map +1 -0
  86. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts +11 -0
  87. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts.map +1 -0
  88. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.js +78 -0
  89. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.js.map +1 -0
  90. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts +2 -0
  91. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts.map +1 -0
  92. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.js +47 -0
  93. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.js.map +1 -0
  94. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.d.ts +13 -0
  95. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.d.ts.map +1 -0
  96. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.js +94 -0
  97. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.js.map +1 -0
  98. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts +2 -0
  99. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts.map +1 -0
  100. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.js +50 -0
  101. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.js.map +1 -0
  102. package/lib-es/commands/use-cases/getDeviceName.d.ts.map +1 -1
  103. package/lib-es/commands/use-cases/getDeviceName.js +15 -0
  104. package/lib-es/commands/use-cases/getDeviceName.js.map +1 -1
  105. package/lib-es/errors.d.ts +40 -0
  106. package/lib-es/errors.d.ts.map +1 -0
  107. package/lib-es/errors.js +27 -0
  108. package/lib-es/errors.js.map +1 -0
  109. package/lib-es/index.d.ts +6 -0
  110. package/lib-es/index.d.ts.map +1 -1
  111. package/lib-es/index.js +5 -0
  112. package/lib-es/index.js.map +1 -1
  113. package/package.json +6 -6
  114. package/src/commands/entities/AppStorageInfo.ts +7 -0
  115. package/src/commands/use-cases/app-backup/backupAppStorage.test.ts +52 -0
  116. package/src/commands/use-cases/app-backup/backupAppStorage.ts +97 -0
  117. package/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts +63 -0
  118. package/src/commands/use-cases/app-backup/getAppStorageInfo.ts +102 -0
  119. package/src/commands/use-cases/app-backup/restoreAppStorage.test.ts +57 -0
  120. package/src/commands/use-cases/app-backup/restoreAppStorage.ts +93 -0
  121. package/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts +45 -0
  122. package/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts +82 -0
  123. package/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts +55 -0
  124. package/src/commands/use-cases/app-backup/restoreAppStorageInit.ts +96 -0
  125. package/src/commands/use-cases/getDeviceName.ts +16 -0
  126. package/src/errors.ts +29 -0
  127. package/src/index.ts +6 -0
@@ -0,0 +1,97 @@
1
+ import Transport, { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
2
+ import { LocalTracer } from "@ledgerhq/logs";
3
+ import type { APDU } from "../../entities/APDU";
4
+ import {
5
+ GenerateAesKeyFailed,
6
+ InternalComputeAesCmacFailed,
7
+ InternalCryptoOperationFailed,
8
+ InvalidBackupState,
9
+ InvalidContext,
10
+ } from "../../../errors";
11
+
12
+ /**
13
+ * Name in documentation: INS_APP_STORAGE_BACKUP
14
+ * cla: 0xe0
15
+ * ins: 0x6b
16
+ * p1: 0x00
17
+ * p2: 0x00
18
+ * lc: 0x00
19
+ */
20
+ const BACKUP_APP_STORAGE = [0xe0, 0x6b, 0x00, 0x00] as const;
21
+
22
+ /**
23
+ * 0x9000: Success.
24
+ * 0x5123: Invalid context, Get Info must be called.
25
+ * 0x5419: Failed to generate AES key.
26
+ * 0x541A: Internal error, crypto operation failed.
27
+ * 0x541B: Internal error, failed to compute AES CMAC.
28
+ * 0x541C: Failed to encrypt the app storage backup.
29
+ * 0x662F: Invalid device state, recovery mode.
30
+ * 0x6642: Invalid backup state, backup already performed.
31
+ */
32
+ const RESPONSE_STATUS_SET: number[] = [
33
+ StatusCodes.OK,
34
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
35
+ StatusCodes.GEN_AES_KEY_FAILED,
36
+ StatusCodes.INTERNAL_CRYPTO_OPERATION_FAILED,
37
+ StatusCodes.INTERNAL_COMPUTE_AES_CMAC_FAILED,
38
+ StatusCodes.ENCRYPT_APP_STORAGE_FAILED,
39
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
40
+ StatusCodes.INVALID_BACKUP_STATE,
41
+ ];
42
+
43
+ /**
44
+ * Retrieves the app storage information (chunk) from the device and returns it
45
+ * as a buffer.
46
+ *
47
+ * @param transport - The transport object used to communicate with the device.
48
+ * @returns A promise that resolves to the app storage information as a buffer.
49
+ */
50
+ export async function backupAppStorage(transport: Transport): Promise<Buffer> {
51
+ const tracer = new LocalTracer("hw", {
52
+ transport: transport.getTraceContext(),
53
+ function: "backupAppStorage",
54
+ });
55
+ tracer.trace("Start");
56
+
57
+ const apdu: Readonly<APDU> = [...BACKUP_APP_STORAGE, Buffer.from([0x00])];
58
+
59
+ const response = await transport.send(...apdu, RESPONSE_STATUS_SET);
60
+
61
+ return parseResponse(response);
62
+ }
63
+
64
+ /**
65
+ * Parses the response data buffer, check the status code and return the data.
66
+ *
67
+ * @param data - The response data buffer w/ status code.
68
+ * @returns The response data as a buffer w/o status code.
69
+ */
70
+ export function parseResponse(data: Buffer): Buffer {
71
+ const tracer = new LocalTracer("hw", {
72
+ function: "parseResponse@backupAppStorage",
73
+ });
74
+ const status = data.readUInt16BE(data.length - 2);
75
+ tracer.trace("Result status from 0xe06b0000", { status });
76
+
77
+ switch (status) {
78
+ case StatusCodes.OK:
79
+ return data.subarray(0, data.length - 2);
80
+ case StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT:
81
+ throw new InvalidContext("Invalid context, getAppStorageInfo must be called.");
82
+ case StatusCodes.GEN_AES_KEY_FAILED:
83
+ throw new GenerateAesKeyFailed("Failed to generate AES key.");
84
+ case StatusCodes.INTERNAL_CRYPTO_OPERATION_FAILED:
85
+ throw new InternalCryptoOperationFailed("Internal error, crypto operation failed.");
86
+ case StatusCodes.INTERNAL_COMPUTE_AES_CMAC_FAILED:
87
+ throw new InternalComputeAesCmacFailed("Internal error, failed to compute AES CMAC.");
88
+ case StatusCodes.ENCRYPT_APP_STORAGE_FAILED:
89
+ throw new GenerateAesKeyFailed("Failed to encrypt the app storage backup.");
90
+ case StatusCodes.DEVICE_IN_RECOVERY_MODE:
91
+ break;
92
+ case StatusCodes.INVALID_BACKUP_STATE:
93
+ throw new InvalidBackupState("Invalid backup state, backup already performed.");
94
+ }
95
+
96
+ throw new TransportStatusError(status);
97
+ }
@@ -0,0 +1,63 @@
1
+ import Transport, { StatusCodes } from "@ledgerhq/hw-transport";
2
+ import { getAppStorageInfo, parseResponse } from "./getAppStorageInfo";
3
+ import { AppStorageInfo } from "../../entities/AppStorageInfo";
4
+ import { InvalidAppNameLength } from "../../../errors";
5
+
6
+ jest.mock("@ledgerhq/hw-transport");
7
+
8
+ describe("getAppStorageInfo", () => {
9
+ let transport: Transport;
10
+ const response = Buffer.from([
11
+ 0x00, 0x00, 0x04, 0xd2, 0x31, 0x2e, 0x30, 0x31, 0x01, 0x01, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61,
12
+ 0x73, 0x68, 0x31, 0x32, 0x33, 0x34, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61,
13
+ 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x68, 0x61, 0x73, 0x68, 0x90, 0x00,
14
+ ]);
15
+
16
+ beforeEach(() => {
17
+ transport = {
18
+ send: jest.fn().mockResolvedValue(response),
19
+ getTraceContext: jest.fn().mockResolvedValue(undefined),
20
+ } as unknown as Transport;
21
+ });
22
+
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ it("should call the send function with correct parameters", async () => {
28
+ const appName = "MyApp";
29
+ await getAppStorageInfo(transport, appName);
30
+ expect(transport.send).toHaveBeenCalledWith(
31
+ 0xe0,
32
+ 0x6a,
33
+ 0x00,
34
+ 0x00,
35
+ Buffer.from([0x05, 0x4d, 0x79, 0x41, 0x70, 0x70]),
36
+ [
37
+ StatusCodes.OK,
38
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
39
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
40
+ StatusCodes.INVALID_APP_NAME_LENGTH,
41
+ ],
42
+ );
43
+ });
44
+
45
+ describe("parseResponse", () => {
46
+ it("should parse the response data correctly", () => {
47
+ const expected: AppStorageInfo = {
48
+ size: 1234,
49
+ dataVersion: "1.01",
50
+ hasSettings: true,
51
+ hasData: true,
52
+ hash: "hashhash1234hashhashhashhashhash",
53
+ };
54
+ expect(parseResponse(response)).toStrictEqual(expected);
55
+ });
56
+ it("should throw TransportStatusError if the response status is invalid", () => {
57
+ const data = Buffer.from([0x67, 0x0a]);
58
+ expect(() => parseResponse(data)).toThrow(
59
+ new InvalidAppNameLength("Invalid application name length, two chars minimum."),
60
+ );
61
+ });
62
+ });
63
+ });
@@ -0,0 +1,102 @@
1
+ import Transport, { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
2
+ import { LocalTracer } from "@ledgerhq/logs";
3
+ import type { AppStorageInfo } from "../../entities/AppStorageInfo";
4
+ import type { APDU } from "../../entities/APDU";
5
+ import { AppNotFound, InvalidAppNameLength } from "../../../errors";
6
+
7
+ /**
8
+ * Name in documentation: INS_APP_STORAGE_GET_INFO
9
+ * cla: 0xe0
10
+ * ins: 0x6a
11
+ * p1: 0x00
12
+ * p2: 0x00
13
+ * data: APP_NAME_LEN (1 byte) + APP_NAME (variable) to configure at runtime
14
+ */
15
+ const GET_APP_STORAGE_INFO = [0xe0, 0x6a, 0x00, 0x00] as const;
16
+
17
+ /**
18
+ * 0x9000: Success.
19
+ * 0x5123: Application not found.
20
+ * 0x662F: If the device is in recovery mode.
21
+ * 0x670A: Invalid application name length, two chars minimum.
22
+ */
23
+ const RESPONSE_STATUS_SET: number[] = [
24
+ StatusCodes.OK,
25
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
26
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
27
+ StatusCodes.INVALID_APP_NAME_LENGTH,
28
+ ];
29
+
30
+ /**
31
+ * Retrieves the application storage information from the device.
32
+ *
33
+ * @param transport - The transport object used to communicate with the device.
34
+ * @param appName - The name of the application to retrieve the storage information for.
35
+ * @returns A promise that resolves to the application storage information object.
36
+ * @throws {TransportStatusError} If the response status is invalid.
37
+ */
38
+ export async function getAppStorageInfo(
39
+ transport: Transport,
40
+ appName: string,
41
+ ): Promise<AppStorageInfo> {
42
+ const tracer = new LocalTracer("hw", {
43
+ transport: transport.getTraceContext(),
44
+ function: "getAppStorageInfo",
45
+ });
46
+ tracer.trace("Start");
47
+
48
+ const params: Buffer = Buffer.concat([
49
+ Buffer.from([appName.length]),
50
+ Buffer.from(appName, "ascii"),
51
+ ]);
52
+ const apdu: Readonly<APDU> = [...GET_APP_STORAGE_INFO, params];
53
+
54
+ const response = await transport.send(...apdu, RESPONSE_STATUS_SET);
55
+
56
+ return parseResponse(response);
57
+ }
58
+
59
+ /**
60
+ * Parses the response data from the device into a string.
61
+ *
62
+ * @param data - The response data received from the device.
63
+ * @returns A string representing the parsed response.
64
+ */
65
+ export function parseResponse(data: Buffer): AppStorageInfo {
66
+ const tracer = new LocalTracer("hw", {
67
+ function: "parseResponse@getAppStorageInfo",
68
+ });
69
+ const status = data.readUInt16BE(data.length - 2);
70
+ tracer.trace("Result status from 0xe06a0000", { status });
71
+
72
+ switch (status) {
73
+ case StatusCodes.OK: {
74
+ /**
75
+ * The backup size is a 4-byte unsigned integer.
76
+ * The data version is a 4-byte string.
77
+ * The hasSettings and hasData flags are 1-byte booleans.
78
+ * The hash is a 32-byte string.
79
+ */
80
+ let offset = 0;
81
+ const size = data.readUInt32BE(offset); // Len = 4
82
+ offset += 4;
83
+ const dataVersion = data.subarray(offset, offset + 4).toString(); // Len = 4
84
+ offset += 4;
85
+ const hasSettings = data.readUIntBE(offset, 1) === 1; // Len = 1
86
+ offset += 1;
87
+ const hasData = data.readUIntBE(offset, 1) === 1; // Len = 1
88
+ offset += 1;
89
+ const hash = data.subarray(offset, offset + 32).toString(); // Len = 32
90
+
91
+ return { size, dataVersion, hasSettings, hasData, hash };
92
+ }
93
+ case StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT:
94
+ throw new AppNotFound("Application not found.");
95
+ case StatusCodes.DEVICE_IN_RECOVERY_MODE:
96
+ break;
97
+ case StatusCodes.INVALID_APP_NAME_LENGTH:
98
+ throw new InvalidAppNameLength("Invalid application name length, two chars minimum.");
99
+ }
100
+
101
+ throw new TransportStatusError(status);
102
+ }
@@ -0,0 +1,57 @@
1
+ import Transport, { StatusCodes } from "@ledgerhq/hw-transport";
2
+ import { restoreAppStorage, parseResponse } from "./restoreAppStorage";
3
+ import { InvalidRestoreState } from "../../../errors";
4
+
5
+ jest.mock("@ledgerhq/hw-transport");
6
+
7
+ describe("restoreAppStorage", () => {
8
+ let transport: Transport;
9
+ const response = Buffer.from([0x90, 0x00]);
10
+
11
+ beforeEach(() => {
12
+ transport = {
13
+ send: jest.fn().mockResolvedValue(response),
14
+ getTraceContext: jest.fn().mockResolvedValue(undefined),
15
+ } as unknown as Transport;
16
+ });
17
+
18
+ afterEach(() => {
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ it("should call the send function with correct parameters", async () => {
23
+ const chunk = Buffer.from("106RueduTemple");
24
+ await restoreAppStorage(transport, chunk);
25
+ expect(transport.send).toHaveBeenCalledWith(
26
+ 0xe0,
27
+ 0x6d,
28
+ 0x00,
29
+ 0x00,
30
+ Buffer.from([
31
+ 0x0e, 0x31, 0x30, 0x36, 0x52, 0x75, 0x65, 0x64, 0x75, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x65,
32
+ ]),
33
+ [
34
+ StatusCodes.OK,
35
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
36
+ StatusCodes.GEN_AES_KEY_FAILED,
37
+ StatusCodes.INTERNAL_CRYPTO_OPERATION_FAILED,
38
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
39
+ StatusCodes.INVALID_RESTORE_STATE,
40
+ StatusCodes.INVALID_CHUNK_LENGTH,
41
+ StatusCodes.INVALID_BACKUP_HEADER,
42
+ ],
43
+ );
44
+ });
45
+
46
+ describe("parseResponse", () => {
47
+ it("should parse the response data correctly", () => {
48
+ expect(() => parseResponse(response)).not.toThrow();
49
+ });
50
+ it("should throw TransportStatusError if the response status is invalid", () => {
51
+ const data = Buffer.from([0x66, 0x43]);
52
+ expect(() => parseResponse(data)).toThrow(
53
+ new InvalidRestoreState("Invalid restore state, restore already performed."),
54
+ );
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,93 @@
1
+ import Transport, { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
2
+ import { LocalTracer } from "@ledgerhq/logs";
3
+ import type { APDU } from "../../entities/APDU";
4
+ import {
5
+ GenerateAesKeyFailed,
6
+ InternalCryptoOperationFailed,
7
+ InvalidBackupHeader,
8
+ InvalidChunkLength,
9
+ InvalidContext,
10
+ InvalidRestoreState,
11
+ } from "../../../errors";
12
+
13
+ /**
14
+ * Name in documentation: INS_APP_STORAGE_GET_INFO
15
+ * cla: 0xe0
16
+ * ins: 0x6d
17
+ * p1: 0x00
18
+ * p2: 0x00
19
+ * data: CHUNK_LEN + CHUNK to configure at runtime
20
+ */
21
+ const RESTORE_APP_STORAGE = [0xe0, 0x6d, 0x00, 0x00] as const;
22
+
23
+ /**
24
+ * 0x9000: Success.
25
+ * 0x5123: Invalid context, Restore Init must be called first.
26
+ * 0x5419: Failed to generate AES key.
27
+ * 0x541A: Failed to decrypt the app storage backup.
28
+ * 0x662F: Invalid device state, recovery mode.
29
+ * 0x6643: Invalid restore state, restore already performed.
30
+ * 0x6734: Invalid CHUNK_LEN.
31
+ * 0x684A: Invalid backup, app storage header is not valid.
32
+ */
33
+ const RESPONSE_STATUS_SET: number[] = [
34
+ StatusCodes.OK,
35
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
36
+ StatusCodes.GEN_AES_KEY_FAILED,
37
+ StatusCodes.INTERNAL_CRYPTO_OPERATION_FAILED,
38
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
39
+ StatusCodes.INVALID_RESTORE_STATE,
40
+ StatusCodes.INVALID_CHUNK_LENGTH,
41
+ StatusCodes.INVALID_BACKUP_HEADER,
42
+ ];
43
+
44
+ /**
45
+ * Restores the application storage.
46
+ *
47
+ * @param transport - The transport object used for communication with the device.
48
+ * @param chunk - The chunk of data to restore.
49
+ * @returns A promise that resolves to void.
50
+ */
51
+ export async function restoreAppStorage(transport: Transport, chunk: Buffer): Promise<void> {
52
+ const tracer = new LocalTracer("hw", {
53
+ transport: transport.getTraceContext(),
54
+ function: "restoreAppStorage",
55
+ });
56
+ tracer.trace("Start");
57
+
58
+ const params = Buffer.concat([Buffer.from([chunk.length]), chunk]);
59
+ const apdu: Readonly<APDU> = [...RESTORE_APP_STORAGE, params];
60
+
61
+ const response = await transport.send(...apdu, RESPONSE_STATUS_SET);
62
+
63
+ parseResponse(response);
64
+ }
65
+
66
+ export function parseResponse(data: Buffer): void {
67
+ const tracer = new LocalTracer("hw", {
68
+ function: "parseResponse@restoreAppStorage",
69
+ });
70
+ const status = data.readUInt16BE(data.length - 2);
71
+ tracer.trace("Result status from 0xe06d0000", { status });
72
+
73
+ switch (status) {
74
+ case StatusCodes.OK:
75
+ return;
76
+ case StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT:
77
+ throw new InvalidContext("Invalid context, restoreAppStorageInit must be called first.");
78
+ case StatusCodes.GEN_AES_KEY_FAILED:
79
+ throw new GenerateAesKeyFailed("Failed to generate AES key.");
80
+ case StatusCodes.INTERNAL_CRYPTO_OPERATION_FAILED:
81
+ throw new InternalCryptoOperationFailed("Failed to decrypt the app storage backup.");
82
+ case StatusCodes.DEVICE_IN_RECOVERY_MODE:
83
+ break;
84
+ case StatusCodes.INVALID_RESTORE_STATE:
85
+ throw new InvalidRestoreState("Invalid restore state, restore already performed.");
86
+ case StatusCodes.INVALID_CHUNK_LENGTH:
87
+ throw new InvalidChunkLength("Invalid chunk length.");
88
+ case StatusCodes.INVALID_BACKUP_HEADER:
89
+ throw new InvalidBackupHeader("Invalid backup, app storage header is not valid.");
90
+ }
91
+
92
+ throw new TransportStatusError(status);
93
+ }
@@ -0,0 +1,45 @@
1
+ import Transport, { StatusCodes } from "@ledgerhq/hw-transport";
2
+ import { restoreAppStorageCommit, parseResponse } from "./restoreAppStorageCommit";
3
+ import { InvalidChunkLength } from "../../../errors";
4
+
5
+ jest.mock("@ledgerhq/hw-transport");
6
+
7
+ describe("restoreAppStorageCommit", () => {
8
+ let transport: Transport;
9
+ const response = Buffer.from([0x90, 0x00]);
10
+
11
+ beforeEach(() => {
12
+ transport = {
13
+ send: jest.fn().mockResolvedValue(response),
14
+ getTraceContext: jest.fn().mockResolvedValue(undefined),
15
+ } as unknown as Transport;
16
+ });
17
+
18
+ afterEach(() => {
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ it("should call the send function with correct parameters", async () => {
23
+ await restoreAppStorageCommit(transport);
24
+ expect(transport.send).toHaveBeenCalledWith(0xe0, 0x6e, 0x00, 0x00, Buffer.from([0x00]), [
25
+ StatusCodes.OK,
26
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
27
+ StatusCodes.GEN_AES_KEY_FAILED,
28
+ StatusCodes.INTERNAL_COMPUTE_AES_CMAC_FAILED,
29
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
30
+ StatusCodes.INVALID_CHUNK_LENGTH,
31
+ ]);
32
+ });
33
+
34
+ describe("parseResponse", () => {
35
+ it("should parse the response data correctly", () => {
36
+ expect(() => parseResponse(response)).not.toThrow();
37
+ });
38
+ it("should throw TransportStatusError if the response status is invalid", () => {
39
+ const data = Buffer.from([0x67, 0x34]);
40
+ expect(() => parseResponse(data)).toThrow(
41
+ new InvalidChunkLength("Invalid size of the restored app storage."),
42
+ );
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,82 @@
1
+ import Transport, { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
2
+ import { LocalTracer } from "@ledgerhq/logs";
3
+ import type { APDU } from "../../entities/APDU";
4
+ import {
5
+ GenerateAesKeyFailed,
6
+ InternalComputeAesCmacFailed,
7
+ InvalidChunkLength,
8
+ InvalidContext,
9
+ } from "../../../errors";
10
+
11
+ /**
12
+ * Name in documentation: INS_APP_STORAGE_RESTORE_COMMIT
13
+ * cla: 0xe0
14
+ * ins: 0x6e
15
+ * p1: 0x00
16
+ * p2: 0x00
17
+ * lc: 0x00
18
+ */
19
+ const RESTORE_APP_STORAGE_COMMIT = [0xe0, 0x6e, 0x00, 0x00] as const;
20
+
21
+ /**
22
+ * 0x9000: Success.
23
+ * 0x5123: Invalid context, Restore Init must be called first.
24
+ * 0x5419: Internal error, crypto operaiton failed.
25
+ * 0x541B: Failed to verify backup authenticity.
26
+ * 0x662F: Invalid device state, recovery mode.
27
+ * 0x6734: Invalid size of the restored app storage.
28
+ */
29
+ const RESPONSE_STATUS_SET: number[] = [
30
+ StatusCodes.OK,
31
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
32
+ StatusCodes.GEN_AES_KEY_FAILED,
33
+ StatusCodes.INTERNAL_COMPUTE_AES_CMAC_FAILED,
34
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
35
+ StatusCodes.INVALID_CHUNK_LENGTH,
36
+ ];
37
+
38
+ /**
39
+ * Restores the application storage commit.
40
+ *
41
+ * @param transport - The transport object used for communication with the device.
42
+ * @returns A promise that resolves to void.
43
+ */
44
+
45
+ export async function restoreAppStorageCommit(transport: Transport): Promise<void> {
46
+ const tracer = new LocalTracer("hw", {
47
+ transport: transport.getTraceContext(),
48
+ function: "restoreAppStorageCommit",
49
+ });
50
+ tracer.trace("Start");
51
+
52
+ const apdu: Readonly<APDU> = [...RESTORE_APP_STORAGE_COMMIT, Buffer.from([0x00])];
53
+
54
+ const response = await transport.send(...apdu, RESPONSE_STATUS_SET);
55
+
56
+ parseResponse(response);
57
+ }
58
+
59
+ export function parseResponse(data: Buffer): void {
60
+ const tracer = new LocalTracer("hw", {
61
+ function: "parseResponse@restoreAppStorageCommit",
62
+ });
63
+ const status = data.readUInt16BE(data.length - 2);
64
+ tracer.trace("Result status from 0xe06e0000", { status });
65
+
66
+ switch (status) {
67
+ case StatusCodes.OK:
68
+ return;
69
+ case StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT:
70
+ throw new InvalidContext("Invalid context, restoreAppStorageInit must be called first.");
71
+ case StatusCodes.GEN_AES_KEY_FAILED:
72
+ throw new GenerateAesKeyFailed("Internal error, crypto operation failed.");
73
+ case StatusCodes.INTERNAL_COMPUTE_AES_CMAC_FAILED:
74
+ throw new InternalComputeAesCmacFailed("Failed to verify backup authenticity.");
75
+ case StatusCodes.DEVICE_IN_RECOVERY_MODE:
76
+ break;
77
+ case StatusCodes.INVALID_CHUNK_LENGTH:
78
+ throw new InvalidChunkLength("Invalid size of the restored app storage.");
79
+ }
80
+
81
+ throw new TransportStatusError(status);
82
+ }
@@ -0,0 +1,55 @@
1
+ import Transport, { StatusCodes } from "@ledgerhq/hw-transport";
2
+ import { restoreAppStorageInit, parseResponse } from "./restoreAppStorageInit";
3
+ import { InvalidAppNameLength } from "../../../errors";
4
+
5
+ jest.mock("@ledgerhq/hw-transport");
6
+
7
+ describe("restoreAppStorageInit", () => {
8
+ let transport: Transport;
9
+ const response = Buffer.from([0x90, 0x00]);
10
+
11
+ beforeEach(() => {
12
+ transport = {
13
+ send: jest.fn().mockResolvedValue(response),
14
+ getTraceContext: jest.fn().mockResolvedValue(undefined),
15
+ } as unknown as Transport;
16
+ });
17
+
18
+ afterEach(() => {
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ it("should call the send function with correct parameters", async () => {
23
+ const appName = "MyApp";
24
+ const backupSize = 1234;
25
+ await restoreAppStorageInit(transport, appName, backupSize);
26
+ expect(transport.send).toHaveBeenCalledWith(
27
+ 0xe0,
28
+ 0x6c,
29
+ 0x00,
30
+ 0x00,
31
+ Buffer.from([0x09, 0x00, 0x00, 0x04, 0xd2, 0x4d, 0x79, 0x41, 0x70, 0x70]),
32
+ [
33
+ StatusCodes.OK,
34
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
35
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
36
+ StatusCodes.USER_REFUSED_ON_DEVICE,
37
+ StatusCodes.PIN_NOT_SET,
38
+ StatusCodes.INVALID_APP_NAME_LENGTH,
39
+ StatusCodes.INVALID_BACKUP_LENGTH,
40
+ ],
41
+ );
42
+ });
43
+
44
+ describe("parseResponse", () => {
45
+ it("should parse the response data correctly", () => {
46
+ expect(() => parseResponse(response)).not.toThrow();
47
+ });
48
+ it("should throw TransportStatusError if the response status is invalid", () => {
49
+ const data = Buffer.from([0x67, 0x0a]);
50
+ expect(() => parseResponse(data)).toThrow(
51
+ new InvalidAppNameLength("Invalid application name length, two chars minimum."),
52
+ );
53
+ });
54
+ });
55
+ });
@@ -0,0 +1,96 @@
1
+ import Transport, { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
2
+ import { LocalTracer } from "@ledgerhq/logs";
3
+ import type { APDU } from "../../entities/APDU";
4
+ import { AppNotFound, InvalidAppNameLength, InvalidBackupLength, PinNotSet } from "../../../errors";
5
+
6
+ /**
7
+ * Name in documentation: INS_APP_STORAGE_RESTORE_INIT
8
+ * cla: 0xe0
9
+ * ins: 0x6c
10
+ * p1: 0x00
11
+ * p2: 0x00
12
+ * data:
13
+ * - LC: BACKUP_LEN_LEN (=0x04) + APP_NAME_LEN (1 byte)
14
+ * - DATA: BACKUP_LEN + APP_NAME
15
+ *
16
+ * For example, the 'bitcoin' app with backup of length 0x00007000:
17
+ * 1. LC is 0x0b
18
+ * 2. DATA is 0x00007000 0x626974636f696e
19
+ */
20
+ const RESTORE_APP_STORAGE_INIT = [0xe0, 0x6c, 0x00, 0x00] as const;
21
+
22
+ /**
23
+ * 0x9000: Success.
24
+ * 0x5123: Application not found.
25
+ * 0x662F: Invalid device state, recovery mode.
26
+ * 0x5501: Invalid consent, user rejected.
27
+ * 0x5502: Invalid consent, pin is not set.
28
+ * 0x670A: Invalid application name length, two chars minimum.
29
+ * 0x6733: Invalid BACKUP_LEN value.
30
+ */
31
+ const RESPONSE_STATUS_SET: number[] = [
32
+ StatusCodes.OK,
33
+ StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT,
34
+ StatusCodes.DEVICE_IN_RECOVERY_MODE,
35
+ StatusCodes.USER_REFUSED_ON_DEVICE,
36
+ StatusCodes.PIN_NOT_SET,
37
+ StatusCodes.INVALID_APP_NAME_LENGTH,
38
+ StatusCodes.INVALID_BACKUP_LENGTH,
39
+ ];
40
+
41
+ /**
42
+ * Restores the application storage initialization.
43
+ *
44
+ * @param transport - The transport object used for communication with the device.
45
+ * @param appName - The name of the application to restore the storage for.
46
+ * @param backupSize - The size of the backup to restore.
47
+ * @returns A promise that resolves to void.
48
+ */
49
+ export async function restoreAppStorageInit(
50
+ transport: Transport,
51
+ appName: string,
52
+ backupSize: number,
53
+ ): Promise<void> {
54
+ const tracer = new LocalTracer("hw", {
55
+ transport: transport.getTraceContext(),
56
+ function: "restoreAppStorageInit",
57
+ });
58
+ tracer.trace("Start");
59
+
60
+ const params: Buffer = Buffer.concat([
61
+ Buffer.from([appName.length + 4]), // LC
62
+ Buffer.from(backupSize.toString(16).padStart(8, "0"), "hex"), // BACKUP_LEN
63
+ Buffer.from(appName, "ascii"), // APP_NAME
64
+ ]);
65
+ const apdu: Readonly<APDU> = [...RESTORE_APP_STORAGE_INIT, params];
66
+
67
+ const response = await transport.send(...apdu, RESPONSE_STATUS_SET);
68
+
69
+ parseResponse(response);
70
+ }
71
+
72
+ export function parseResponse(data: Buffer): void {
73
+ const tracer = new LocalTracer("hw", {
74
+ function: "parseResponse@restoreAppStorageInit",
75
+ });
76
+ const status = data.readUInt16BE(data.length - 2);
77
+ tracer.trace("Result status from 0xe06c0000", { status });
78
+
79
+ switch (status) {
80
+ case StatusCodes.OK:
81
+ return;
82
+ case StatusCodes.APP_NOT_FOUND_OR_INVALID_CONTEXT:
83
+ throw new AppNotFound("Application not found.");
84
+ case StatusCodes.DEVICE_IN_RECOVERY_MODE:
85
+ case StatusCodes.USER_REFUSED_ON_DEVICE:
86
+ break;
87
+ case StatusCodes.PIN_NOT_SET:
88
+ throw new PinNotSet("Invalid consent, PIN is not set.");
89
+ case StatusCodes.INVALID_APP_NAME_LENGTH:
90
+ throw new InvalidAppNameLength("Invalid application name length, two chars minimum.");
91
+ case StatusCodes.INVALID_BACKUP_LENGTH:
92
+ throw new InvalidBackupLength("Invalid backup length.");
93
+ }
94
+
95
+ throw new TransportStatusError(status);
96
+ }