favalib 0.0.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 (67) hide show
  1. package/build/Command/BaseCommand.d.mts +65 -0
  2. package/build/Command/BaseCommand.mjs +54 -0
  3. package/build/Command/CommandQueue.d.mts +28 -0
  4. package/build/Command/CommandQueue.mjs +43 -0
  5. package/build/Command/commandConstructors.d.mts +11 -0
  6. package/build/Command/commandConstructors.mjs +11 -0
  7. package/build/Command/commands/AddEntryCommand.d.mts +31 -0
  8. package/build/Command/commands/AddEntryCommand.mjs +43 -0
  9. package/build/Command/commands/AddSyncDeviceCommand.d.mts +36 -0
  10. package/build/Command/commands/AddSyncDeviceCommand.mjs +42 -0
  11. package/build/Command/commands/DeleteEntryCommand.d.mts +35 -0
  12. package/build/Command/commands/DeleteEntryCommand.mjs +50 -0
  13. package/build/Command/commands/UpdateEntryCommand.d.mts +38 -0
  14. package/build/Command/commands/UpdateEntryCommand.mjs +51 -0
  15. package/build/CryptoProviders/browser/index.d.mts +73 -0
  16. package/build/CryptoProviders/browser/index.mjs +209 -0
  17. package/build/CryptoProviders/node/index.d.mts +62 -0
  18. package/build/CryptoProviders/node/index.mjs +189 -0
  19. package/build/TwoFALibError.d.mts +77 -0
  20. package/build/TwoFALibError.mjs +91 -0
  21. package/build/TwoFaLib.d.mts +95 -0
  22. package/build/TwoFaLib.mjs +180 -0
  23. package/build/TwoFaLibEvent.d.mts +8 -0
  24. package/build/TwoFaLibEvent.mjs +9 -0
  25. package/build/TwoFaLibMediator.d.mts +37 -0
  26. package/build/TwoFaLibMediator.mjs +58 -0
  27. package/build/interfaces/CommandTypes.d.mts +20 -0
  28. package/build/interfaces/CommandTypes.mjs +1 -0
  29. package/build/interfaces/CryptoLib.d.mts +113 -0
  30. package/build/interfaces/CryptoLib.mjs +1 -0
  31. package/build/interfaces/Entry.d.mts +33 -0
  32. package/build/interfaces/Entry.mjs +1 -0
  33. package/build/interfaces/Events.d.mts +22 -0
  34. package/build/interfaces/Events.mjs +1 -0
  35. package/build/interfaces/PassphraseExtraDict.d.ts +2 -0
  36. package/build/interfaces/PassphraseExtraDict.js +1 -0
  37. package/build/interfaces/SyncTypes.d.mts +45 -0
  38. package/build/interfaces/SyncTypes.mjs +1 -0
  39. package/build/interfaces/Vault.d.mts +30 -0
  40. package/build/interfaces/Vault.mjs +1 -0
  41. package/build/main.d.mts +12 -0
  42. package/build/main.mjs +5 -0
  43. package/build/subclasses/CommandManager.d.mts +46 -0
  44. package/build/subclasses/CommandManager.mjs +117 -0
  45. package/build/subclasses/ExportImportManager.d.mts +58 -0
  46. package/build/subclasses/ExportImportManager.mjs +105 -0
  47. package/build/subclasses/LibraryLoader.d.mts +56 -0
  48. package/build/subclasses/LibraryLoader.mjs +108 -0
  49. package/build/subclasses/PersistentStorageManager.d.mts +71 -0
  50. package/build/subclasses/PersistentStorageManager.mjs +127 -0
  51. package/build/subclasses/SyncManager.d.mts +161 -0
  52. package/build/subclasses/SyncManager.mjs +567 -0
  53. package/build/subclasses/VaultDataManager.d.mts +68 -0
  54. package/build/subclasses/VaultDataManager.mjs +114 -0
  55. package/build/subclasses/VaultOperationsManager.d.mts +91 -0
  56. package/build/subclasses/VaultOperationsManager.mjs +163 -0
  57. package/build/utils/constants.d.mts +2 -0
  58. package/build/utils/constants.mjs +1 -0
  59. package/build/utils/creationUtils.d.mts +43 -0
  60. package/build/utils/creationUtils.mjs +125 -0
  61. package/build/utils/exportImportUtils.d.mts +53 -0
  62. package/build/utils/exportImportUtils.mjs +185 -0
  63. package/build/utils/qrUtils.d.mts +25 -0
  64. package/build/utils/qrUtils.mjs +84 -0
  65. package/build/utils/syncUtils.d.mts +26 -0
  66. package/build/utils/syncUtils.mjs +78 -0
  67. package/package.json +56 -0
