@ledgerhq/ledger-key-ring-protocol 0.5.1-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 (180) hide show
  1. package/.eslintrc.js +33 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.unimportedrc.json +16 -0
  4. package/CHANGELOG.md +299 -0
  5. package/LICENSE.txt +21 -0
  6. package/README.md +3 -0
  7. package/jest.config.js +13 -0
  8. package/lib/HWDeviceProvider.d.ts +25 -0
  9. package/lib/HWDeviceProvider.d.ts.map +1 -0
  10. package/lib/HWDeviceProvider.js +88 -0
  11. package/lib/HWDeviceProvider.js.map +1 -0
  12. package/lib/api.d.ts +77 -0
  13. package/lib/api.d.ts.map +1 -0
  14. package/lib/api.js +150 -0
  15. package/lib/api.js.map +1 -0
  16. package/lib/auth.d.ts +3 -0
  17. package/lib/auth.d.ts.map +1 -0
  18. package/lib/auth.js +79 -0
  19. package/lib/auth.js.map +1 -0
  20. package/lib/errors.d.ts +40 -0
  21. package/lib/errors.d.ts.map +1 -0
  22. package/lib/errors.js +18 -0
  23. package/lib/errors.js.map +1 -0
  24. package/lib/index.d.ts +6 -0
  25. package/lib/index.d.ts.map +1 -0
  26. package/lib/index.js +17 -0
  27. package/lib/index.js.map +1 -0
  28. package/lib/mockSdk.d.ts +22 -0
  29. package/lib/mockSdk.d.ts.map +1 -0
  30. package/lib/mockSdk.js +208 -0
  31. package/lib/mockSdk.js.map +1 -0
  32. package/lib/qrcode/cipher.d.ts +12 -0
  33. package/lib/qrcode/cipher.d.ts.map +1 -0
  34. package/lib/qrcode/cipher.js +69 -0
  35. package/lib/qrcode/cipher.js.map +1 -0
  36. package/lib/qrcode/cipher.test.d.ts +2 -0
  37. package/lib/qrcode/cipher.test.d.ts.map +1 -0
  38. package/lib/qrcode/cipher.test.js +40 -0
  39. package/lib/qrcode/cipher.test.js.map +1 -0
  40. package/lib/qrcode/index.d.ts +70 -0
  41. package/lib/qrcode/index.d.ts.map +1 -0
  42. package/lib/qrcode/index.js +312 -0
  43. package/lib/qrcode/index.js.map +1 -0
  44. package/lib/qrcode/index.test.d.ts +2 -0
  45. package/lib/qrcode/index.test.d.ts.map +1 -0
  46. package/lib/qrcode/index.test.js +131 -0
  47. package/lib/qrcode/index.test.js.map +1 -0
  48. package/lib/qrcode/types.d.ts +69 -0
  49. package/lib/qrcode/types.d.ts.map +1 -0
  50. package/lib/qrcode/types.js +3 -0
  51. package/lib/qrcode/types.js.map +1 -0
  52. package/lib/sdk.d.ts +31 -0
  53. package/lib/sdk.d.ts.map +1 -0
  54. package/lib/sdk.js +380 -0
  55. package/lib/sdk.js.map +1 -0
  56. package/lib/store.d.ts +71 -0
  57. package/lib/store.d.ts.map +1 -0
  58. package/lib/store.js +62 -0
  59. package/lib/store.js.map +1 -0
  60. package/lib/types.d.ts +181 -0
  61. package/lib/types.d.ts.map +1 -0
  62. package/lib/types.js +10 -0
  63. package/lib/types.js.map +1 -0
  64. package/lib-es/HWDeviceProvider.d.ts +25 -0
  65. package/lib-es/HWDeviceProvider.d.ts.map +1 -0
  66. package/lib-es/HWDeviceProvider.js +81 -0
  67. package/lib-es/HWDeviceProvider.js.map +1 -0
  68. package/lib-es/api.d.ts +77 -0
  69. package/lib-es/api.d.ts.map +1 -0
  70. package/lib-es/api.js +145 -0
  71. package/lib-es/api.js.map +1 -0
  72. package/lib-es/auth.d.ts +3 -0
  73. package/lib-es/auth.d.ts.map +1 -0
  74. package/lib-es/auth.js +75 -0
  75. package/lib-es/auth.js.map +1 -0
  76. package/lib-es/errors.d.ts +40 -0
  77. package/lib-es/errors.d.ts.map +1 -0
  78. package/lib-es/errors.js +15 -0
  79. package/lib-es/errors.js.map +1 -0
  80. package/lib-es/index.d.ts +6 -0
  81. package/lib-es/index.d.ts.map +1 -0
  82. package/lib-es/index.js +13 -0
  83. package/lib-es/index.js.map +1 -0
  84. package/lib-es/mockSdk.d.ts +22 -0
  85. package/lib-es/mockSdk.d.ts.map +1 -0
  86. package/lib-es/mockSdk.js +201 -0
  87. package/lib-es/mockSdk.js.map +1 -0
  88. package/lib-es/qrcode/cipher.d.ts +12 -0
  89. package/lib-es/qrcode/cipher.d.ts.map +1 -0
  90. package/lib-es/qrcode/cipher.js +61 -0
  91. package/lib-es/qrcode/cipher.js.map +1 -0
  92. package/lib-es/qrcode/cipher.test.d.ts +2 -0
  93. package/lib-es/qrcode/cipher.test.d.ts.map +1 -0
  94. package/lib-es/qrcode/cipher.test.js +38 -0
  95. package/lib-es/qrcode/cipher.test.js.map +1 -0
  96. package/lib-es/qrcode/index.d.ts +70 -0
  97. package/lib-es/qrcode/index.d.ts.map +1 -0
  98. package/lib-es/qrcode/index.js +304 -0
  99. package/lib-es/qrcode/index.js.map +1 -0
  100. package/lib-es/qrcode/index.test.d.ts +2 -0
  101. package/lib-es/qrcode/index.test.d.ts.map +1 -0
  102. package/lib-es/qrcode/index.test.js +126 -0
  103. package/lib-es/qrcode/index.test.js.map +1 -0
  104. package/lib-es/qrcode/types.d.ts +69 -0
  105. package/lib-es/qrcode/types.d.ts.map +1 -0
  106. package/lib-es/qrcode/types.js +2 -0
  107. package/lib-es/qrcode/types.js.map +1 -0
  108. package/lib-es/sdk.d.ts +31 -0
  109. package/lib-es/sdk.d.ts.map +1 -0
  110. package/lib-es/sdk.js +371 -0
  111. package/lib-es/sdk.js.map +1 -0
  112. package/lib-es/store.d.ts +71 -0
  113. package/lib-es/store.d.ts.map +1 -0
  114. package/lib-es/store.js +51 -0
  115. package/lib-es/store.js.map +1 -0
  116. package/lib-es/types.d.ts +181 -0
  117. package/lib-es/types.d.ts.map +1 -0
  118. package/lib-es/types.js +7 -0
  119. package/lib-es/types.js.map +1 -0
  120. package/mocks/scenarios/addSameMemberMultipleTimes.json +426 -0
  121. package/mocks/scenarios/create2trustchainInARow.json +616 -0
  122. package/mocks/scenarios/getOrCreateTransactionCases.json +591 -0
  123. package/mocks/scenarios/member3implicitlyAdded.json +648 -0
  124. package/mocks/scenarios/membersManySelfAdd.json +1427 -0
  125. package/mocks/scenarios/randomMemberTryToDestroy.json +371 -0
  126. package/mocks/scenarios/removeMemberWithTheWrongSeed.json +510 -0
  127. package/mocks/scenarios/removedMemberEjectedOnDeletedTrustchain.json +481 -0
  128. package/mocks/scenarios/removedMemberEjectedOnGetMembers.json +648 -0
  129. package/mocks/scenarios/removedMemberEjectedOnRestore.json +648 -0
  130. package/mocks/scenarios/removingAMemberCreatesAnInteraction.json +593 -0
  131. package/mocks/scenarios/removingYourselfIsForbidden.json +397 -0
  132. package/mocks/scenarios/success.json +978 -0
  133. package/mocks/scenarios/tokenExpires.json +371 -0
  134. package/mocks/scenarios/twoAddMembersFollowedByDeviceAdd.json +705 -0
  135. package/mocks/scenarios/userRefusesAuth.json +40 -0
  136. package/mocks/scenarios/userRefusesRemoveMember.json +542 -0
  137. package/package.json +91 -0
  138. package/scripts/README.md +15 -0
  139. package/scripts/e2e.ts +57 -0
  140. package/src/HWDeviceProvider.ts +105 -0
  141. package/src/__tests__/integration/mock.sdk.test.ts +47 -0
  142. package/src/__tests__/integration/sdk.test.ts +20 -0
  143. package/src/__tests__/tsconfig.json +8 -0
  144. package/src/__tests__/unit/sdk.test.ts +236 -0
  145. package/src/api.ts +202 -0
  146. package/src/auth.ts +81 -0
  147. package/src/errors.ts +18 -0
  148. package/src/index.ts +20 -0
  149. package/src/mockSdk.ts +253 -0
  150. package/src/qrcode/cipher.test.ts +30 -0
  151. package/src/qrcode/cipher.ts +63 -0
  152. package/src/qrcode/index.test.ts +138 -0
  153. package/src/qrcode/index.ts +395 -0
  154. package/src/qrcode/types.ts +70 -0
  155. package/src/sdk.ts +542 -0
  156. package/src/store.ts +99 -0
  157. package/src/types.ts +242 -0
  158. package/tests/scenarios/_template.ts +18 -0
  159. package/tests/scenarios/addSameMemberMultipleTimes.ts +20 -0
  160. package/tests/scenarios/create2trustchainInARow.ts +14 -0
  161. package/tests/scenarios/getOrCreateTransactionCases.ts +74 -0
  162. package/tests/scenarios/member3implicitlyAdded.ts +51 -0
  163. package/tests/scenarios/membersManySelfAdd.ts +18 -0
  164. package/tests/scenarios/randomMemberTryToDestroy.ts +23 -0
  165. package/tests/scenarios/removeMemberWithTheWrongSeed.ts +28 -0
  166. package/tests/scenarios/removedMemberEjectedOnDeletedTrustchain.ts +31 -0
  167. package/tests/scenarios/removedMemberEjectedOnGetMembers.ts +29 -0
  168. package/tests/scenarios/removedMemberEjectedOnRestore.ts +31 -0
  169. package/tests/scenarios/removingAMemberCreatesAnInteraction.ts +42 -0
  170. package/tests/scenarios/removingYourselfIsForbidden.ts +11 -0
  171. package/tests/scenarios/success.ts +94 -0
  172. package/tests/scenarios/tokenExpires.ts +20 -0
  173. package/tests/scenarios/twoAddMembersFollowedByDeviceAdd.ts +49 -0
  174. package/tests/scenarios/userRefusesAuth.ts +28 -0
  175. package/tests/scenarios/userRefusesRemoveMember.ts +66 -0
  176. package/tests/test-helpers/recordTrustchainSdkTests.ts +178 -0
  177. package/tests/test-helpers/replayTrustchainSdkTests.ts +141 -0
  178. package/tests/test-helpers/types.ts +45 -0
  179. package/tests/tsconfig.json +8 -0
  180. package/tsconfig.json +15 -0
