@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
@@ -0,0 +1,138 @@
1
+ import { createQRCodeHostInstance, createQRCodeCandidateInstance } from ".";
2
+ import WebSocket from "ws";
3
+ import { convertKeyPairToLiveCredentials } from "../sdk";
4
+ import { crypto } from "@ledgerhq/hw-ledger-key-ring-protocol";
5
+ import { ScannedInvalidQrCode, ScannedOldImportQrCode } from "../errors";
6
+
7
+ describe("Trustchain QR Code", () => {
8
+ let server;
9
+ let a;
10
+ let b;
11
+
12
+ beforeAll(() => {
13
+ server = new WebSocket.Server({ port: 1234 });
14
+ server.on("connection", ws => {
15
+ if (!a) {
16
+ a = ws;
17
+ } else if (!b) {
18
+ b = ws;
19
+ }
20
+ ws.on("message", message => {
21
+ if (ws === a && b) {
22
+ b.send(message);
23
+ } else if (ws === b && a) {
24
+ a.send(message);
25
+ }
26
+ });
27
+ });
28
+ });
29
+
30
+ afterAll(() => {
31
+ server.close();
32
+ });
33
+
34
+ test("digits matching scenario", async () => {
35
+ const onDisplayDigits = jest.fn();
36
+ const trustchain = {
37
+ rootId: "test-root-id",
38
+ walletSyncEncryptionKey: "test-wallet-sync-encryption-key",
39
+ applicationPath: "m/0'/16'/0'",
40
+ };
41
+ const addMember = jest.fn(() => Promise.resolve(trustchain));
42
+ const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
43
+ const memberName = "foo";
44
+
45
+ let scannedUrlResolve: (url: string) => void;
46
+ const scannedUrlPromise = new Promise<string>(resolve => {
47
+ scannedUrlResolve = resolve;
48
+ });
49
+ const onDisplayQRCode = (url: string) => {
50
+ scannedUrlResolve(url);
51
+ };
52
+ const onRequestQRCodeInput = jest.fn((config, callback) =>
53
+ callback(onDisplayDigits.mock.calls[0][0]),
54
+ );
55
+
56
+ const hostP = createQRCodeHostInstance({
57
+ trustchainApiBaseUrl: "ws://localhost:1234",
58
+ onDisplayQRCode,
59
+ onDisplayDigits,
60
+ addMember,
61
+ memberCredentials,
62
+ memberName,
63
+ initialTrustchainId: trustchain.rootId,
64
+ });
65
+
66
+ const scannedUrl = await scannedUrlPromise;
67
+
68
+ const candidateP = createQRCodeCandidateInstance({
69
+ memberCredentials,
70
+ memberName,
71
+ initialTrustchainId: undefined,
72
+ addMember,
73
+ scannedUrl,
74
+ onRequestQRCodeInput,
75
+ });
76
+
77
+ const [_, res] = await Promise.all([hostP, candidateP]);
78
+
79
+ expect(onDisplayDigits).toHaveBeenCalledWith(expect.any(String));
80
+ expect(addMember).toHaveBeenCalled();
81
+ expect(onRequestQRCodeInput).toHaveBeenCalledWith(
82
+ { digits: 3, connected: false },
83
+ expect.any(Function),
84
+ );
85
+ expect(res).toEqual(trustchain);
86
+ });
87
+ test("invalid qr code scanned", async () => {
88
+ const trustchain = {
89
+ rootId: "test-root-id",
90
+ walletSyncEncryptionKey: "test-wallet-sync-encryption-key",
91
+ applicationPath: "m/0'/16'/0'",
92
+ };
93
+ const addMember = jest.fn(() => Promise.resolve(trustchain));
94
+ const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
95
+ const memberName = "foo";
96
+
97
+ const onRequestQRCodeInput = jest.fn();
98
+
99
+ const scannedUrl = "https://example.com";
100
+
101
+ const candidateP = createQRCodeCandidateInstance({
102
+ memberCredentials,
103
+ memberName,
104
+ initialTrustchainId: undefined,
105
+ addMember,
106
+ scannedUrl,
107
+ onRequestQRCodeInput,
108
+ });
109
+
110
+ await expect(candidateP).rejects.toThrow(new ScannedInvalidQrCode());
111
+ });
112
+ test("old accounts export qr code scanned", async () => {
113
+ const trustchain = {
114
+ rootId: "test-root-id",
115
+ walletSyncEncryptionKey: "test-wallet-sync-encryption-key",
116
+ applicationPath: "m/0'/16'/0'",
117
+ };
118
+ const addMember = jest.fn(() => Promise.resolve(trustchain));
119
+ const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
120
+ const memberName = "foo";
121
+
122
+ const onRequestQRCodeInput = jest.fn();
123
+
124
+ const scannedUrl =
125
+ "ZAADAAIAAAAEd2JXMpuoYdzvkNzFTlmQLPcGf2LSjDOgqaB3nQoZqlimcCX6HNkescWKyT1DCGuwO7IesD7oYg+fdZPkiIfFL3V9swfZRePkaNN09IjXsWLsim9hK/qi/RC1/ofX3hYNKUxUAgYVVG82WKXIk47siWfUlRZsCYSAARQ6ASpUgidPjMHaOMK6w53wTZplwo7Zjv1HrIyKwr3Ci8OmrFye5g==";
126
+
127
+ const candidateP = createQRCodeCandidateInstance({
128
+ memberCredentials,
129
+ memberName,
130
+ initialTrustchainId: undefined,
131
+ addMember,
132
+ scannedUrl,
133
+ onRequestQRCodeInput,
134
+ });
135
+
136
+ await expect(candidateP).rejects.toThrow(new ScannedOldImportQrCode());
137
+ });
138
+ });
@@ -0,0 +1,395 @@
1
+ import { Permissions, crypto } from "@ledgerhq/hw-ledger-key-ring-protocol";
2
+ import WebSocket from "isomorphic-ws";
3
+ import { MemberCredentials, Trustchain, TrustchainMember } from "../types";
4
+ import { makeCipher, makeMessageCipher } from "./cipher";
5
+ import { Message } from "./types";
6
+ import {
7
+ InvalidDigitsError,
8
+ NoTrustchainInitialized,
9
+ QRCodeWSClosed,
10
+ ScannedInvalidQrCode,
11
+ ScannedOldImportQrCode,
12
+ TrustchainAlreadyInitialized,
13
+ } from "../errors";
14
+ import { log } from "@ledgerhq/logs";
15
+
16
+ const version = 1;
17
+
18
+ const CLOSE_TIMEOUT = 100; // just enough time for the onerror to appear before onclose
19
+
20
+ const commonSwitch = async ({
21
+ data,
22
+ cipher,
23
+ addMember,
24
+ send,
25
+ publisher,
26
+ resolve,
27
+ memberCredentials,
28
+ memberName,
29
+ reject,
30
+ ws,
31
+ setFinished,
32
+ initialTrustchainId,
33
+ }) => {
34
+ switch (data.message) {
35
+ case "TrustchainShareCredential": {
36
+ if (!initialTrustchainId) {
37
+ const payload = {
38
+ type: "UNEXPECTED_SHARE_CREDENTIAL",
39
+ message: "unexpected share credential",
40
+ };
41
+ send({ version, publisher, message: "Failure", payload });
42
+ throw new NoTrustchainInitialized("unexpected share credential");
43
+ }
44
+ setFinished(true);
45
+ if (!cipher) {
46
+ throw new Error("sessionEncryptionKey not set");
47
+ }
48
+ const { id, name } = await cipher.decryptMessage(data);
49
+ const trustchain = await addMember({ id, name, permissions: Permissions.OWNER });
50
+ const payload = await cipher.encryptMessagePayload({ trustchain });
51
+ send({ version, publisher, message: "TrustchainAddedMember", payload });
52
+ resolve();
53
+ break;
54
+ }
55
+
56
+ case "TrustchainRequestCredential": {
57
+ if (initialTrustchainId) {
58
+ const payload = {
59
+ type: "UNEXPECTED_REQUEST_CREDENTIAL",
60
+ message: initialTrustchainId,
61
+ };
62
+ send({ version, publisher, message: "Failure", payload });
63
+ throw new TrustchainAlreadyInitialized(initialTrustchainId);
64
+ }
65
+ const payload = await cipher.encryptMessagePayload({
66
+ id: memberCredentials.pubkey,
67
+ name: memberName,
68
+ });
69
+ send({ version, publisher, message: "TrustchainShareCredential", payload });
70
+ break;
71
+ }
72
+ case "TrustchainAddedMember": {
73
+ setFinished(true);
74
+ const { trustchain } = await cipher.decryptMessage(data);
75
+ resolve(trustchain);
76
+ ws.close();
77
+ break;
78
+ }
79
+ case "Failure": {
80
+ setFinished(true);
81
+ log("trustchain/qrcode", "Failure", { data });
82
+ const error = fromErrorMessage(data.payload);
83
+ reject(error);
84
+ ws.close();
85
+ break;
86
+ }
87
+ case "HandshakeChallenge":
88
+ case "HandshakeCompletionSucceeded":
89
+ case "InitiateHandshake":
90
+ case "CompleteHandshakeChallenge":
91
+ break;
92
+ default:
93
+ throw new Error("unexpected message");
94
+ }
95
+ };
96
+
97
+ /**
98
+ * establish a channel to be able to add a member to the trustchain after displaying the QR Code
99
+ * @returns a promise that resolves when this is done
100
+ */
101
+ export async function createQRCodeHostInstance({
102
+ trustchainApiBaseUrl,
103
+ onDisplayQRCode,
104
+ onDisplayDigits,
105
+ addMember,
106
+ memberCredentials,
107
+ memberName,
108
+ initialTrustchainId,
109
+ }: {
110
+ /**
111
+ * the base URL of the trustchain API
112
+ */
113
+ trustchainApiBaseUrl: string;
114
+ /**
115
+ * this function will need to display a UI to show the QR Code
116
+ */
117
+ onDisplayQRCode: (url: string) => void;
118
+ /**
119
+ * this function will need to display a UI to show the digits
120
+ */
121
+ onDisplayDigits: (digits: string) => void;
122
+ /**
123
+ * this function will need to using the TrustchainSDK (and use sdk.addMember)
124
+ */
125
+ addMember: (member: TrustchainMember) => Promise<Trustchain>;
126
+ /**
127
+ * the client credentials of the instance (given by TrustchainSDK)
128
+ */
129
+ memberCredentials: MemberCredentials;
130
+ /**
131
+ * the name of the member
132
+ */
133
+ memberName: string;
134
+ /**
135
+ * if the member already has a trustchain, this will be defined
136
+ */
137
+ initialTrustchainId?: string;
138
+ }): Promise<Trustchain | void> {
139
+ const ephemeralKey = await crypto.randomKeypair();
140
+ const publisher = crypto.to_hex(ephemeralKey.publicKey);
141
+ const url = `${trustchainApiBaseUrl.replace("http", "ws")}/v1/qr?host=${publisher}`;
142
+ const ws = new WebSocket(url);
143
+ function send(message: Message) {
144
+ ws.send(JSON.stringify(message));
145
+ }
146
+
147
+ let sessionEncryptionKey: Uint8Array | undefined;
148
+ let cipher: ReturnType<typeof makeMessageCipher> | undefined;
149
+ let expectedDigits: string | undefined;
150
+ let finished = false;
151
+ const setFinished = newValue => (finished = newValue);
152
+
153
+ onDisplayQRCode(url);
154
+ return new Promise((resolve, reject) => {
155
+ const startedAt = Date.now();
156
+
157
+ ws.addEventListener("error", reject);
158
+ ws.addEventListener("close", () => {
159
+ if (finished) return;
160
+ // this error would reflect a protocol error. because otherwise, we would get the "error" event.
161
+ const time = Date.now() - startedAt;
162
+ reject(new QRCodeWSClosed("qrcode websocket prematurely closed", { time }));
163
+ });
164
+ ws.addEventListener("message", async e => {
165
+ try {
166
+ const data = parseMessage(e.data);
167
+ switch (data.message) {
168
+ case "InitiateHandshake": {
169
+ const candidatePublicKey = crypto.from_hex(data.payload.ephemeral_public_key);
170
+ sessionEncryptionKey = await crypto.ecdh(ephemeralKey, candidatePublicKey);
171
+ cipher = makeMessageCipher(makeCipher(sessionEncryptionKey));
172
+ // --- end of handshake first phase ---
173
+ const digitsCount = 3;
174
+ const digits = await randomDigits(digitsCount);
175
+ expectedDigits = digits;
176
+ onDisplayDigits(digits);
177
+ const payload = await cipher.encryptMessagePayload({
178
+ digits: digitsCount,
179
+ connected: false,
180
+ });
181
+ send({ version, publisher, message: "HandshakeChallenge", payload });
182
+ break;
183
+ }
184
+ case "CompleteHandshakeChallenge": {
185
+ if (!cipher) {
186
+ throw new Error("sessionEncryptionKey not set");
187
+ }
188
+ const { digits } = await cipher.decryptMessage(data);
189
+ if (digits !== expectedDigits) {
190
+ console.warn("User invalid digits", { digits, expectedDigits });
191
+ const payload = {
192
+ type: "HANDSHAKE_COMPLETION_FAILED",
193
+ message: "invalid digits",
194
+ };
195
+ send({ version, publisher, message: "Failure", payload });
196
+ throw new InvalidDigitsError("invalid digits");
197
+ }
198
+ const payload = await cipher.encryptMessagePayload({});
199
+ send({ version, publisher, message: "HandshakeCompletionSucceeded", payload });
200
+ break;
201
+ }
202
+ }
203
+ await commonSwitch({
204
+ data,
205
+ cipher,
206
+ addMember,
207
+ send,
208
+ publisher,
209
+ resolve,
210
+ memberCredentials,
211
+ memberName,
212
+ reject,
213
+ ws,
214
+ setFinished,
215
+ initialTrustchainId,
216
+ });
217
+ } catch (e) {
218
+ console.error("socket error", e);
219
+ ws.close();
220
+ reject(e);
221
+ }
222
+ });
223
+ });
224
+ }
225
+
226
+ /**
227
+ * establish a channel to be able to add myself to the trustchain after scanning the QR Code
228
+ * @returns a promise that resolves a Trustchain when this is done
229
+ */
230
+ export async function createQRCodeCandidateInstance({
231
+ memberCredentials,
232
+ memberName,
233
+ addMember,
234
+ initialTrustchainId,
235
+ scannedUrl,
236
+ onRequestQRCodeInput,
237
+ }: {
238
+ /**
239
+ * the client credentials of the instance (given by TrustchainSDK)
240
+ */
241
+ memberCredentials: MemberCredentials;
242
+ /**
243
+ * the name of the member
244
+ */
245
+ memberName: string;
246
+ /**
247
+ * if the member already has a trustchain, this will be defined
248
+ */
249
+ initialTrustchainId?: string;
250
+ /**
251
+ * this function will need to using the TrustchainSDK (and use sdk.addMember)
252
+ */
253
+ addMember: (member: TrustchainMember) => Promise<Trustchain>;
254
+ /**
255
+ * the scanned URL that contains the host public key
256
+ */
257
+ scannedUrl: string;
258
+ /**
259
+ * this function will need to display a UI to ask the user to input the digits
260
+ * and then call the callback with the digits
261
+ */
262
+ onRequestQRCodeInput: (
263
+ config: {
264
+ digits: number;
265
+ connected: boolean;
266
+ },
267
+ callback: (digits: string) => void,
268
+ ) => void;
269
+ }): Promise<Trustchain | void> {
270
+ const m = scannedUrl.match(/host=([0-9A-Fa-f]+)/);
271
+ if (!m) {
272
+ if (isFromOldAccountsImport(scannedUrl)) throw new ScannedOldImportQrCode();
273
+ throw new ScannedInvalidQrCode();
274
+ }
275
+ const hostPublicKey = crypto.from_hex(m[1]);
276
+ const ephemeralKey = await crypto.randomKeypair();
277
+ const publisher = crypto.to_hex(ephemeralKey.publicKey);
278
+ const sessionEncryptionKey = await crypto.ecdh(ephemeralKey, hostPublicKey);
279
+ const cipher = makeMessageCipher(makeCipher(sessionEncryptionKey));
280
+ const ws = new WebSocket(scannedUrl);
281
+ function send(message: Message) {
282
+ ws.send(JSON.stringify(message));
283
+ }
284
+ let finished = false;
285
+ const setFinished = newValue => (finished = newValue);
286
+
287
+ return new Promise((resolve, reject) => {
288
+ ws.addEventListener("close", () => {
289
+ if (finished) return;
290
+ // this error would reflect a protocol error. because otherwise, we would get the "error" event. it shouldn't be visible to user, but we use it to ensure the promise ends.
291
+ setTimeout(() => reject(new Error("qrcode websocket prematurely closed")), CLOSE_TIMEOUT);
292
+ });
293
+
294
+ ws.addEventListener("message", async e => {
295
+ try {
296
+ const data = parseMessage(e.data);
297
+ switch (data.message) {
298
+ case "HandshakeChallenge": {
299
+ const config = await cipher.decryptMessage(data);
300
+ onRequestQRCodeInput(config, digits => {
301
+ cipher.encryptMessagePayload({ digits }).then(payload => {
302
+ send({ version, publisher, message: "CompleteHandshakeChallenge", payload });
303
+ });
304
+ });
305
+ break;
306
+ }
307
+ case "HandshakeCompletionSucceeded": {
308
+ if (initialTrustchainId) {
309
+ const payload = await cipher.encryptMessagePayload({});
310
+ send({ version, publisher, message: "TrustchainRequestCredential", payload });
311
+ } else {
312
+ const payload = await cipher.encryptMessagePayload({
313
+ id: memberCredentials.pubkey,
314
+ name: memberName,
315
+ });
316
+ send({ version, publisher, message: "TrustchainShareCredential", payload });
317
+ }
318
+ break;
319
+ }
320
+ }
321
+ await commonSwitch({
322
+ data,
323
+ cipher,
324
+ addMember,
325
+ send,
326
+ publisher,
327
+ resolve,
328
+ memberCredentials,
329
+ memberName,
330
+ reject,
331
+ ws,
332
+ setFinished,
333
+ initialTrustchainId,
334
+ });
335
+ } catch (e) {
336
+ console.error("socket error", e);
337
+ ws.close();
338
+ reject(e);
339
+ }
340
+ });
341
+ ws.addEventListener("error", reject);
342
+ ws.addEventListener("open", () => {
343
+ const payload = { ephemeral_public_key: crypto.to_hex(ephemeralKey.publicKey) };
344
+ send({ version, publisher, message: "InitiateHandshake", payload });
345
+ });
346
+ });
347
+ }
348
+
349
+ async function randomDigits(count: number): Promise<string> {
350
+ const bytes = await crypto.randomBytes(count);
351
+ let digits = "";
352
+ for (let i = 0; i < count; i++) {
353
+ digits += (bytes[i] % 10).toString();
354
+ }
355
+ return digits;
356
+ }
357
+
358
+ function parseMessage(e): Message {
359
+ const message = JSON.parse(e.toString());
360
+ if (!message || typeof message !== "object") {
361
+ throw new Error("invalid message");
362
+ }
363
+ if (message.version !== 1) {
364
+ throw new Error("invalid version");
365
+ }
366
+ if (typeof message.publisher !== "string") {
367
+ throw new Error("invalid publisher");
368
+ }
369
+ if (typeof message.message !== "string") {
370
+ throw new Error("invalid message");
371
+ }
372
+ if (typeof message.payload !== "object") {
373
+ throw new Error("invalid payload");
374
+ }
375
+ return message;
376
+ }
377
+
378
+ function fromErrorMessage(payload: { message: string; type: string }): Error {
379
+ if (payload.type === "HANDSHAKE_COMPLETION_FAILED") {
380
+ throw new InvalidDigitsError(payload.message);
381
+ }
382
+ if (payload.type === "UNEXPECTED_SHARE_CREDENTIAL") {
383
+ throw new NoTrustchainInitialized(payload.message);
384
+ }
385
+ if (payload.type === "UNEXPECTED_REQUEST_CREDENTIAL") {
386
+ throw new TrustchainAlreadyInitialized(payload.message);
387
+ }
388
+ const error = new Error(payload.message);
389
+ error.name = "TrustchainQRCode-" + payload.type;
390
+ return error;
391
+ }
392
+
393
+ function isFromOldAccountsImport(scannedUrl: string): boolean {
394
+ return !!scannedUrl.match(/^[A-Za-z0-9+/=]*$/);
395
+ }
@@ -0,0 +1,70 @@
1
+ import { Trustchain } from "../types";
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4
+ export type Encrypted<T> = {
5
+ encrypted: string;
6
+ };
7
+ export type Message =
8
+ | {
9
+ version: number;
10
+ publisher: string;
11
+ message: "InitiateHandshake";
12
+ payload: { ephemeral_public_key: string };
13
+ }
14
+ | {
15
+ version: number;
16
+ publisher: string;
17
+ message: "Failure";
18
+ payload: { message: string; type: string };
19
+ }
20
+ | {
21
+ version: number;
22
+ publisher: string;
23
+ message: "HandshakeChallenge";
24
+ payload: Encrypted<{ digits: number; connected: boolean }>;
25
+ }
26
+ | {
27
+ version: number;
28
+ publisher: string;
29
+ message: "CompleteHandshakeChallenge";
30
+ payload: Encrypted<{ digits: string }>;
31
+ }
32
+ | {
33
+ version: number;
34
+ publisher: string;
35
+ message: "HandshakeCompletionSucceeded";
36
+ payload: Encrypted<Record<string, never>>;
37
+ }
38
+ | {
39
+ version: number;
40
+ publisher: string;
41
+ message: "TrustchainRequestCredential";
42
+ payload: Encrypted<Record<string, never>>;
43
+ }
44
+ | {
45
+ version: number;
46
+ publisher: string;
47
+ message: "TrustchainShareCredential";
48
+ payload: Encrypted<{
49
+ // public key of the member
50
+ id: string;
51
+ // name of the member
52
+ name: string;
53
+ }>;
54
+ }
55
+ | {
56
+ version: number;
57
+ publisher: string;
58
+ message: "TrustchainAddedMember";
59
+ payload: Encrypted<{
60
+ trustchain: Trustchain;
61
+ }>;
62
+ };
63
+
64
+ export type DecryptedPayload<M> = M extends { payload: Encrypted<infer T> }
65
+ ? T
66
+ : M extends { payload: infer T }
67
+ ? T
68
+ : never;
69
+
70
+ export type ExtractEncryptedPayloads<T> = T extends { payload: Encrypted<infer P> } ? P : never;