@ledgerhq/device-core 0.2.1 → 0.2.2-nightly.1

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 (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  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/errors.d.ts +40 -0
  48. package/lib/errors.d.ts.map +1 -0
  49. package/lib/errors.js +30 -0
  50. package/lib/errors.js.map +1 -0
  51. package/lib/index.d.ts +6 -0
  52. package/lib/index.d.ts.map +1 -1
  53. package/lib/index.js +11 -1
  54. package/lib/index.js.map +1 -1
  55. package/lib-es/commands/entities/AppStorageInfo.d.ts +8 -0
  56. package/lib-es/commands/entities/AppStorageInfo.d.ts.map +1 -0
  57. package/lib-es/commands/entities/AppStorageInfo.js +2 -0
  58. package/lib-es/commands/entities/AppStorageInfo.js.map +1 -0
  59. package/lib-es/commands/use-cases/app-backup/backupAppStorage.d.ts +18 -0
  60. package/lib-es/commands/use-cases/app-backup/backupAppStorage.d.ts.map +1 -0
  61. package/lib-es/commands/use-cases/app-backup/backupAppStorage.js +93 -0
  62. package/lib-es/commands/use-cases/app-backup/backupAppStorage.js.map +1 -0
  63. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.d.ts +2 -0
  64. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.d.ts.map +1 -0
  65. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.js +54 -0
  66. package/lib-es/commands/use-cases/app-backup/backupAppStorage.test.js.map +1 -0
  67. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.d.ts +20 -0
  68. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.d.ts.map +1 -0
  69. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.js +99 -0
  70. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.js.map +1 -0
  71. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts +2 -0
  72. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.d.ts.map +1 -0
  73. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.js +57 -0
  74. package/lib-es/commands/use-cases/app-backup/getAppStorageInfo.test.js.map +1 -0
  75. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.d.ts +12 -0
  76. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.d.ts.map +1 -0
  77. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.js +88 -0
  78. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.js.map +1 -0
  79. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.d.ts +2 -0
  80. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.d.ts.map +1 -0
  81. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.js +52 -0
  82. package/lib-es/commands/use-cases/app-backup/restoreAppStorage.test.js.map +1 -0
  83. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts +11 -0
  84. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.d.ts.map +1 -0
  85. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.js +78 -0
  86. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.js.map +1 -0
  87. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts +2 -0
  88. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.d.ts.map +1 -0
  89. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.js +47 -0
  90. package/lib-es/commands/use-cases/app-backup/restoreAppStorageCommit.test.js.map +1 -0
  91. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.d.ts +13 -0
  92. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.d.ts.map +1 -0
  93. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.js +94 -0
  94. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.js.map +1 -0
  95. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts +2 -0
  96. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.d.ts.map +1 -0
  97. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.js +50 -0
  98. package/lib-es/commands/use-cases/app-backup/restoreAppStorageInit.test.js.map +1 -0
  99. package/lib-es/errors.d.ts +40 -0
  100. package/lib-es/errors.d.ts.map +1 -0
  101. package/lib-es/errors.js +27 -0
  102. package/lib-es/errors.js.map +1 -0
  103. package/lib-es/index.d.ts +6 -0
  104. package/lib-es/index.d.ts.map +1 -1
  105. package/lib-es/index.js +5 -0
  106. package/lib-es/index.js.map +1 -1
  107. package/package.json +6 -6
  108. package/src/commands/entities/AppStorageInfo.ts +7 -0
  109. package/src/commands/use-cases/app-backup/backupAppStorage.test.ts +52 -0
  110. package/src/commands/use-cases/app-backup/backupAppStorage.ts +97 -0
  111. package/src/commands/use-cases/app-backup/getAppStorageInfo.test.ts +63 -0
  112. package/src/commands/use-cases/app-backup/getAppStorageInfo.ts +102 -0
  113. package/src/commands/use-cases/app-backup/restoreAppStorage.test.ts +57 -0
  114. package/src/commands/use-cases/app-backup/restoreAppStorage.ts +93 -0
  115. package/src/commands/use-cases/app-backup/restoreAppStorageCommit.test.ts +45 -0
  116. package/src/commands/use-cases/app-backup/restoreAppStorageCommit.ts +82 -0
  117. package/src/commands/use-cases/app-backup/restoreAppStorageInit.test.ts +55 -0
  118. package/src/commands/use-cases/app-backup/restoreAppStorageInit.ts +96 -0
  119. package/src/errors.ts +29 -0
  120. package/src/index.ts +6 -0
@@ -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
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { createCustomErrorClass } from "@ledgerhq/errors";
2
+
3
+ // 0x5123
4
+ export const AppNotFound = createCustomErrorClass("AppNotFound");
5
+ export const InvalidContext = createCustomErrorClass("InvalidContext");
6
+ // 0x670a
7
+ export const InvalidAppNameLength = createCustomErrorClass("InvalidAppNameLength");
8
+ // 0x5419
9
+ export const GenerateAesKeyFailed = createCustomErrorClass("GenerateAesKeyFailed");
10
+ // 0x541a
11
+ export const InternalCryptoOperationFailed = createCustomErrorClass(
12
+ "InternalCryptoOperationFailed",
13
+ );
14
+ // 0x541b
15
+ export const InternalComputeAesCmacFailed = createCustomErrorClass("InternalComputeAesCmacFailed");
16
+ // 0x541c
17
+ export const EncryptAppStorageFailed = createCustomErrorClass("EncryptAppStorageFailed");
18
+ // 0x5502
19
+ export const PinNotSet = createCustomErrorClass("PinNotSet");
20
+ // 0x684a
21
+ export const InvalidBackupHeader = createCustomErrorClass("InvalidBackupHeader");
22
+ // 0x6733
23
+ export const InvalidBackupLength = createCustomErrorClass("InvalidBackupLength");
24
+ // 0x6642
25
+ export const InvalidBackupState = createCustomErrorClass("InvalidBackupState");
26
+ // 0x6643
27
+ export const InvalidRestoreState = createCustomErrorClass("InvalidRestoreState");
28
+ // 0x6734
29
+ export const InvalidChunkLength = createCustomErrorClass("InvalidChunkLength");
package/src/index.ts CHANGED
@@ -22,6 +22,12 @@ export { getDeviceName } from "./commands/use-cases/getDeviceName";
22
22
  export { isHardwareVersionSupported } from "./commands/use-cases/isHardwareVersionSupported";
23
23
  export { isBootloaderVersionSupported } from "./commands/use-cases/isBootloaderVersionSupported";
24
24
  export { getVersion } from "./commands/use-cases/getVersion";
25
+ export type { AppStorageInfo } from "./commands/entities/AppStorageInfo";
26
+ export { backupAppStorage } from "./commands/use-cases/app-backup/backupAppStorage";
27
+ export { getAppStorageInfo } from "./commands/use-cases/app-backup/getAppStorageInfo";
28
+ export { restoreAppStorage } from "./commands/use-cases/app-backup/restoreAppStorage";
29
+ export { restoreAppStorageCommit } from "./commands/use-cases/app-backup/restoreAppStorageCommit";
30
+ export { restoreAppStorageInit } from "./commands/use-cases/app-backup/restoreAppStorageInit";
25
31
  // src/capabilities/
26
32
  export {
27
33
  type CLSSupportedDeviceModelId,