@@ -0,0 +1,567 @@
1
+ import WebSocket from 'isomorphic-ws';
2
+ import { base64ToUint8Array, hexToUint8Array, stringToBase64, uint8ArrayToBase64, uint8ArrayToHex, } from 'uint8array-extras';
3
+ import { deriveSFromPassword, JPakeThreePass, } from 'jpake-ts';
4
+ import { TwoFaLibEvent } from '../TwoFaLibEvent.mjs';
5
+ import { decodeInitiatorData, jsonToUint8Array } from '../utils/syncUtils.mjs';
6
+ import { InitializationError, SyncAddDeviceFlowConflictError, SyncError, SyncInWrongStateError, SyncNoServerConnectionError, TwoFALibError, } from '../TwoFALibError.mjs';
7
+ import AddSyncDeviceCommand from '../Command/commands/AddSyncDeviceCommand.mjs';
8
+ const IN_TESTING = process.env.NODE_ENV === 'test';
9
+ const IN_DEV = process.env.NODE_ENV === 'development';
10
+ const generateNonCryptographicRandomString = () => {
11
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
12
+ const length = Math.floor(Math.random() * 64) + 1;
13
+ return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
14
+ };
15
+ /**
16
+ * Manages synchronization of 2FA devices and communication with the server.
17
+ */
18
+ class SyncManager {
19
+ /**
20
+ * Public getter for the command send queue.
21
+ * @returns The command send queue.
22
+ */
23
+ getCommandSendQueue() {
24
+ return this.commandSendQueue;
25
+ }
26
+ /**
27
+ * Creates an instance of SyncManager.
28
+ * @param mediator - The mediator for accessing other components.
29
+ * @param deviceType - The type of the device.
30
+ * @param publicKey - The public key of the device.
31
+ * @param privateKey - The private key of the device.
32
+ * @param syncState - The state of the sync.
33
+ * @param deviceId - The unique identifier of the device.
34
+ * @throws {InitializationError} If initialization fails (e.g., if the server URL is invalid).
35
+ */
36
+ constructor(mediator, deviceType, publicKey, privateKey, syncState, deviceId) {
37
+ this.mediator = mediator;
38
+ this.deviceType = deviceType;
39
+ this.publicKey = publicKey;
40
+ this.privateKey = privateKey;
41
+ this.reconnectInterval = IN_TESTING ? 100 : 5000; // 5 seconds
42
+ this.readyEventEmitted = false;
43
+ this.commandSendQueue = [];
44
+ this.shouldReconnect = true;
45
+ const { serverUrl, devices, commandSendQueue } = syncState;
46
+ if (!serverUrl.startsWith('wss://')) {
47
+ if (!serverUrl.startsWith('ws://') && !(IN_DEV || IN_TESTING)) {
48
+ throw new InitializationError('Invalid server URL, protocol must be wss');
49
+ }
50
+ }
51
+ this.deviceId = deviceId;
52
+ this.syncDevices = devices;
53
+ this.commandSendQueue = commandSendQueue;
54
+ this.serverUrl = serverUrl;
55
+ this.initServerConnection();
56
+ }
57
+ get libraryLoader() {
58
+ return this.mediator.getComponent('libraryLoader');
59
+ }
60
+ get cryptoLib() {
61
+ return this.libraryLoader.getCryptoLib();
62
+ }
63
+ get persistentStorageManager() {
64
+ return this.mediator.getComponent('persistentStorageManager');
65
+ }
66
+ get commandManager() {
67
+ return this.mediator.getComponent('commandManager');
68
+ }
69
+ get dispatchLibEvent() {
70
+ return this.mediator.getComponent('dispatchLibEvent');
71
+ }
72
+ get log() {
73
+ return this.mediator.getComponent('log');
74
+ }
75
+ /**
76
+ * @returns Whether an add device flow is currently active.
77
+ */
78
+ get inAddDeviceFlow() {
79
+ return Boolean(this.activeAddDeviceFlow);
80
+ }
81
+ /**
82
+ * @returns Whether the WebSocket connection is open.
83
+ */
84
+ get webSocketConnected() {
85
+ return this.ws?.readyState === WebSocket.OPEN;
86
+ }
87
+ async getNonce() {
88
+ return uint8ArrayToBase64(await this.cryptoLib.getRandomBytes(16));
89
+ }
90
+ sendToServer(type, data) {
91
+ if (!this.ws || !this.webSocketConnected) {
92
+ throw new SyncNoServerConnectionError();
93
+ }
94
+ this.ws.send(JSON.stringify({ type, data }));
95
+ }
96
+ /**
97
+ * Initializes the WebSocket connection to the server.
98
+ */
99
+ initServerConnection() {
100
+ const ws = new WebSocket(this.serverUrl);
101
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
102
+ const syncManager = this;
103
+ ws.addEventListener('error', (event) => {
104
+ syncManager.log('warning', `Error in websocket: ${event.error}`);
105
+ });
106
+ ws.addEventListener('message', function message(message) {
107
+ try {
108
+ const jsonString = String(message.data);
109
+ const parsedMessage = JSON.parse(jsonString);
110
+ syncManager.handleServerMessage(parsedMessage);
111
+ }
112
+ catch (error) {
113
+ // eslint-disable-next-line no-restricted-globals
114
+ if (error instanceof Error) {
115
+ syncManager.log('warning', `Failed to parse message: ${error.message}`);
116
+ }
117
+ else {
118
+ syncManager.log('warning', `Failed to parse message: ${String(error)}`);
119
+ }
120
+ }
121
+ });
122
+ ws.addEventListener('open', () => {
123
+ this.log('info', 'Connected to server.');
124
+ this.sendToServer('connect', { deviceId: syncManager.deviceId });
125
+ this.dispatchLibEvent(TwoFaLibEvent.ConnectionToSyncServerStatusChanged, {
126
+ connected: true,
127
+ });
128
+ // send any commands that were done while offline
129
+ void this.processCommandSendQueue();
130
+ });
131
+ ws.addEventListener('close', this.handleWebSocketClose.bind(this));
132
+ this.ws = ws;
133
+ }
134
+ handleWebSocketClose(event) {
135
+ this.dispatchLibEvent(TwoFaLibEvent.ConnectionToSyncServerStatusChanged, {
136
+ connected: false,
137
+ });
138
+ if (this.shouldReconnect) {
139
+ // if we shouldn't reconnect, this closing is expected
140
+ this.log('warning', `WebSocket closed: ${event.code} ${event.reason}`);
141
+ this.attemptReconnect();
142
+ }
143
+ }
144
+ handleServerMessage(message) {
145
+ switch (message.type) {
146
+ case 'confirmAddSyncDeviceInitialiseData': {
147
+ if (this.activeAddDeviceFlow?.state !== 'initiator:initiated') {
148
+ throw new SyncInWrongStateError(`Expected initiator:initiated, got ${this.activeAddDeviceFlow?.state}`);
149
+ }
150
+ clearTimeout(this.activeAddDeviceFlow.timeout);
151
+ this.activeAddDeviceFlow.resolveContinuePromise(message);
152
+ break;
153
+ }
154
+ case 'JPAKEPass2': {
155
+ const { data } = message;
156
+ const unconvertedPass2Result = data.pass2Result;
157
+ const pass2Result = {
158
+ round1Result: jsonToUint8Array(unconvertedPass2Result.round1Result),
159
+ round2Result: jsonToUint8Array(unconvertedPass2Result.round2Result),
160
+ };
161
+ void this.finishAddDeviceFlowKeyExchangeInitiator(pass2Result, data.responderDeviceId, data.responderDeviceType);
162
+ break;
163
+ }
164
+ case 'JPAKEPass3': {
165
+ const { data } = message;
166
+ const pass3Result = jsonToUint8Array(data.pass3Result);
167
+ void this.finishAddDeviceFlowKeyExchangeResponder(pass3Result);
168
+ break;
169
+ }
170
+ case 'publicKey': {
171
+ const { data } = message;
172
+ const { responderEncryptedPublicKey } = data;
173
+ void this.sendInitialVaultData(responderEncryptedPublicKey);
174
+ break;
175
+ }
176
+ case 'vault': {
177
+ const { data } = message;
178
+ const { encryptedVaultData, initiatorEncryptedPublicKey } = data;
179
+ void this.importInitialVaultState(encryptedVaultData, initiatorEncryptedPublicKey);
180
+ break;
181
+ }
182
+ case 'syncCommandsReceived': {
183
+ const { data: { commandIds }, } = message;
184
+ void this.commandsSuccesfullyReceived(commandIds);
185
+ break;
186
+ }
187
+ case 'syncCommands': {
188
+ const { data: commands } = message;
189
+ void this.receiveCommands(commands);
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ attemptReconnect() {
195
+ this.log('info', 'Connection to server lost, attempting to reconnect...');
196
+ this.reconnectTimeout = setTimeout(() => {
197
+ this.initServerConnection();
198
+ }, this.reconnectInterval);
199
+ }
200
+ /**
201
+ * @inheritdoc
202
+ */
203
+ async initiateAddDeviceFlow(returnAs) {
204
+ if (this.activeAddDeviceFlow) {
205
+ throw new SyncAddDeviceFlowConflictError();
206
+ }
207
+ if (!this.ws || !this.webSocketConnected) {
208
+ throw new SyncNoServerConnectionError();
209
+ }
210
+ const addDevicePassword = deriveSFromPassword(uint8ArrayToBase64(await this.cryptoLib.getRandomBytes(60)));
211
+ const timestamp = Date.now();
212
+ const jpak = new JPakeThreePass(this.deviceId);
213
+ const pass1Result = jpak.pass1();
214
+ const continuePromise = new Promise((resolve, reject) => {
215
+ // Set a timeout for if we get no response from the server
216
+ const timeout = setTimeout(() => {
217
+ if (this.activeAddDeviceFlow?.state === 'initiator:initiated') {
218
+ reject(new TwoFALibError('Timeout of registerAddDeviceFlowRequest, no response'));
219
+ this.activeAddDeviceFlow = undefined;
220
+ }
221
+ }, 10000);
222
+ this.activeAddDeviceFlow = {
223
+ state: 'initiator:initiated',
224
+ jpak,
225
+ addDevicePassword,
226
+ initiatorDeviceId: this.deviceId,
227
+ timestamp,
228
+ resolveContinuePromise: resolve,
229
+ timeout,
230
+ };
231
+ });
232
+ // register this add device request at the server
233
+ this.sendToServer('addSyncDeviceInitialiseData', {
234
+ initiatorDeviceType: this.deviceType,
235
+ initiatorDeviceId: this.deviceId,
236
+ timestamp,
237
+ nonce: await this.getNonce(),
238
+ });
239
+ // wait for the server to confirm it has registered the add device request
240
+ await continuePromise;
241
+ const returnData = {
242
+ addDevicePassword: uint8ArrayToBase64(addDevicePassword),
243
+ initiatorDeviceId: this.deviceId,
244
+ initiatorDeviceType: this.deviceType,
245
+ timestamp,
246
+ pass1Result: {
247
+ G1: uint8ArrayToHex(pass1Result.G1),
248
+ G2: uint8ArrayToHex(pass1Result.G2),
249
+ ZKPx1: uint8ArrayToHex(pass1Result.ZKPx1),
250
+ ZKPx2: uint8ArrayToHex(pass1Result.ZKPx2),
251
+ },
252
+ };
253
+ let returnQr = null;
254
+ if (returnAs.qr) {
255
+ const qrGeneratorLib = await this.libraryLoader.getQrGeneratorLib();
256
+ returnQr = await qrGeneratorLib.toDataURL(JSON.stringify(returnData));
257
+ }
258
+ const returnText = returnAs.text
259
+ ? stringToBase64(JSON.stringify(returnData), { urlSafe: true })
260
+ : null;
261
+ return {
262
+ qr: returnQr,
263
+ text: returnText,
264
+ };
265
+ }
266
+ /**
267
+ * Responds to an add device flow initiated by another device.
268
+ * @param initiatorData The data received from the initiating device.
269
+ * @param initiatorDataType The type of the initiatorData, determines how it should be decoded
270
+ * @throws {SyncNoServerConnectionError} If there is no server connection.
271
+ * @throws {SyncAddDeviceFlowConflictError} If an add device flow is already active.
272
+ * @throws {SyncError} If the initiator data is invalid.
273
+ */
274
+ async respondToAddDeviceFlow(initiatorData, initiatorDataType) {
275
+ if (!this.ws || !this.webSocketConnected) {
276
+ throw new SyncNoServerConnectionError();
277
+ }
278
+ if (this.activeAddDeviceFlow) {
279
+ throw new SyncAddDeviceFlowConflictError();
280
+ }
281
+ const { addDevicePassword, initiatorDeviceId, timestamp, pass1Result, initiatorDeviceType: initiatorDeviceIdentifier, } = await decodeInitiatorData(initiatorData, initiatorDataType, await this.libraryLoader.getJsQrLib(), this.libraryLoader.getCanvasLib.bind(this));
282
+ if (!addDevicePassword ||
283
+ !initiatorDeviceId ||
284
+ !timestamp ||
285
+ !pass1Result ||
286
+ !initiatorDeviceIdentifier) {
287
+ throw new SyncError('Missing required fields in initiator data');
288
+ }
289
+ // Decode the base64 password
290
+ const decodedPassword = base64ToUint8Array(addDevicePassword);
291
+ const jpak = new JPakeThreePass(this.deviceId);
292
+ // Process the first pass from the initiator
293
+ const initiatorPass1Result = {
294
+ G1: hexToUint8Array(pass1Result.G1),
295
+ G2: hexToUint8Array(pass1Result.G2),
296
+ ZKPx1: hexToUint8Array(pass1Result.ZKPx1),
297
+ ZKPx2: hexToUint8Array(pass1Result.ZKPx2),
298
+ };
299
+ let pass2Result;
300
+ try {
301
+ pass2Result = jpak.pass2(initiatorPass1Result, decodedPassword, initiatorDeviceId);
302
+ }
303
+ catch {
304
+ throw new SyncError('Error processing initiator pass 1');
305
+ }
306
+ this.activeAddDeviceFlow = {
307
+ state: 'responder:initated',
308
+ jpak,
309
+ addDevicePassword: decodedPassword,
310
+ responderDeviceId: this.deviceId,
311
+ initiatorDeviceId: initiatorDeviceId,
312
+ initiatorDeviceType: initiatorDeviceIdentifier,
313
+ timestamp: Date.now(),
314
+ };
315
+ // respond to this add device request at the server
316
+ this.sendToServer('JPAKEPass2', {
317
+ nonce: await this.getNonce(),
318
+ // @ts-expect-error we get a type mismatch because we input Uint8Array instead of JsonifiedUint8Array, but it will get jsonified later
319
+ pass2Result,
320
+ responderDeviceId: this.deviceId,
321
+ initiatorDeviceId: initiatorDeviceId,
322
+ responderDeviceType: this.deviceType,
323
+ });
324
+ }
325
+ async finishAddDeviceFlowKeyExchangeInitiator(pass2Result, responderDeviceId, responderDeviceType) {
326
+ if (!this.ws || !this.webSocketConnected) {
327
+ throw new SyncNoServerConnectionError();
328
+ }
329
+ if (this.activeAddDeviceFlow?.state !== 'initiator:initiated') {
330
+ throw new SyncInWrongStateError(`Expected initiator:initiated, got ${this.activeAddDeviceFlow?.state}`);
331
+ }
332
+ const pass3Result = this.activeAddDeviceFlow.jpak.pass3(pass2Result, this.activeAddDeviceFlow.addDevicePassword, responderDeviceId);
333
+ this.sendToServer('JPAKEPass3', {
334
+ nonce: await this.getNonce(),
335
+ initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
336
+ // @ts-expect-error we get a type mismatch because we input Uint8Array instead of JsonifiedUint8Array, but it will get jsonified later
337
+ pass3Result,
338
+ });
339
+ const sharedKey = this.activeAddDeviceFlow.jpak.deriveSharedKey();
340
+ const syncKey = await this.cryptoLib.createSyncKey(sharedKey, responderDeviceId);
341
+ this.activeAddDeviceFlow = {
342
+ ...this.activeAddDeviceFlow,
343
+ state: 'initiator:syncKeyCreated',
344
+ responderDeviceId: responderDeviceId,
345
+ responderDeviceType: responderDeviceType,
346
+ syncKey,
347
+ };
348
+ }
349
+ async finishAddDeviceFlowKeyExchangeResponder(pass3Result) {
350
+ if (!this.ws || !this.webSocketConnected) {
351
+ throw new SyncNoServerConnectionError();
352
+ }
353
+ if (this.activeAddDeviceFlow?.state !== 'responder:initated') {
354
+ throw new SyncInWrongStateError(`Expected responder:initiated, got ${this.activeAddDeviceFlow?.state}`);
355
+ }
356
+ if (!this.publicKey) {
357
+ throw new SyncError('Public key not set');
358
+ }
359
+ this.activeAddDeviceFlow.jpak.receivePass3Results(pass3Result);
360
+ const sharedKey = this.activeAddDeviceFlow.jpak.deriveSharedKey();
361
+ const syncKey = await this.cryptoLib.createSyncKey(sharedKey, this.activeAddDeviceFlow.responderDeviceId);
362
+ this.activeAddDeviceFlow = {
363
+ ...this.activeAddDeviceFlow,
364
+ state: 'responder:syncKeyCreated',
365
+ syncKey,
366
+ };
367
+ const responderEncryptedPublicKey = await this.cryptoLib.encryptSymmetric(syncKey, this.publicKey);
368
+ // send our public key
369
+ this.sendToServer('publicKey', {
370
+ nonce: await this.getNonce(),
371
+ responderEncryptedPublicKey,
372
+ initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
373
+ });
374
+ }
375
+ async sendInitialVaultData(responderEncryptedPublicKey) {
376
+ if (!this.ws || !this.webSocketConnected) {
377
+ throw new SyncNoServerConnectionError();
378
+ }
379
+ if (this.activeAddDeviceFlow?.state !== 'initiator:syncKeyCreated') {
380
+ throw new SyncInWrongStateError(`Expected initiator:syncKeyCreated, got ${this.activeAddDeviceFlow?.state}`);
381
+ }
382
+ if (!this.publicKey) {
383
+ throw new SyncError('Public key not set');
384
+ }
385
+ const syncKey = this.activeAddDeviceFlow.syncKey;
386
+ // Decrypt the received public key
387
+ const decryptedPublicKey = await this.cryptoLib.decryptSymmetric(syncKey, responderEncryptedPublicKey);
388
+ // get the vault data (encrypted with the sync key)
389
+ const encryptedVaultData = await this.persistentStorageManager.getEncryptedVaultState(syncKey);
390
+ const initiatorEncryptedPublicKey = await this.cryptoLib.encryptSymmetric(syncKey, this.publicKey);
391
+ // Send the encrypted vault data to the server
392
+ this.sendToServer('vault', {
393
+ nonce: await this.getNonce(),
394
+ encryptedVaultData,
395
+ initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
396
+ initiatorEncryptedPublicKey,
397
+ });
398
+ // save the added the sync device, done via command so this is synced to
399
+ // all the other (already existing) sync devices
400
+ const command = AddSyncDeviceCommand.create({
401
+ deviceId: this.activeAddDeviceFlow.responderDeviceId,
402
+ deviceType: this.activeAddDeviceFlow.responderDeviceType,
403
+ publicKey: decryptedPublicKey,
404
+ });
405
+ await this.commandManager.execute(command);
406
+ // all done
407
+ this.activeAddDeviceFlow = undefined;
408
+ }
409
+ async importInitialVaultState(encryptedVaultState, encryptedPublicKey) {
410
+ if (this.activeAddDeviceFlow?.state !== 'responder:syncKeyCreated') {
411
+ throw new SyncInWrongStateError(`Expected responder:syncKeyCreated, got ${this.activeAddDeviceFlow?.state}`);
412
+ }
413
+ const syncKey = this.activeAddDeviceFlow.syncKey;
414
+ // Decrypt the received public key
415
+ const decryptedPublicKey = await this.cryptoLib.decryptSymmetric(syncKey, encryptedPublicKey);
416
+ const vaultState = JSON.parse(await this.cryptoLib.decryptSymmetric(syncKey, encryptedVaultState));
417
+ if (vaultState.deviceId !== this.activeAddDeviceFlow.initiatorDeviceId) {
418
+ throw new SyncError(`DeviceId mismatch when importing, expected ${this.activeAddDeviceFlow.initiatorDeviceId} got ${vaultState.deviceId}`);
419
+ }
420
+ this.syncDevices = vaultState.sync.devices;
421
+ this.mediator
422
+ .getComponent('vaultDataManager')
423
+ .replaceVault(vaultState.vault);
424
+ // Update the sync devices list with the initiator's information
425
+ // Not done as a command as all other devices already have the senders info
426
+ await this.addSyncDevice({
427
+ deviceId: this.activeAddDeviceFlow.initiatorDeviceId,
428
+ deviceType: this.activeAddDeviceFlow.initiatorDeviceType,
429
+ publicKey: decryptedPublicKey,
430
+ });
431
+ // Reset the active add device flow
432
+ this.activeAddDeviceFlow = undefined;
433
+ this.dispatchLibEvent(TwoFaLibEvent.ConnectToExistingVaultFinished);
434
+ }
435
+ /**
436
+ * Cancels the active add sync device flow.
437
+ * @throws {SyncNoServerConnectionError} If there is no server connection.
438
+ * @throws {SyncInWrongStateError} If there is no active add device flow.
439
+ */
440
+ cancelAddSyncDevice() {
441
+ if (!this.ws || !this.webSocketConnected) {
442
+ throw new SyncNoServerConnectionError();
443
+ }
444
+ if (!this.activeAddDeviceFlow) {
445
+ throw new SyncInWrongStateError('Trying to cancel addSyncDevice while not active');
446
+ }
447
+ this.sendToServer('addSyncDeviceCancelled', {
448
+ initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
449
+ });
450
+ // Reset the active add device flow
451
+ this.activeAddDeviceFlow = undefined;
452
+ this.dispatchLibEvent(TwoFaLibEvent.ConnectToExistingVaultFinished);
453
+ }
454
+ /**
455
+ * Sends a command to the server to synchronize with other devices.
456
+ * @param command - The command to be sent.
457
+ * @throws {SyncNoServerConnectionError} If there is no server connection.
458
+ */
459
+ async sendCommand(command) {
460
+ const commandJson = command.toJSON();
461
+ await Promise.all(this.syncDevices.map(async (device) => {
462
+ const symmetricKey = await this.cryptoLib.createSymmetricKey();
463
+ const encryptedSymmetricKey = await this.cryptoLib.encrypt(device.publicKey, symmetricKey);
464
+ const encryptedCommand = await this.cryptoLib.encryptSymmetric(symmetricKey, JSON.stringify({
465
+ ...commandJson,
466
+ id: undefined,
467
+ padding: generateNonCryptographicRandomString(), // make it harder to guess the length
468
+ }));
469
+ this.commandSendQueue.push({
470
+ commandId: command.id,
471
+ deviceId: device.deviceId,
472
+ encryptedSymmetricKey,
473
+ encryptedCommand,
474
+ });
475
+ }));
476
+ await this.processCommandSendQueue();
477
+ }
478
+ async processCommandSendQueue() {
479
+ if (this.syncDevices.length === 0) {
480
+ // no devices to sync with, no need to send anything
481
+ this.commandSendQueue = [];
482
+ return;
483
+ }
484
+ if (!this.ws || !this.webSocketConnected) {
485
+ // not possible to process commands at this point
486
+ this.log('warning', 'Could not sync commands, no server connection, will retry later.');
487
+ return;
488
+ }
489
+ if (this.commandSendQueue.length === 0) {
490
+ // no commands to sync
491
+ return;
492
+ }
493
+ this.sendToServer('syncCommands', {
494
+ nonce: await this.getNonce(),
495
+ commands: this.commandSendQueue,
496
+ });
497
+ }
498
+ /**
499
+ * Handles the confirmation that the sever succesfully received (some) send commands
500
+ * @param commandIds - The ids of the received commands
501
+ */
502
+ commandsSuccesfullyReceived(commandIds) {
503
+ // remove all succesfully received commands frm the queue
504
+ this.commandSendQueue = this.commandSendQueue.filter((command) => !commandIds.includes(command.commandId));
505
+ }
506
+ /**
507
+ * Receives and processes commands from other devices.
508
+ * @param encryptedCommands - The commands
509
+ * @throws {CryptoError} If decryption fails.
510
+ */
511
+ async receiveCommands(encryptedCommands) {
512
+ await Promise.all(encryptedCommands.map(async (data) => {
513
+ const symmetricKey = await this.cryptoLib.decrypt(this.privateKey, data.encryptedSymmetricKey);
514
+ const command = JSON.parse(await this.cryptoLib.decryptSymmetric(symmetricKey, data.encryptedCommand));
515
+ this.commandManager.receiveRemoteCommand({
516
+ ...command,
517
+ id: data.commandId,
518
+ });
519
+ }));
520
+ const commandsExecutedIds = await this.commandManager.processRemoteCommands();
521
+ // if this was the first time we received commands,
522
+ // we can signal that we're done loading after the commands where processed
523
+ if (!this.readyEventEmitted) {
524
+ this.readyEventEmitted = true;
525
+ this.dispatchLibEvent(TwoFaLibEvent.Ready);
526
+ }
527
+ if (commandsExecutedIds.length > 0) {
528
+ this.sendToServer('syncCommandsExecuted', {
529
+ commandIds: commandsExecutedIds,
530
+ });
531
+ }
532
+ }
533
+ /**
534
+ * Add a sync device
535
+ * @param deviceInfo - The info about the device
536
+ */
537
+ async addSyncDevice(deviceInfo) {
538
+ if (deviceInfo.deviceId === this.deviceId) {
539
+ // This is the command that adds this device to all sync devices
540
+ // besides the sender after the device add flow. We don't want to
541
+ // add ourselves to our syncDevices
542
+ return;
543
+ }
544
+ this.log('info', `Adding syncdevice ${deviceInfo.deviceId} to ${this.deviceId}`);
545
+ this.syncDevices.push(deviceInfo);
546
+ await this.persistentStorageManager.save();
547
+ }
548
+ /**
549
+ * Function to call when the server connection should be closed
550
+ */
551
+ closeServerConnection() {
552
+ this.shouldReconnect = false;
553
+ if (this.reconnectTimeout) {
554
+ clearTimeout(this.reconnectTimeout);
555
+ this.reconnectTimeout = undefined;
556
+ }
557
+ if (this.ws) {
558
+ const ws = this.ws;
559
+ this.ws = undefined;
560
+ ws.close();
561
+ // force terminate the connection after 5 seconds
562
+ // ws.terminate is not defined in the test enviroment, which is the reason for the &&
563
+ setTimeout(() => ws.terminate && ws.terminate(), 5000);
564
+ }
565
+ }
566
+ }
567
+ export default SyncManager;
@@ -0,0 +1,68 @@
1
+ import type Entry from '../interfaces/Entry.mjs';
2
+ import type { EntryId, Token } from '../interfaces/Entry.mjs';
3
+ import type TwoFaLibMediator from '../TwoFaLibMediator.mjs';
4
+ import { Vault } from '../interfaces/Vault.mjs';
5
+ /**
6
+ * Manages the data within the vault. This class should only be used internally
7
+ * by the library, for public methods, see VaultOperationsManager.
8
+ */
9
+ declare class VaultDataManager {
10
+ private readonly mediator;
11
+ private vault;
12
+ /**
13
+ * Constructs a new VaultDataManager instance.
14
+ * @param mediator - The mediator for accessing other components.
15
+ */
16
+ constructor(mediator: TwoFaLibMediator);
17
+ private get persistentStorageManager();
18
+ /**
19
+ * @returns The number of entries in the vault.
20
+ */
21
+ get size(): number;
22
+ /**
23
+ * Retrieve a specific entry.
24
+ * @param entryId - The unique identifier of the entry.
25
+ * @returns The entry.
26
+ * @throws {EntryNotFoundError} If no entry exists with the given ID.
27
+ */
28
+ getFullEntry(entryId: EntryId): Entry;
29
+ /**
30
+ * Retrieve all entries in the vault.
31
+ * @returns An array of all entries.
32
+ */
33
+ getAllEntries(): Vault;
34
+ /**
35
+ * Generate a time-based one-time password (TOTP) for a specific entry.
36
+ * @param id - The unique identifier of the entry.
37
+ * @param timestamp - Optional timestamp to use for token generation (default is current time).
38
+ * @returns An object containing the token and the validity period.
39
+ * @throws {EntryNotFoundError} If no entry exists with the given ID.
40
+ * @throws {TokenGenerationError} If token generation fails due to invalid entry data or technical issues.
41
+ */
42
+ generateTokenForEntry(id: EntryId, timestamp?: number): Token;
43
+ /**
44
+ * Add a new entry to the vault.
45
+ * @param entry - The entry data to add (without an ID, as it will be generated).
46
+ * @returns A promise that resolves when the entry is added.
47
+ */
48
+ addEntry(entry: Entry): Promise<void>;
49
+ /**
50
+ * Delete an entry from the vault.
51
+ * @param entryId - The identifier of the entry to delete.
52
+ * @throws {EntryNotFoundError} If no entry exists with the given ID.
53
+ */
54
+ deleteEntry(entryId: EntryId): Promise<void>;
55
+ /**
56
+ * Update an existing entry in the vault.
57
+ * @param updatedEntry - An object containing the updated entry.
58
+ * @returns A promise that resolves when the entry is updated.
59
+ * @throws {EntryNotFoundError} If no entry exists with the given ID.
60
+ */
61
+ updateEntry(updatedEntry: Entry): Promise<void>;
62
+ /**
63
+ * Replace the current vault with a new one.
64
+ * @param newVault - The new vault data to replace the existing vault.
65
+ */
66
+ replaceVault(newVault: Vault): void;
67
+ }
68
+ export default VaultDataManager;