package/src/api.ts ADDED
@@ -0,0 +1,202 @@
1
+ import network from "@ledgerhq/live-network";
2
+ import { JWT } from "./types";
3
+
4
+ export type StatusAPIResponse = {
5
+ name: string;
6
+ version: string;
7
+ };
8
+
9
+ export type APIJWT = {
10
+ access_token: string;
11
+ permissions: {
12
+ [trustchainId: string]: {
13
+ [path: string]: string[];
14
+ };
15
+ };
16
+ };
17
+
18
+ export type Challenge = {
19
+ version: number;
20
+ challenge: {
21
+ data: string;
22
+ expiry: string;
23
+ };
24
+ host: string;
25
+ rp: {
26
+ credential: {
27
+ version: number;
28
+ curveId: number;
29
+ signAlgorithm: number;
30
+ publicKey: string;
31
+ };
32
+ signature: string;
33
+ }[];
34
+ protocolVersion: {
35
+ major: number;
36
+ minor: number;
37
+ patch: number;
38
+ };
39
+ };
40
+
41
+ export type ChallengeSignature = {
42
+ credential: {
43
+ version: number;
44
+ curveId: number;
45
+ signAlgorithm: number;
46
+ publicKey: string;
47
+ };
48
+ signature: string;
49
+ attestation: string;
50
+ };
51
+
52
+ export type TrustchainsResponse = {
53
+ [trustchainId: string]: {
54
+ [path: string]: string[]; // list of permissions
55
+ };
56
+ };
57
+
58
+ export type TrustchainResponse = {
59
+ [key: string]: string;
60
+ };
61
+
62
+ export type PutCommandsRequest = {
63
+ path: string;
64
+ blocks: string[];
65
+ };
66
+
67
+ const getApi = (apiBaseURL: string) => {
68
+ async function getAuthenticationChallenge(): Promise<{ json: Challenge; tlv: string }> {
69
+ const { data } = await network<{ json: Challenge; tlv: string }>({
70
+ url: `${apiBaseURL}/v1/challenge`,
71
+ method: "GET",
72
+ });
73
+ return data;
74
+ }
75
+
76
+ async function postChallengeResponse(request: {
77
+ challenge: Challenge;
78
+ signature: ChallengeSignature;
79
+ }): Promise<JWT> {
80
+ const { data } = await network<APIJWT>({
81
+ url: `${apiBaseURL}/v1/authenticate`,
82
+ method: "POST",
83
+ data: request,
84
+ });
85
+ return {
86
+ accessToken: data.access_token,
87
+ permissions: data.permissions,
88
+ };
89
+ }
90
+
91
+ async function refreshAuth(jwt: JWT): Promise<JWT> {
92
+ const { data } = await network<APIJWT>({
93
+ url: `${apiBaseURL}/v1/refresh`,
94
+ method: "GET",
95
+ headers: {
96
+ Authorization: `Bearer ${jwt.accessToken}`,
97
+ },
98
+ });
99
+ return {
100
+ accessToken: data.access_token,
101
+ permissions: data.permissions,
102
+ };
103
+ }
104
+
105
+ async function getTrustchains(jwt: JWT): Promise<TrustchainsResponse> {
106
+ const { data } = await network<TrustchainsResponse>({
107
+ url: `${apiBaseURL}/v1/trustchains`,
108
+ method: "GET",
109
+ headers: {
110
+ Authorization: `Bearer ${jwt.accessToken}`,
111
+ },
112
+ });
113
+ return data;
114
+ }
115
+
116
+ async function getTrustchain(jwt: JWT, trustchain_id: string): Promise<TrustchainResponse> {
117
+ const { data } = await network<TrustchainResponse>({
118
+ url: `${apiBaseURL}/v1/trustchain/${trustchain_id}`,
119
+ method: "GET",
120
+ headers: {
121
+ Authorization: `Bearer ${jwt.accessToken}`,
122
+ },
123
+ });
124
+ return data;
125
+ }
126
+
127
+ async function postDerivation(
128
+ jwt: JWT,
129
+ trustchain_id: string,
130
+ commandStream: string,
131
+ ): Promise<void> {
132
+ await network<void>({
133
+ url: `${apiBaseURL}/v1/trustchain/${trustchain_id}/derivation`,
134
+ method: "POST",
135
+ headers: {
136
+ Authorization: `Bearer ${jwt.accessToken}`,
137
+ "Content-Type": "application/json",
138
+ },
139
+ data: commandStream,
140
+ });
141
+ }
142
+
143
+ async function postSeed(jwt: JWT, commandStream: string): Promise<void> {
144
+ await network<void>({
145
+ url: `${apiBaseURL}/v1/seed`,
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ Authorization: `Bearer ${jwt.accessToken}`,
150
+ },
151
+ data: commandStream,
152
+ });
153
+ }
154
+
155
+ async function putCommands(
156
+ jwt: JWT,
157
+ trustchain_id: string,
158
+ request: PutCommandsRequest,
159
+ ): Promise<void> {
160
+ await network<void>({
161
+ url: `${apiBaseURL}/v1/trustchain/${trustchain_id}/commands`,
162
+ method: "PUT",
163
+ headers: {
164
+ Authorization: `Bearer ${jwt.accessToken}`,
165
+ },
166
+ data: request,
167
+ });
168
+ }
169
+
170
+ async function deleteTrustchain(jwt: JWT, trustchain_id: string): Promise<void> {
171
+ await network<void>({
172
+ url: `${apiBaseURL}/v1/trustchain/${trustchain_id}`,
173
+ method: "DELETE",
174
+ headers: {
175
+ Authorization: `Bearer ${jwt.accessToken}`,
176
+ },
177
+ });
178
+ }
179
+
180
+ async function fetchStatus(): Promise<StatusAPIResponse> {
181
+ const { data } = await network<StatusAPIResponse>({
182
+ url: `${apiBaseURL}/_info`,
183
+ method: "GET",
184
+ });
185
+ return data;
186
+ }
187
+
188
+ return {
189
+ getAuthenticationChallenge,
190
+ postChallengeResponse,
191
+ refreshAuth,
192
+ getTrustchains,
193
+ getTrustchain,
194
+ postDerivation,
195
+ postSeed,
196
+ putCommands,
197
+ deleteTrustchain,
198
+ fetchStatus,
199
+ };
200
+ };
201
+
202
+ export default getApi;
package/src/auth.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { LedgerAPI4xx } from "@ledgerhq/errors";
2
+ import { log } from "@ledgerhq/logs";
3
+ import { AuthCachePolicy, JWT } from "./types";
4
+ import { TrustchainNotAllowed, TrustchainOutdated } from "./errors";
5
+
6
+ export async function genericWithJWT<T>(
7
+ job: (jwt: JWT) => Promise<T>,
8
+ initialJWT: JWT | undefined,
9
+ auth: () => Promise<JWT>,
10
+ refreshAuth: (jw: JWT) => Promise<JWT>,
11
+ policy: AuthCachePolicy = "cache",
12
+ ): Promise<T> {
13
+ function refresh(jwt: JWT) {
14
+ return refreshAuth(jwt).catch(e => {
15
+ log("trustchain", "JWT refresh failed, reauthenticating", e);
16
+ const { hasExpired, isNotPermitted, isTrustchainOutdated } = networkCheckJwtExpiration(e);
17
+ if (isNotPermitted) {
18
+ throw new TrustchainNotAllowed();
19
+ }
20
+ if (isTrustchainOutdated) {
21
+ throw new TrustchainOutdated();
22
+ }
23
+ if (hasExpired) {
24
+ return auth();
25
+ }
26
+ throw e;
27
+ });
28
+ }
29
+
30
+ // initial jwt depending on the policy
31
+ let jwt =
32
+ policy === "no-cache" || !initialJWT
33
+ ? await auth()
34
+ : policy === "refresh"
35
+ ? await refresh(initialJWT)
36
+ : initialJWT;
37
+
38
+ return job(jwt).catch(async e => {
39
+ // JWT expiration handling: if the function fails, we will recover a valid jwt accordingly to spec. https://ledgerhq.atlassian.net/wiki/spaces/BE/pages/4207083687/TCH+Usage+documentation#JWT-expiration-handling
40
+ const { hasExpired, canBeRefreshed, isNotPermitted, isTrustchainOutdated } =
41
+ networkCheckJwtExpiration(e);
42
+ if (isNotPermitted) {
43
+ throw new TrustchainNotAllowed();
44
+ }
45
+ if (isTrustchainOutdated) {
46
+ throw new TrustchainOutdated();
47
+ }
48
+ if (hasExpired) {
49
+ log("trustchain", "JWT expired -> " + (canBeRefreshed ? "refreshing" : "reauthenticating"));
50
+ jwt = await (jwt && canBeRefreshed ? refresh(jwt) : auth());
51
+ return job(jwt);
52
+ }
53
+ throw e;
54
+ });
55
+ }
56
+
57
+ type JwtExpirationCheck = {
58
+ hasExpired: boolean;
59
+ canBeRefreshed: boolean;
60
+ isNotPermitted: boolean;
61
+ isTrustchainOutdated: boolean;
62
+ };
63
+
64
+ function networkCheckJwtExpiration(error: unknown): JwtExpirationCheck {
65
+ let hasExpired = false;
66
+ let canBeRefreshed = false;
67
+ let isNotPermitted = false;
68
+ let isTrustchainOutdated = false;
69
+ // this assume live-network is used and we adapt to its error's format
70
+ if (error instanceof LedgerAPI4xx) {
71
+ if (error.message.includes("JWT is expired")) {
72
+ hasExpired = true;
73
+ canBeRefreshed = error.message.includes("/refresh");
74
+ } else if (error.message.includes("JWT contains no permission")) {
75
+ isNotPermitted = true;
76
+ } else if (error.message.includes("path does not match")) {
77
+ isTrustchainOutdated = true;
78
+ }
79
+ }
80
+ return { hasExpired, canBeRefreshed, isNotPermitted, isTrustchainOutdated };
81
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { createCustomErrorClass } from "@ledgerhq/errors";
2
+
3
+ export const ScannedOldImportQrCode = createCustomErrorClass("ScannedOldImportQrCode");
4
+ export const ScannedNewImportQrCode = createCustomErrorClass("ScannedNewImportQrCode");
5
+ export const ScannedInvalidQrCode = createCustomErrorClass("ScannedInvalidQrCode");
6
+ export const InvalidDigitsError = createCustomErrorClass("InvalidDigitsError");
7
+ export const InvalidEncryptionKeyError = createCustomErrorClass("InvalidEncryptionKeyError");
8
+ export const TrustchainEjected = createCustomErrorClass("TrustchainEjected");
9
+ export const TrustchainNotAllowed = createCustomErrorClass("TrustchainNotAllowed");
10
+ export const TrustchainOutdated = createCustomErrorClass("TrustchainOutdated");
11
+ export const TrustchainNotFound = createCustomErrorClass("TrustchainNotFound");
12
+ export const NoTrustchainInitialized = createCustomErrorClass("NoTrustchainInitialized");
13
+ export const TrustchainAlreadyInitialized = createCustomErrorClass("TrustchainAlreadyInitialized");
14
+ export const TrustchainAlreadyInitializedWithOtherSeed = createCustomErrorClass(
15
+ "TrustchainAlreadyInitializedWithOtherSeed",
16
+ );
17
+
18
+ export const QRCodeWSClosed = createCustomErrorClass<{ time: number }>("QRCodeWSClosed");
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { HWDeviceProvider } from "./HWDeviceProvider";
2
+ import { SDK } from "./sdk";
3
+ import { MockSDK } from "./mockSdk";
4
+ import { TrustchainSDKContext, TrustchainSDK, TrustchainLifecycle, WithDevice } from "./types";
5
+
6
+ /**
7
+ * Get an implementation of a TrustchainSDK
8
+ */
9
+ export const getSdk = (
10
+ isMockEnv: boolean,
11
+ context: TrustchainSDKContext,
12
+ withDevice: WithDevice,
13
+ lifecycle?: TrustchainLifecycle,
14
+ ): TrustchainSDK => {
15
+ if (isMockEnv) {
16
+ return new MockSDK(context, lifecycle);
17
+ }
18
+
19
+ return new SDK(context, new HWDeviceProvider(context.apiBaseUrl, withDevice), lifecycle);
20
+ };
package/src/mockSdk.ts ADDED
@@ -0,0 +1,253 @@
1
+ import {
2
+ JWT,
3
+ MemberCredentials,
4
+ Trustchain,
5
+ TrustchainDeviceCallbacks,
6
+ TrustchainLifecycle,
7
+ TrustchainMember,
8
+ TrustchainResult,
9
+ TrustchainResultType,
10
+ TrustchainSDK,
11
+ TrustchainSDKContext,
12
+ } from "./types";
13
+ import { Permissions } from "@ledgerhq/hw-ledger-key-ring-protocol";
14
+ import { TrustchainEjected } from "./errors";
15
+ import getApi from "./api";
16
+
17
+ const mockedLiveCredentialsPrivateKey = "mock-private-key";
18
+
19
+ function assertTrustchain(trustchain: Trustchain) {
20
+ if (!trustchain.rootId.startsWith("mock-root-id")) {
21
+ throw new Error("in mock context, trustchain must be the mocked trustchain");
22
+ }
23
+ }
24
+
25
+ function assertLiveCredentials(memberCredentials: MemberCredentials) {
26
+ if (!memberCredentials.privatekey.startsWith(mockedLiveCredentialsPrivateKey)) {
27
+ throw new Error("in mock context, memberCredentials must be the mocked memberCredentials");
28
+ }
29
+ }
30
+
31
+ function assertAllowedPermissions(trustchainId: string, memberId: string) {
32
+ const members = trustchainMembers.get(trustchainId) || [];
33
+ const member = members.find(m => m.id === memberId);
34
+ if (!member) {
35
+ throw new TrustchainEjected();
36
+ }
37
+ }
38
+
39
+ const mockedLiveJWT = {
40
+ accessToken: "mock-live-jwt",
41
+ permissions: {},
42
+ };
43
+
44
+ // global states in memory
45
+ const trustchains = new Map<string, Trustchain>();
46
+ const trustchainMembers = new Map<string, TrustchainMember[]>();
47
+
48
+ /**
49
+ * to mock the encryption/decryption, we just xor the data with 0xff
50
+ */
51
+ const applyXor = (a: Uint8Array) => {
52
+ const b = new Uint8Array(a.length);
53
+ for (let i = 0; i < a.length; i++) {
54
+ b[i] = a[i] ^ 0xff;
55
+ }
56
+ return b;
57
+ };
58
+
59
+ export class MockSDK implements TrustchainSDK {
60
+ private context: TrustchainSDKContext;
61
+ private lifecyle?: TrustchainLifecycle;
62
+ private api: ReturnType<typeof getApi>;
63
+
64
+ constructor(context: TrustchainSDKContext, lifecyle?: TrustchainLifecycle) {
65
+ this.context = context;
66
+ this.lifecyle = lifecyle;
67
+ this.api = getApi(context.apiBaseUrl);
68
+ }
69
+
70
+ private deviceJwtAcquired = false;
71
+
72
+ private _id = 1;
73
+ initMemberCredentials(): Promise<MemberCredentials> {
74
+ const id = this._id++;
75
+ return Promise.resolve({
76
+ privatekey: "mock-private-key-" + this.context.name + "-" + id,
77
+ pubkey: "mock-pub-key-" + this.context.name + "-" + id,
78
+ });
79
+ }
80
+
81
+ withAuth<T>(
82
+ trustchain: Trustchain,
83
+ memberCredentials: MemberCredentials,
84
+ job: (jwt: JWT) => Promise<T>,
85
+ ): Promise<T> {
86
+ assertTrustchain(trustchain);
87
+ assertLiveCredentials(memberCredentials);
88
+ return job(mockedLiveJWT);
89
+ }
90
+
91
+ async getOrCreateTrustchain(
92
+ deviceId: string,
93
+ memberCredentials: MemberCredentials,
94
+ callbacks?: TrustchainDeviceCallbacks,
95
+ ): Promise<TrustchainResult> {
96
+ this.invalidateJwt();
97
+ assertLiveCredentials(memberCredentials);
98
+ let type = trustchains.has("mock-root-id")
99
+ ? TrustchainResultType.restored
100
+ : TrustchainResultType.created;
101
+
102
+ const trustchain: Trustchain = trustchains.get("mock-root-id") || {
103
+ rootId: "mock-root-id",
104
+ walletSyncEncryptionKey: "mock-wallet-sync-encryption-key",
105
+ applicationPath: "m/0'/16'/0'",
106
+ };
107
+ trustchains.set(trustchain.rootId, trustchain);
108
+
109
+ if (!this.deviceJwtAcquired) {
110
+ callbacks?.onStartRequestUserInteraction?.();
111
+ this.deviceJwtAcquired = true; // simulate device auth interaction
112
+ callbacks?.onEndRequestUserInteraction?.();
113
+ }
114
+
115
+ const currentMembers = trustchainMembers.get(trustchain.rootId) || [];
116
+ // add itself if not yet here
117
+ if (!currentMembers.some(m => m.id === memberCredentials.pubkey)) {
118
+ if (type === TrustchainResultType.restored) type = TrustchainResultType.updated;
119
+ callbacks?.onStartRequestUserInteraction?.();
120
+ // simulate device add interaction
121
+ callbacks?.onEndRequestUserInteraction?.();
122
+ currentMembers.push({
123
+ id: memberCredentials.pubkey,
124
+ name: this.context.name,
125
+ permissions: Permissions.OWNER,
126
+ });
127
+ trustchainMembers.set(trustchain.rootId, currentMembers);
128
+ }
129
+ return Promise.resolve({ type, trustchain });
130
+ }
131
+
132
+ async refreshAuth(jwt: JWT): Promise<JWT> {
133
+ return jwt;
134
+ }
135
+
136
+ async restoreTrustchain(
137
+ trustchain: Trustchain,
138
+ memberCredentials: MemberCredentials,
139
+ ): Promise<Trustchain> {
140
+ assertTrustchain(trustchain);
141
+ assertLiveCredentials(memberCredentials);
142
+ assertAllowedPermissions(trustchain.rootId, memberCredentials.pubkey);
143
+ const latest = trustchains.get(trustchain.rootId);
144
+ if (!latest) {
145
+ throw new Error("trustchain not found");
146
+ }
147
+ return Promise.resolve(latest);
148
+ }
149
+
150
+ async getMembers(
151
+ trustchain: Trustchain,
152
+ memberCredentials: MemberCredentials,
153
+ ): Promise<TrustchainMember[]> {
154
+ assertTrustchain(trustchain);
155
+ assertLiveCredentials(memberCredentials);
156
+ assertAllowedPermissions(trustchain.rootId, memberCredentials.pubkey);
157
+ const currentMembers = trustchainMembers.get(trustchain.rootId) || [];
158
+ return Promise.resolve([...currentMembers]);
159
+ }
160
+
161
+ async removeMember(
162
+ deviceId: string,
163
+ trustchain: Trustchain,
164
+ memberCredentials: MemberCredentials,
165
+ member: TrustchainMember,
166
+ callbacks?: TrustchainDeviceCallbacks,
167
+ ): Promise<Trustchain> {
168
+ this.invalidateJwt();
169
+ assertTrustchain(trustchain);
170
+ assertLiveCredentials(memberCredentials);
171
+ assertAllowedPermissions(trustchain.rootId, memberCredentials.pubkey);
172
+ if (member.id === memberCredentials.pubkey) {
173
+ throw new Error("cannot remove self");
174
+ }
175
+ const afterRotation = await this.lifecyle?.onTrustchainRotation(
176
+ this,
177
+ trustchain,
178
+ memberCredentials,
179
+ );
180
+
181
+ if (!this.deviceJwtAcquired) {
182
+ callbacks?.onStartRequestUserInteraction?.();
183
+ this.deviceJwtAcquired = true; // simulate device auth interaction
184
+ callbacks?.onEndRequestUserInteraction?.();
185
+ }
186
+
187
+ callbacks?.onStartRequestUserInteraction?.();
188
+ // simulate device interaction
189
+ callbacks?.onEndRequestUserInteraction?.();
190
+
191
+ callbacks?.onStartRequestUserInteraction?.();
192
+ // simulate device interaction
193
+ callbacks?.onEndRequestUserInteraction?.();
194
+
195
+ const currentMembers = (trustchainMembers.get(trustchain.rootId) || []).filter(
196
+ m => m.id !== member.id,
197
+ );
198
+ trustchainMembers.set(trustchain.rootId, currentMembers);
199
+ // we extract the index part to increment it and recreate a path
200
+ const index = 1 + parseInt(trustchain.applicationPath.split("/")[3].split("'")[0]);
201
+ const newTrustchain = {
202
+ rootId: trustchain.rootId,
203
+ walletSyncEncryptionKey: "mock-wallet-sync-encryption-key-" + index,
204
+ applicationPath: "m/0'/16'/" + index + "'",
205
+ };
206
+ trustchains.set(newTrustchain.rootId, newTrustchain);
207
+
208
+ if (afterRotation) await afterRotation(newTrustchain);
209
+
210
+ return Promise.resolve(newTrustchain);
211
+ }
212
+
213
+ async destroyTrustchain(
214
+ trustchain: Trustchain,
215
+ memberCredentials: MemberCredentials,
216
+ ): Promise<void> {
217
+ assertTrustchain(trustchain);
218
+ assertLiveCredentials(memberCredentials);
219
+ trustchains.delete(trustchain.rootId);
220
+ trustchainMembers.delete(trustchain.rootId);
221
+ return;
222
+ }
223
+
224
+ addMember(
225
+ trustchain: Trustchain,
226
+ memberCredentials: MemberCredentials,
227
+ member: TrustchainMember,
228
+ ): Promise<void> {
229
+ assertTrustchain(trustchain);
230
+ assertLiveCredentials(memberCredentials);
231
+ const currentMembers = trustchainMembers.get(trustchain.rootId) || [];
232
+ if (currentMembers.find(m => m.id === member.id)) {
233
+ return Promise.resolve();
234
+ }
235
+ currentMembers.push(member);
236
+ trustchainMembers.set(trustchain.rootId, currentMembers);
237
+ return Promise.resolve();
238
+ }
239
+
240
+ encryptUserData(trustchain: Trustchain, input: Uint8Array): Promise<Uint8Array> {
241
+ assertTrustchain(trustchain);
242
+ return Promise.resolve(applyXor(input));
243
+ }
244
+
245
+ decryptUserData(trustchain: Trustchain, data: Uint8Array): Promise<Uint8Array> {
246
+ assertTrustchain(trustchain);
247
+ return Promise.resolve(applyXor(data));
248
+ }
249
+
250
+ invalidateJwt(): void {
251
+ this.deviceJwtAcquired = false;
252
+ }
253
+ }
@@ -0,0 +1,30 @@
1
+ import { crypto } from "@ledgerhq/hw-ledger-key-ring-protocol";
2
+ import { makeCipher } from "./cipher";
3
+ import { InvalidEncryptionKeyError } from "../errors";
4
+
5
+ describe("makeCipher", () => {
6
+ it("should encrypt and decrypt correctly", async () => {
7
+ const ephemeralKey = await crypto.randomKeypair();
8
+ const candidate = await crypto.randomKeypair();
9
+ const sessionEncryptionKey = await crypto.ecdh(ephemeralKey, candidate.publicKey);
10
+ const cipher = makeCipher(sessionEncryptionKey);
11
+ const plaintext = { message: "Hello, World!" };
12
+ const encrypted = await cipher.encrypt(plaintext);
13
+ expect(typeof encrypted).toBe("string");
14
+ const decrypted = await cipher.decrypt(encrypted);
15
+ expect(decrypted).toEqual(plaintext);
16
+ });
17
+
18
+ it("should throw InvalidEncryptionKeyError if key changes", async () => {
19
+ const ephemeralKey = await crypto.randomKeypair();
20
+ const candidate = await crypto.randomKeypair();
21
+ const sessionEncryptionKey = await crypto.ecdh(ephemeralKey, candidate.publicKey);
22
+ const cipher = makeCipher(sessionEncryptionKey);
23
+ const plaintext = { message: "Hello, World!" };
24
+ const encrypted = await cipher.encrypt(plaintext);
25
+ const ephemeralKey2 = await crypto.randomKeypair();
26
+ const otherSessionEncryptionKey = await crypto.ecdh(ephemeralKey2, candidate.publicKey);
27
+ const cipher2 = makeCipher(otherSessionEncryptionKey);
28
+ expect(cipher2.decrypt(encrypted)).rejects.toThrow(InvalidEncryptionKeyError);
29
+ });
30
+ });
@@ -0,0 +1,63 @@
1
+ import Base64 from "base64-js";
2
+ import { crypto } from "@ledgerhq/hw-ledger-key-ring-protocol";
3
+ import { DecryptedPayload, Encrypted, ExtractEncryptedPayloads, Message } from "./types";
4
+ import { InvalidEncryptionKeyError } from "../errors";
5
+
6
+ export type Cipher = {
7
+ encrypt: (obj: object) => Promise<string>;
8
+ decrypt: (data: string) => Promise<object>;
9
+ };
10
+
11
+ export type MessageCipher = {
12
+ encryptMessagePayload: <P extends ExtractEncryptedPayloads<Message>>(
13
+ payload: P,
14
+ ) => Promise<Encrypted<P>>;
15
+ decryptMessage: <M extends Message>(message: M) => Promise<DecryptedPayload<M>>;
16
+ };
17
+
18
+ export function makeCipher(sessionEncryptionKey: Uint8Array): Cipher {
19
+ async function encrypt(obj: object): Promise<string> {
20
+ const plaintext = JSON.stringify(obj);
21
+ const data = new TextEncoder().encode(plaintext);
22
+ const nonce = await crypto.randomBytes(16);
23
+ const ciphertext = await crypto.encrypt(sessionEncryptionKey, nonce, data);
24
+ const blob = new Uint8Array(nonce.length + ciphertext.length);
25
+ blob.set(nonce);
26
+ blob.set(ciphertext, nonce.length);
27
+ return Base64.fromByteArray(blob);
28
+ }
29
+
30
+ async function decrypt(data: string): Promise<object> {
31
+ const blob = Base64.toByteArray(data);
32
+ const nonce = blob.slice(0, 16);
33
+ const ciphertext = blob.slice(16);
34
+ try {
35
+ const plaintext = await crypto.decrypt(sessionEncryptionKey, nonce, ciphertext);
36
+ const text = new TextDecoder().decode(plaintext);
37
+ return JSON.parse(text);
38
+ } catch (e) {
39
+ throw new InvalidEncryptionKeyError("data can't be decrypted");
40
+ }
41
+ }
42
+
43
+ return { encrypt, decrypt };
44
+ }
45
+
46
+ export function makeMessageCipher(cipher: Cipher): MessageCipher {
47
+ async function encryptMessagePayload<P extends ExtractEncryptedPayloads<Message>>(
48
+ payload: P,
49
+ ): Promise<Encrypted<P>> {
50
+ const encrypted = await cipher.encrypt(payload);
51
+ return { encrypted };
52
+ }
53
+
54
+ async function decryptMessage<M extends Message>(message: M): Promise<DecryptedPayload<M>> {
55
+ if (message.message === "InitiateHandshake" || message.message === "Failure") {
56
+ throw new Error(message.message + " is not encrypted");
57
+ }
58
+ const decrypted = (await cipher.decrypt(message.payload.encrypted)) as DecryptedPayload<M>;
59
+ return decrypted;
60
+ }
61
+
62
+ return { encryptMessagePayload, decryptMessage };
63
+ }