favalib 0.0.4 → 0.0.5

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.
@@ -30,9 +30,9 @@ declare abstract class BaseCommand<T extends CommandData = CommandData> {
30
30
  /**
31
31
  * Creates an undo command that, when executed, reverses the effects of this command.
32
32
  * @param VaultDataManager - The TwoFaLibMediator instance to use for creating the undo command.
33
- * @returns A BaseCommand instance that undoes this command.
33
+ * @returns A BaseCommand instance that undoes this command or false if this command has no undo.
34
34
  */
35
- abstract createUndoCommand(TwoFaLibMediator: TwoFaLibMediator): BaseCommand;
35
+ abstract createUndoCommand(TwoFaLibMediator: TwoFaLibMediator): BaseCommand | false;
36
36
  /**
37
37
  * Creates a new instance of the command with the provided data.
38
38
  * @param data - The data to use for creating the new command instance.
@@ -2,10 +2,12 @@ import AddEntryCommand from './commands/AddEntryCommand.mjs';
2
2
  import AddSyncDeviceCommand from './commands/AddSyncDeviceCommand.mjs';
3
3
  import DeleteEntryCommand from './commands/DeleteEntryCommand.mjs';
4
4
  import UpdateEntryCommand from './commands/UpdateEntryCommand.mjs';
5
+ import ChangeSyncDeviceMetaCommand from './commands/ChangeSyncDeviceMetaCommand.mjs';
5
6
  declare const commandConstructors: {
6
7
  AddEntry: typeof AddEntryCommand;
7
8
  DeleteEntry: typeof DeleteEntryCommand;
8
9
  UpdateEntry: typeof UpdateEntryCommand;
9
10
  AddSyncDevice: typeof AddSyncDeviceCommand;
11
+ ChangeSyncDeviceMeta: typeof ChangeSyncDeviceMetaCommand;
10
12
  };
11
13
  export default commandConstructors;
@@ -2,10 +2,12 @@ import AddEntryCommand from './commands/AddEntryCommand.mjs';
2
2
  import AddSyncDeviceCommand from './commands/AddSyncDeviceCommand.mjs';
3
3
  import DeleteEntryCommand from './commands/DeleteEntryCommand.mjs';
4
4
  import UpdateEntryCommand from './commands/UpdateEntryCommand.mjs';
5
+ import ChangeSyncDeviceMetaCommand from './commands/ChangeSyncDeviceMetaCommand.mjs';
5
6
  const commandConstructors = {
6
7
  AddEntry: AddEntryCommand,
7
8
  DeleteEntry: DeleteEntryCommand,
8
9
  UpdateEntry: UpdateEntryCommand,
9
10
  AddSyncDevice: AddSyncDeviceCommand,
11
+ ChangeSyncDeviceMeta: ChangeSyncDeviceMetaCommand,
10
12
  };
11
13
  export default commandConstructors;
@@ -1,10 +1,9 @@
1
1
  import type TwoFaLibMediator from '../../TwoFaLibMediator.mjs';
2
2
  import Command from '../BaseCommand.mjs';
3
- import type { DeviceId, DeviceType } from '../../interfaces/SyncTypes.mjs';
3
+ import type { DeviceId } from '../../interfaces/SyncTypes.mjs';
4
4
  import type { PublicKey } from '../../interfaces/CryptoLib.mjs';
5
5
  export interface AddSyncDeviceData {
6
6
  deviceId: DeviceId;
7
- deviceType: DeviceType;
8
7
  publicKey: PublicKey;
9
8
  }
10
9
  /**
@@ -12,13 +11,13 @@ export interface AddSyncDeviceData {
12
11
  */
13
12
  declare class AddSyncDeviceCommand extends Command<AddSyncDeviceData> {
14
13
  /**
15
- * Creates a new AddEntryCommand instance.
14
+ * Creates a new AddSyncDeviceCommand instance.
16
15
  * @inheritdoc
17
16
  * @param data - The data of the entry to be added.
18
17
  */
19
18
  constructor(data: AddSyncDeviceData, id?: string, timestamp?: number, version?: string, fromRemote?: boolean);
20
19
  /**
21
- * Executes the command to add the entry to the vault.
20
+ * Executes the command to add a sync device
22
21
  * @inheritdoc
23
22
  * @throws {InvalidCommandError} If the command data is invalid.
24
23
  */
@@ -5,7 +5,7 @@ import Command from '../BaseCommand.mjs';
5
5
  */
6
6
  class AddSyncDeviceCommand extends Command {
7
7
  /**
8
- * Creates a new AddEntryCommand instance.
8
+ * Creates a new AddSyncDeviceCommand instance.
9
9
  * @inheritdoc
10
10
  * @param data - The data of the entry to be added.
11
11
  */
@@ -13,7 +13,7 @@ class AddSyncDeviceCommand extends Command {
13
13
  super('AddSyncDevice', data, id, timestamp, version, fromRemote);
14
14
  }
15
15
  /**
16
- * Executes the command to add the entry to the vault.
16
+ * Executes the command to add a sync device
17
17
  * @inheritdoc
18
18
  * @throws {InvalidCommandError} If the command data is invalid.
19
19
  */
@@ -0,0 +1,32 @@
1
+ import type TwoFaLibMediator from '../../TwoFaLibMediator.mjs';
2
+ import Command from '../BaseCommand.mjs';
3
+ import type { DeviceFriendlyName, DeviceId, DeviceType } from '../../interfaces/SyncTypes.mjs';
4
+ export interface ChangeSyncDeviceMetaData {
5
+ deviceId: DeviceId;
6
+ newMeta: {
7
+ deviceFriendlyName: DeviceFriendlyName;
8
+ deviceType: DeviceType;
9
+ };
10
+ }
11
+ /**
12
+ * Represents a command that when executed changes the meta info of a sync device
13
+ */
14
+ declare class ChangeSyncDeviceMetaCommand extends Command<ChangeSyncDeviceMetaData> {
15
+ /**
16
+ * Creates a new ChangeSyncDeviceMetaCommand instance.
17
+ * @inheritdoc
18
+ * @param data - The id of the device to change and the new meta info
19
+ */
20
+ constructor(data: ChangeSyncDeviceMetaData, id?: string, timestamp?: number, version?: string, fromRemote?: boolean);
21
+ /**
22
+ * Executes the command to change the sync device metadata
23
+ * @inheritdoc
24
+ * @throws {InvalidCommandError} If the referenced sync device cannot be found
25
+ */
26
+ execute(mediator: TwoFaLibMediator): Promise<void>;
27
+ /**
28
+ * @inheritdoc
29
+ */
30
+ createUndoCommand(): Command;
31
+ }
32
+ export default ChangeSyncDeviceMetaCommand;
@@ -0,0 +1,37 @@
1
+ import { InvalidCommandError, TwoFALibError } from '../../TwoFALibError.mjs';
2
+ import Command from '../BaseCommand.mjs';
3
+ /**
4
+ * Represents a command that when executed changes the meta info of a sync device
5
+ */
6
+ class ChangeSyncDeviceMetaCommand extends Command {
7
+ /**
8
+ * Creates a new ChangeSyncDeviceMetaCommand instance.
9
+ * @inheritdoc
10
+ * @param data - The id of the device to change and the new meta info
11
+ */
12
+ constructor(data, id, timestamp, version, fromRemote = false) {
13
+ super('ChangeSyncDeviceMeta', data, id, timestamp, version, fromRemote);
14
+ }
15
+ /**
16
+ * Executes the command to change the sync device metadata
17
+ * @inheritdoc
18
+ * @throws {InvalidCommandError} If the referenced sync device cannot be found
19
+ */
20
+ execute(mediator) {
21
+ const syncManager = mediator.getComponent('syncManager');
22
+ // eslint-disable-next-line @typescript-eslint/dot-notation
23
+ const device = syncManager['syncDevices'].find((d) => d.deviceId === this.data.deviceId);
24
+ if (!device) {
25
+ throw new InvalidCommandError('Trying to change meta of device that is not found');
26
+ }
27
+ device.meta = this.data.newMeta;
28
+ return Promise.resolve();
29
+ }
30
+ /**
31
+ * @inheritdoc
32
+ */
33
+ createUndoCommand() {
34
+ throw new TwoFALibError('Not implemented yet');
35
+ }
36
+ }
37
+ export default ChangeSyncDeviceMetaCommand;
@@ -1,7 +1,7 @@
1
1
  import { TypedEventTarget } from 'typescript-event-target';
2
2
  import type CryptoLib from './interfaces/CryptoLib.mjs';
3
3
  import type { EncryptedPrivateKey, EncryptedSymmetricKey, Passphrase, PrivateKey, PublicKey, Salt, SymmetricKey } from './interfaces/CryptoLib.mjs';
4
- import type { DeviceId, DeviceType } from './interfaces/SyncTypes.mjs';
4
+ import type { DeviceFriendlyName, DeviceId, DeviceType } from './interfaces/SyncTypes.mjs';
5
5
  import type { TwoFaLibEventMapEvents } from './interfaces/Events.mjs';
6
6
  import type { PassphraseExtraDict } from './interfaces/PassphraseExtraDict.js';
7
7
  import type { Vault, VaultSyncState } from './interfaces/Vault.mjs';
@@ -15,13 +15,14 @@ declare class TwoFaLib extends TypedEventTarget<TwoFaLibEventMapEvents> {
15
15
  static readonly version = "0.0.1";
16
16
  readonly deviceId: DeviceId;
17
17
  readonly deviceType: DeviceType;
18
+ deviceFriendlyName: DeviceFriendlyName;
18
19
  private mediator;
19
20
  private readonly publicKey;
20
21
  private readonly privateKey;
21
22
  readonly ready: Promise<unknown>;
22
23
  /**
23
24
  * Constructs a new instance of TwoFaLib. If a serverUrl is provided, the library will use it for its sync operations.
24
- * @param deviceType - A unique identifier for this device type (e.g. 2fa-cli).
25
+ * @param deviceType - The identifier for this device type (e.g. 2fa-cli).
25
26
  * @param cryptoLib - An instance of CryptoLib that is compatible with the environment.
26
27
  * @param passphraseExtraDict - Additional words to be used for passphrase strength evaluation.
27
28
  * @param privateKey - The private key used for cryptographic operations.
@@ -17,7 +17,7 @@ class TwoFaLib extends TypedEventTarget {
17
17
  static { this.version = '0.0.1'; }
18
18
  /**
19
19
  * Constructs a new instance of TwoFaLib. If a serverUrl is provided, the library will use it for its sync operations.
20
- * @param deviceType - A unique identifier for this device type (e.g. 2fa-cli).
20
+ * @param deviceType - The identifier for this device type (e.g. 2fa-cli).
21
21
  * @param cryptoLib - An instance of CryptoLib that is compatible with the environment.
22
22
  * @param passphraseExtraDict - Additional words to be used for passphrase strength evaluation.
23
23
  * @param privateKey - The private key used for cryptographic operations.
@@ -35,6 +35,7 @@ class TwoFaLib extends TypedEventTarget {
35
35
  */
36
36
  constructor(deviceType, cryptoLib, passphraseExtraDict, privateKey, symmetricKey, encryptedPrivateKey, encryptedSymmetricKey, salt, publicKey, deviceId, vault, syncState) {
37
37
  super();
38
+ this.deviceFriendlyName = '';
38
39
  if (!deviceType) {
39
40
  throw new InitializationError('Device type is required');
40
41
  }
@@ -73,7 +74,7 @@ class TwoFaLib extends TypedEventTarget {
73
74
  }
74
75
  if (syncState?.serverUrl) {
75
76
  // Initiate the syncManager
76
- this.mediator.registerComponent('syncManager', new SyncManager(this.mediator, this.deviceType, this.publicKey, this.privateKey, syncState, this.deviceId));
77
+ this.mediator.registerComponent('syncManager', new SyncManager(this.mediator, this.publicKey, this.privateKey, syncState, this.deviceId));
77
78
  }
78
79
  else {
79
80
  // If no syncmanager we're ready now, otherwise the syncmanager is responsible for emitting the ready event
@@ -152,7 +153,7 @@ class TwoFaLib extends TypedEventTarget {
152
153
  devices: [],
153
154
  commandSendQueue: [],
154
155
  };
155
- const newSyncManager = new SyncManager(this.mediator, this.deviceType, this.publicKey, this.privateKey, newSyncState, this.deviceId);
156
+ const newSyncManager = new SyncManager(this.mediator, this.publicKey, this.privateKey, newSyncState, this.deviceId);
156
157
  const success = await new Promise((resolve) => {
157
158
  this.addEventListener(TwoFaLibEvent.ConnectionToSyncServerStatusChanged, (event) => {
158
159
  if (event.detail.newStatus === ConnectionStatus.CONNECTED) {
@@ -176,9 +177,11 @@ class TwoFaLib extends TypedEventTarget {
176
177
  throw new SyncError(`Failed to connect to server at ${serverUrl}, not setting`);
177
178
  }
178
179
  }
179
- // connection succeeded (or force=true), switch to the new syncManager
180
+ // connection succeeded (or force=true)
181
+ // switch to the new syncManager
180
182
  this.mediator.unRegisterComponent('syncManager');
181
183
  this.mediator.registerComponent('syncManager', newSyncManager);
184
+ // save
182
185
  await this.forceSave();
183
186
  }
184
187
  /**
@@ -2,6 +2,7 @@ import type { AddEntryData } from '../Command/commands/AddEntryCommand.mjs';
2
2
  import type { DeleteEntryData } from '../Command/commands/DeleteEntryCommand.mjs';
3
3
  import type { UpdateEntryData } from '../Command/commands/UpdateEntryCommand.mjs';
4
4
  import type { AddSyncDeviceData } from '../Command/commands/AddSyncDeviceCommand.mjs';
5
+ import type { ChangeSyncDeviceMetaData } from '../Command/commands/ChangeSyncDeviceMetaCommand.mjs';
5
6
  export type SyncCommand = ({
6
7
  type: 'AddEntry';
7
8
  data: AddEntryData;
@@ -14,7 +15,10 @@ export type SyncCommand = ({
14
15
  } | {
15
16
  type: 'AddSyncDevice';
16
17
  data: AddSyncDeviceData;
18
+ } | {
19
+ type: 'ChangeSyncDeviceMeta';
20
+ data: ChangeSyncDeviceMetaData;
17
21
  }) & {
18
22
  id: string;
19
23
  };
20
- export type CommandData = AddEntryData | DeleteEntryData | UpdateEntryData | AddSyncDeviceData;
24
+ export type CommandData = AddEntryData | DeleteEntryData | UpdateEntryData | AddSyncDeviceData | ChangeSyncDeviceMetaData;
@@ -3,11 +3,16 @@ import type { JPakeThreePass, Round1Result } from 'jpake-ts';
3
3
  import { PublicKey, SyncKey } from './CryptoLib.mjs';
4
4
  export type DeviceId = Tagged<string, 'DeviceId'>;
5
5
  export type DeviceType = Tagged<string, 'DeviceType'>;
6
+ export type DeviceFriendlyName = Tagged<string, 'DeviceFriendlyName'>;
6
7
  export interface SyncDevice {
7
8
  deviceId: DeviceId;
8
- deviceType: DeviceType;
9
9
  publicKey: PublicKey;
10
+ meta?: {
11
+ deviceType: DeviceType;
12
+ deviceFriendlyName: DeviceFriendlyName;
13
+ };
10
14
  }
15
+ export type PublicSyncDevice = Omit<SyncDevice, 'publicKey'>;
11
16
  export interface BaseAddDeviceFlow {
12
17
  jpak: JPakeThreePass;
13
18
  addDevicePassword: Uint8Array;
@@ -22,14 +27,12 @@ export interface AddDeviceFlowInitiator_Initiated extends BaseAddDeviceFlow {
22
27
  export interface AddDeviceFlowInitiator_SyncKeyCreated extends Omit<AddDeviceFlowInitiator_Initiated, 'state' | 'resolveContinuePromise'> {
23
28
  state: 'initiator:syncKeyCreated';
24
29
  responderDeviceId: DeviceId;
25
- responderDeviceType: DeviceType;
26
30
  syncKey: SyncKey;
27
31
  }
28
32
  export interface AddDeviceFlowResponder_Initiated extends BaseAddDeviceFlow {
29
33
  state: 'responder:initated';
30
- initiatorDeviceId: DeviceId;
31
34
  responderDeviceId: DeviceId;
32
- initiatorDeviceType: DeviceType;
35
+ initiatorDeviceId: DeviceId;
33
36
  }
34
37
  export interface AddDeviceFlowResponder_SyncKeyCreated extends Omit<AddDeviceFlowResponder_Initiated, 'state'> {
35
38
  state: 'responder:syncKeyCreated';
@@ -39,7 +42,6 @@ export type ActiveAddDeviceFlow = AddDeviceFlowInitiator_Initiated | AddDeviceFl
39
42
  export interface InitiateAddDeviceFlowResult {
40
43
  addDevicePassword: string;
41
44
  initiatorDeviceId: DeviceId;
42
- initiatorDeviceType: DeviceType;
43
45
  timestamp: number;
44
46
  pass1Result: Record<keyof Round1Result, string>;
45
47
  }
package/build/main.d.mts CHANGED
@@ -3,10 +3,10 @@ import type Entry from './interfaces/Entry.mjs';
3
3
  import type { EntryId, NewEntry, EntryMeta, EntryType, TotpPayload, Token, EntryMetaWithToken } from './interfaces/Entry.mjs';
4
4
  import type CryptoLib from './interfaces/CryptoLib.mjs';
5
5
  import type { Encrypted, EncryptedPrivateKey, EncryptedSymmetricKey, EncryptedPublicKey, PrivateKey, SymmetricKey, PublicKey, Passphrase, Salt } from './interfaces/CryptoLib.mjs';
6
- import type { SyncDevice, DeviceId, DeviceType } from './interfaces/SyncTypes.mjs';
6
+ import type { PublicSyncDevice, DeviceId, DeviceType, DeviceFriendlyName } from './interfaces/SyncTypes.mjs';
7
7
  import type { EncryptedVaultStateString, LockedRepresentationString } from './interfaces/Vault.mjs';
8
8
  import { TwoFALibError, InitializationError, AuthenticationError, EntryNotFoundError, TokenGenerationError } from './TwoFALibError.mjs';
9
9
  import { TwoFaLibEvent } from './TwoFaLibEvent.mjs';
10
10
  import { getTwoFaLibVaultCreationUtils } from './utils/creationUtils.mjs';
11
11
  export { TwoFaLib, TwoFALibError, getTwoFaLibVaultCreationUtils, InitializationError, AuthenticationError, EntryNotFoundError, TokenGenerationError, TwoFaLibEvent, };
12
- export type { Entry, EntryId, NewEntry, EntryMeta, EntryMetaWithToken, EntryType, TotpPayload, Token, EncryptedVaultStateString, LockedRepresentationString, CryptoLib, Encrypted, EncryptedPrivateKey, EncryptedPublicKey, EncryptedSymmetricKey, PrivateKey, SymmetricKey, PublicKey, Passphrase, Salt, DeviceId, DeviceType, SyncDevice, };
12
+ export type { Entry, EntryId, NewEntry, EntryMeta, EntryMetaWithToken, EntryType, TotpPayload, Token, EncryptedVaultStateString, LockedRepresentationString, CryptoLib, Encrypted, EncryptedPrivateKey, EncryptedPublicKey, EncryptedSymmetricKey, PrivateKey, SymmetricKey, PublicKey, Passphrase, Salt, DeviceId, DeviceType, DeviceFriendlyName, PublicSyncDevice, };
@@ -49,8 +49,15 @@ class CommandManager {
49
49
  const command = this.executedCommands.pop();
50
50
  if (command) {
51
51
  const undoCommand = command.createUndoCommand(this.mediator);
52
- await undoCommand.execute(this.mediator);
53
- this.undoneCommands.push(command);
52
+ // check if the last command was undoable
53
+ if (undoCommand) {
54
+ await undoCommand.execute(this.mediator);
55
+ this.undoneCommands.push(command);
56
+ }
57
+ else {
58
+ // if it was not, skip it
59
+ await this.undo();
60
+ }
54
61
  }
55
62
  }
56
63
  /**
@@ -55,7 +55,8 @@ class PersistentStorageManager {
55
55
  vault,
56
56
  deviceId: this.deviceId,
57
57
  sync: {
58
- devices: this.syncManager?.syncDevices ?? [],
58
+ // eslint-disable-next-line @typescript-eslint/dot-notation
59
+ devices: this.syncManager ? this.syncManager['syncDevices'] : [],
59
60
  serverUrl: this.syncManager?.serverUrl,
60
61
  commandSendQueue: this.syncManager?.getCommandSendQueue() ?? [],
61
62
  },
@@ -1,11 +1,10 @@
1
- import { SyncDevice, DeviceId, DeviceType } from '../interfaces/SyncTypes.mjs';
1
+ import { SyncDevice, DeviceId, PublicSyncDevice } from '../interfaces/SyncTypes.mjs';
2
2
  import type { PrivateKey, PublicKey } from '../interfaces/CryptoLib.mjs';
3
3
  import type Command from '../Command/BaseCommand.mjs';
4
4
  import type TwoFaLibMediator from '../TwoFaLibMediator.mjs';
5
5
  import { VaultSyncStateWithServerUrl } from '../interfaces/Vault.mjs';
6
6
  import { SyncCommandFromServer } from 'favaserver/ServerMessage';
7
7
  import { SyncCommandFromClient } from 'favaserver/ClientMessage';
8
- import type { AddSyncDeviceData } from '../Command/commands/AddSyncDeviceCommand.mjs';
9
8
  export declare enum ConnectionStatus {
10
9
  CONNECTING = 0,
11
10
  CONNECTED = 1,
@@ -17,14 +16,13 @@ export declare enum ConnectionStatus {
17
16
  */
18
17
  declare class SyncManager {
19
18
  private readonly mediator;
20
- private readonly deviceType;
21
19
  private readonly publicKey;
22
20
  private readonly privateKey;
23
21
  private ws?;
24
22
  private activeAddDeviceFlow?;
25
23
  private readonly reconnectInterval;
26
24
  readonly serverUrl: string;
27
- syncDevices: SyncDevice[];
25
+ private syncDevices;
28
26
  deviceId: DeviceId;
29
27
  private readyEventEmitted;
30
28
  private commandSendQueue;
@@ -37,17 +35,21 @@ declare class SyncManager {
37
35
  * @returns The command send queue.
38
36
  */
39
37
  getCommandSendQueue(): SyncCommandFromClient[];
38
+ /**
39
+ * Public getter for the sync devices
40
+ * @returns The sync devices (without their public key)
41
+ */
42
+ getSyncDevices(): PublicSyncDevice[];
40
43
  /**
41
44
  * Creates an instance of SyncManager.
42
45
  * @param mediator - The mediator for accessing other components.
43
- * @param deviceType - The type of the device.
44
46
  * @param publicKey - The public key of the device.
45
47
  * @param privateKey - The private key of the device.
46
48
  * @param syncState - The state of the sync.
47
49
  * @param deviceId - The unique identifier of the device.
48
50
  * @throws {InitializationError} If initialization fails (e.g., if the server URL is invalid).
49
51
  */
50
- constructor(mediator: TwoFaLibMediator, deviceType: DeviceType, publicKey: PublicKey, privateKey: PrivateKey, syncState: VaultSyncStateWithServerUrl, deviceId: DeviceId);
52
+ constructor(mediator: TwoFaLibMediator, publicKey: PublicKey, privateKey: PrivateKey, syncState: VaultSyncStateWithServerUrl, deviceId: DeviceId);
51
53
  private get libraryLoader();
52
54
  private get cryptoLib();
53
55
  private get persistentStorageManager();
@@ -130,8 +132,9 @@ declare class SyncManager {
130
132
  respondToAddDeviceFlow(initiatorData: string | Uint8Array | File, initiatorDataType: 'text' | 'qr'): Promise<void>;
131
133
  private finishAddDeviceFlowKeyExchangeInitiator;
132
134
  private finishAddDeviceFlowKeyExchangeResponder;
133
- private sendInitialVaultData;
135
+ private sendFullVaultData;
134
136
  private importInitialVaultState;
137
+ private importVaultState;
135
138
  /**
136
139
  * Cancels the active add sync device flow.
137
140
  * @throws {SyncNoServerConnectionError} If there is no server connection.
@@ -156,11 +159,20 @@ declare class SyncManager {
156
159
  * @throws {CryptoError} If decryption fails.
157
160
  */
158
161
  receiveCommands(encryptedCommands: SyncCommandFromServer[]): Promise<void>;
162
+ /**
163
+ * Sends vault data to the server for each sync device
164
+ */
165
+ private resilver;
159
166
  /**
160
167
  * Add a sync device
161
168
  * @param deviceInfo - The info about the device
169
+ * @param saveAfter - Whether to save the new vault after adding it (set to false when adding multiple devices)
170
+ */
171
+ addSyncDevice(deviceInfo: SyncDevice, saveAfter?: boolean): Promise<void>;
172
+ /**
173
+ * Requests a resilver of the vault
162
174
  */
163
- addSyncDevice(deviceInfo: AddSyncDeviceData): Promise<void>;
175
+ requestResilver(): void;
164
176
  /**
165
177
  * Function to call when the server connection should be closed
166
178
  */
@@ -30,19 +30,29 @@ class SyncManager {
30
30
  getCommandSendQueue() {
31
31
  return this.commandSendQueue;
32
32
  }
33
+ /**
34
+ * Public getter for the sync devices
35
+ * @returns The sync devices (without their public key)
36
+ */
37
+ getSyncDevices() {
38
+ return this.syncDevices
39
+ .filter((d) => d.deviceId !== this.deviceId)
40
+ .map((d) => ({
41
+ deviceId: d.deviceId,
42
+ meta: d.meta,
43
+ }));
44
+ }
33
45
  /**
34
46
  * Creates an instance of SyncManager.
35
47
  * @param mediator - The mediator for accessing other components.
36
- * @param deviceType - The type of the device.
37
48
  * @param publicKey - The public key of the device.
38
49
  * @param privateKey - The private key of the device.
39
50
  * @param syncState - The state of the sync.
40
51
  * @param deviceId - The unique identifier of the device.
41
52
  * @throws {InitializationError} If initialization fails (e.g., if the server URL is invalid).
42
53
  */
43
- constructor(mediator, deviceType, publicKey, privateKey, syncState, deviceId) {
54
+ constructor(mediator, publicKey, privateKey, syncState, deviceId) {
44
55
  this.mediator = mediator;
45
- this.deviceType = deviceType;
46
56
  this.publicKey = publicKey;
47
57
  this.privateKey = privateKey;
48
58
  this.reconnectInterval = IN_TESTING ? 100 : 5000; // 5 seconds
@@ -60,6 +70,11 @@ class SyncManager {
60
70
  this.commandSendQueue = commandSendQueue;
61
71
  this.serverUrl = serverUrl;
62
72
  this.initServerConnection();
73
+ // add ourselves to the list of syncdevices if we're missing
74
+ void this.addSyncDevice({
75
+ deviceId: deviceId,
76
+ publicKey: this.publicKey,
77
+ }, false);
63
78
  // if not yet connected after 2 tries, emit ready event so we can continue
64
79
  this.connectionFailedTimeout = setTimeout(() => {
65
80
  if (!this.readyEventEmitted && !this.webSocketConnected) {
@@ -186,7 +201,7 @@ class SyncManager {
186
201
  round1Result: jsonToUint8Array(unconvertedPass2Result.round1Result),
187
202
  round2Result: jsonToUint8Array(unconvertedPass2Result.round2Result),
188
203
  };
189
- void this.finishAddDeviceFlowKeyExchangeInitiator(pass2Result, data.responderDeviceId, data.responderDeviceType);
204
+ void this.finishAddDeviceFlowKeyExchangeInitiator(pass2Result, data.responderDeviceId);
190
205
  break;
191
206
  }
192
207
  case 'JPAKEPass3': {
@@ -198,13 +213,21 @@ class SyncManager {
198
213
  case 'publicKey': {
199
214
  const { data } = message;
200
215
  const { responderEncryptedPublicKey } = data;
201
- void this.sendInitialVaultData(responderEncryptedPublicKey);
216
+ void this.sendFullVaultData(responderEncryptedPublicKey);
217
+ break;
218
+ }
219
+ case 'initialVault': {
220
+ const { data } = message;
221
+ const { encryptedVaultData } = data;
222
+ void this.importInitialVaultState(encryptedVaultData);
202
223
  break;
203
224
  }
204
225
  case 'vault': {
205
226
  const { data } = message;
206
- const { encryptedVaultData, initiatorEncryptedPublicKey } = data;
207
- void this.importInitialVaultState(encryptedVaultData, initiatorEncryptedPublicKey);
227
+ const { encryptedVaultData, encryptedSymmetricKey, fromDeviceId } = data;
228
+ void this.cryptoLib
229
+ .decrypt(this.privateKey, encryptedSymmetricKey)
230
+ .then((symmetricKey) => this.importVaultState(encryptedVaultData, symmetricKey, fromDeviceId));
208
231
  break;
209
232
  }
210
233
  case 'syncCommandsReceived': {
@@ -217,6 +240,12 @@ class SyncManager {
217
240
  void this.receiveCommands(commands);
218
241
  break;
219
242
  }
243
+ case 'startResilver': {
244
+ // const { data } = message
245
+ // todo: check for missing deviceIds
246
+ void this.resilver();
247
+ break;
248
+ }
220
249
  }
221
250
  }
222
251
  attemptReconnect() {
@@ -259,7 +288,6 @@ class SyncManager {
259
288
  });
260
289
  // register this add device request at the server
261
290
  this.sendToServer('addSyncDeviceInitialiseData', {
262
- initiatorDeviceType: this.deviceType,
263
291
  initiatorDeviceId: this.deviceId,
264
292
  timestamp,
265
293
  nonce: await this.getNonce(),
@@ -269,7 +297,6 @@ class SyncManager {
269
297
  const returnData = {
270
298
  addDevicePassword: uint8ArrayToBase64(addDevicePassword),
271
299
  initiatorDeviceId: this.deviceId,
272
- initiatorDeviceType: this.deviceType,
273
300
  timestamp,
274
301
  pass1Result: {
275
302
  G1: uint8ArrayToHex(pass1Result.G1),
@@ -306,12 +333,11 @@ class SyncManager {
306
333
  if (this.activeAddDeviceFlow) {
307
334
  throw new SyncAddDeviceFlowConflictError();
308
335
  }
309
- const { addDevicePassword, initiatorDeviceId, timestamp, pass1Result, initiatorDeviceType: initiatorDeviceIdentifier, } = await decodeInitiatorData(initiatorData, initiatorDataType, await this.libraryLoader.getJsQrLib(), this.libraryLoader.getCanvasLib.bind(this));
336
+ const { addDevicePassword, initiatorDeviceId, timestamp, pass1Result } = await decodeInitiatorData(initiatorData, initiatorDataType, await this.libraryLoader.getJsQrLib(), this.libraryLoader.getCanvasLib.bind(this));
310
337
  if (!addDevicePassword ||
311
338
  !initiatorDeviceId ||
312
339
  !timestamp ||
313
- !pass1Result ||
314
- !initiatorDeviceIdentifier) {
340
+ !pass1Result) {
315
341
  throw new SyncError('Missing required fields in initiator data');
316
342
  }
317
343
  // Decode the base64 password
@@ -337,7 +363,6 @@ class SyncManager {
337
363
  addDevicePassword: decodedPassword,
338
364
  responderDeviceId: this.deviceId,
339
365
  initiatorDeviceId: initiatorDeviceId,
340
- initiatorDeviceType: initiatorDeviceIdentifier,
341
366
  timestamp: Date.now(),
342
367
  };
343
368
  // respond to this add device request at the server
@@ -347,10 +372,9 @@ class SyncManager {
347
372
  pass2Result,
348
373
  responderDeviceId: this.deviceId,
349
374
  initiatorDeviceId: initiatorDeviceId,
350
- responderDeviceType: this.deviceType,
351
375
  });
352
376
  }
353
- async finishAddDeviceFlowKeyExchangeInitiator(pass2Result, responderDeviceId, responderDeviceType) {
377
+ async finishAddDeviceFlowKeyExchangeInitiator(pass2Result, responderDeviceId) {
354
378
  if (!this.ws || !this.webSocketConnected) {
355
379
  throw new SyncNoServerConnectionError();
356
380
  }
@@ -370,7 +394,6 @@ class SyncManager {
370
394
  ...this.activeAddDeviceFlow,
371
395
  state: 'initiator:syncKeyCreated',
372
396
  responderDeviceId: responderDeviceId,
373
- responderDeviceType: responderDeviceType,
374
397
  syncKey,
375
398
  };
376
399
  }
@@ -400,7 +423,7 @@ class SyncManager {
400
423
  initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
401
424
  });
402
425
  }
403
- async sendInitialVaultData(responderEncryptedPublicKey) {
426
+ async sendFullVaultData(responderEncryptedPublicKey) {
404
427
  if (!this.ws || !this.webSocketConnected) {
405
428
  throw new SyncNoServerConnectionError();
406
429
  }
@@ -415,51 +438,46 @@ class SyncManager {
415
438
  const decryptedPublicKey = await this.cryptoLib.decryptSymmetric(syncKey, responderEncryptedPublicKey);
416
439
  // get the vault data (encrypted with the sync key)
417
440
  const encryptedVaultData = await this.persistentStorageManager.getEncryptedVaultState(syncKey);
418
- const initiatorEncryptedPublicKey = await this.cryptoLib.encryptSymmetric(syncKey, this.publicKey);
419
441
  // Send the encrypted vault data to the server
420
- this.sendToServer('vault', {
442
+ this.sendToServer('initialVault', {
421
443
  nonce: await this.getNonce(),
422
444
  encryptedVaultData,
423
445
  initiatorDeviceId: this.activeAddDeviceFlow.initiatorDeviceId,
424
- initiatorEncryptedPublicKey,
425
446
  });
426
- // save the added the sync device, done via command so this is synced to
427
- // all the other (already existing) sync devices
447
+ // save the added the sync device, done via command so this is synced to all sync devices
448
+ // this also re-adds the sync device to the just added sync device, which syncDevices list is now equal to our own
428
449
  const command = AddSyncDeviceCommand.create({
429
450
  deviceId: this.activeAddDeviceFlow.responderDeviceId,
430
- deviceType: this.activeAddDeviceFlow.responderDeviceType,
431
451
  publicKey: decryptedPublicKey,
432
452
  });
433
453
  await this.commandManager.execute(command);
434
454
  // all done
435
455
  this.activeAddDeviceFlow = undefined;
436
456
  }
437
- async importInitialVaultState(encryptedVaultState, encryptedPublicKey) {
457
+ async importInitialVaultState(encryptedVaultState) {
438
458
  if (this.activeAddDeviceFlow?.state !== 'responder:syncKeyCreated') {
439
459
  throw new SyncInWrongStateError(`Expected responder:syncKeyCreated, got ${this.activeAddDeviceFlow?.state}`);
440
460
  }
441
- const syncKey = this.activeAddDeviceFlow.syncKey;
442
- // Decrypt the received public key
443
- const decryptedPublicKey = await this.cryptoLib.decryptSymmetric(syncKey, encryptedPublicKey);
444
- const vaultState = JSON.parse(await this.cryptoLib.decryptSymmetric(syncKey, encryptedVaultState));
445
- if (vaultState.deviceId !== this.activeAddDeviceFlow.initiatorDeviceId) {
446
- throw new SyncError(`DeviceId mismatch when importing, expected ${this.activeAddDeviceFlow.initiatorDeviceId} got ${vaultState.deviceId}`);
447
- }
448
- this.syncDevices = vaultState.sync.devices;
449
- this.mediator
450
- .getComponent('vaultDataManager')
451
- .replaceVault(vaultState.vault);
452
- // Update the sync devices list with the initiator's information
453
- // Not done as a command as all other devices already have the senders info
454
- await this.addSyncDevice({
455
- deviceId: this.activeAddDeviceFlow.initiatorDeviceId,
456
- deviceType: this.activeAddDeviceFlow.initiatorDeviceType,
457
- publicKey: decryptedPublicKey,
458
- });
461
+ await this.importVaultState(encryptedVaultState, this.activeAddDeviceFlow.syncKey, this.activeAddDeviceFlow.initiatorDeviceId);
459
462
  // Reset the active add device flow
460
463
  this.activeAddDeviceFlow = undefined;
461
464
  this.dispatchLibEvent(TwoFaLibEvent.ConnectToExistingVaultFinished);
462
465
  }
466
+ async importVaultState(encryptedVaultState, symmetricKey, expectedDeviceId) {
467
+ const vaultState = JSON.parse(await this.cryptoLib.decryptSymmetric(symmetricKey, encryptedVaultState));
468
+ if (vaultState.deviceId !== expectedDeviceId) {
469
+ throw new SyncError(`DeviceId mismatch when importing, expected ${expectedDeviceId} got ${vaultState.deviceId}`);
470
+ }
471
+ this.syncDevices = vaultState.sync.devices;
472
+ for (const device of vaultState.sync.devices) {
473
+ await this.addSyncDevice(device, false);
474
+ }
475
+ const vaultDataManager = this.mediator.getComponent('vaultDataManager');
476
+ for (const entry of vaultState.vault) {
477
+ await vaultDataManager.addEntry(entry, false);
478
+ }
479
+ await this.persistentStorageManager.save();
480
+ }
463
481
  /**
464
482
  * Cancels the active add sync device flow.
465
483
  * @throws {SyncNoServerConnectionError} If there is no server connection.
@@ -487,6 +505,10 @@ class SyncManager {
487
505
  async sendCommand(command) {
488
506
  const commandJson = command.toJSON();
489
507
  await Promise.all(this.syncDevices.map(async (device) => {
508
+ if (device.deviceId === this.deviceId) {
509
+ // skip ourselves
510
+ return;
511
+ }
490
512
  const symmetricKey = await this.cryptoLib.createSymmetricKey();
491
513
  const encryptedSymmetricKey = await this.cryptoLib.encrypt(device.publicKey, symmetricKey);
492
514
  const encryptedCommand = await this.cryptoLib.encryptSymmetric(symmetricKey, JSON.stringify({
@@ -558,20 +580,50 @@ class SyncManager {
558
580
  });
559
581
  }
560
582
  }
583
+ /**
584
+ * Sends vault data to the server for each sync device
585
+ */
586
+ async resilver() {
587
+ for (const device of this.syncDevices) {
588
+ if (device.deviceId === this.deviceId) {
589
+ continue;
590
+ }
591
+ const symmetricKey = await this.cryptoLib.createSymmetricKey();
592
+ const encryptedSymmetricKey = await this.cryptoLib.encrypt(device.publicKey, symmetricKey);
593
+ const encryptedVaultData = await this.persistentStorageManager.getEncryptedVaultState(symmetricKey);
594
+ this.sendToServer('vault', {
595
+ forDeviceId: device.deviceId,
596
+ nonce: await this.getNonce(),
597
+ encryptedVaultData,
598
+ encryptedSymmetricKey,
599
+ });
600
+ }
601
+ }
561
602
  /**
562
603
  * Add a sync device
563
604
  * @param deviceInfo - The info about the device
605
+ * @param saveAfter - Whether to save the new vault after adding it (set to false when adding multiple devices)
564
606
  */
565
- async addSyncDevice(deviceInfo) {
566
- if (deviceInfo.deviceId === this.deviceId) {
567
- // This is the command that adds this device to all sync devices
568
- // besides the sender after the device add flow. We don't want to
569
- // add ourselves to our syncDevices
607
+ async addSyncDevice(deviceInfo, saveAfter = true) {
608
+ if (this.syncDevices.some((d) => d.deviceId === deviceInfo.deviceId)) {
609
+ // we already have this device
570
610
  return;
571
611
  }
572
612
  this.log('info', `Adding syncdevice ${deviceInfo.deviceId} to ${this.deviceId}`);
573
- this.syncDevices.push(deviceInfo);
574
- await this.persistentStorageManager.save();
613
+ this.syncDevices.push({
614
+ ...deviceInfo,
615
+ });
616
+ if (saveAfter) {
617
+ await this.persistentStorageManager.save();
618
+ }
619
+ }
620
+ /**
621
+ * Requests a resilver of the vault
622
+ */
623
+ requestResilver() {
624
+ this.sendToServer('startResilver', {
625
+ deviceIds: this.syncDevices.map((d) => d.deviceId),
626
+ });
575
627
  }
576
628
  /**
577
629
  * Function to call when the server connection should be closed
@@ -42,10 +42,11 @@ declare class VaultDataManager {
42
42
  generateTokenForEntry(id: EntryId, timestamp?: number): Token;
43
43
  /**
44
44
  * Add a new entry to the vault.
45
- * @param entry - The entry data to add (without an ID, as it will be generated).
45
+ * @param entry - The entry data to add
46
+ * @param saveAfter - Whether to save the new vault after adding it (set to false when adding multiple entries)
46
47
  * @returns A promise that resolves when the entry is added.
47
48
  */
48
- addEntry(entry: Entry): Promise<void>;
49
+ addEntry(entry: Entry, saveAfter?: boolean): Promise<void>;
49
50
  /**
50
51
  * Delete an entry from the vault.
51
52
  * @param entryId - The identifier of the entry to delete.
@@ -70,12 +70,19 @@ class VaultDataManager {
70
70
  }
71
71
  /**
72
72
  * Add a new entry to the vault.
73
- * @param entry - The entry data to add (without an ID, as it will be generated).
73
+ * @param entry - The entry data to add
74
+ * @param saveAfter - Whether to save the new vault after adding it (set to false when adding multiple entries)
74
75
  * @returns A promise that resolves when the entry is added.
75
76
  */
76
- async addEntry(entry) {
77
+ async addEntry(entry, saveAfter = true) {
78
+ if (this.vault.find((e) => e.id === entry.id)) {
79
+ // We already have this entry
80
+ return;
81
+ }
77
82
  this.vault.push(entry);
78
- await this.persistentStorageManager.save();
83
+ if (saveAfter) {
84
+ await this.persistentStorageManager.save();
85
+ }
79
86
  }
80
87
  /**
81
88
  * Delete an entry from the vault.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favalib",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {