@metamask-previews/seedless-onboarding-controller 0.0.0-preview-c4b7ea2e → 0.0.0-preview-e9f8bc88

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 (38) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/SecretMetadata.cjs.map +1 -1
  3. package/dist/SecretMetadata.d.cts +0 -5
  4. package/dist/SecretMetadata.d.cts.map +1 -1
  5. package/dist/SecretMetadata.d.mts +0 -5
  6. package/dist/SecretMetadata.d.mts.map +1 -1
  7. package/dist/SecretMetadata.mjs.map +1 -1
  8. package/dist/SeedlessOnboardingController.cjs +328 -117
  9. package/dist/SeedlessOnboardingController.cjs.map +1 -1
  10. package/dist/SeedlessOnboardingController.d.cts +47 -1
  11. package/dist/SeedlessOnboardingController.d.cts.map +1 -1
  12. package/dist/SeedlessOnboardingController.d.mts +47 -1
  13. package/dist/SeedlessOnboardingController.d.mts.map +1 -1
  14. package/dist/SeedlessOnboardingController.mjs +330 -119
  15. package/dist/SeedlessOnboardingController.mjs.map +1 -1
  16. package/dist/constants.cjs +5 -1
  17. package/dist/constants.cjs.map +1 -1
  18. package/dist/constants.d.cts +5 -1
  19. package/dist/constants.d.cts.map +1 -1
  20. package/dist/constants.d.mts +5 -1
  21. package/dist/constants.d.mts.map +1 -1
  22. package/dist/constants.mjs +4 -0
  23. package/dist/constants.mjs.map +1 -1
  24. package/dist/errors.cjs +26 -1
  25. package/dist/errors.cjs.map +1 -1
  26. package/dist/errors.d.cts +13 -0
  27. package/dist/errors.d.cts.map +1 -1
  28. package/dist/errors.d.mts +13 -0
  29. package/dist/errors.d.mts.map +1 -1
  30. package/dist/errors.mjs +24 -0
  31. package/dist/errors.mjs.map +1 -1
  32. package/dist/types.cjs.map +1 -1
  33. package/dist/types.d.cts +16 -1
  34. package/dist/types.d.cts.map +1 -1
  35. package/dist/types.d.mts +16 -1
  36. package/dist/types.d.mts.map +1 -1
  37. package/dist/types.mjs.map +1 -1
  38. package/package.json +1 -1
@@ -9,14 +9,15 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup, _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey, _SeedlessOnboardingController_withPersistedSeedPhraseBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_serializeKeyData, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertIsValidVaultData;
12
+ var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_recoverPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup, _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey, _SeedlessOnboardingController_withPersistedSeedPhraseBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_serializeKeyData, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertIsSRPBackedUpUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_assertIsValidVaultData;
13
13
  import { keccak256AndHexify } from "@metamask/auth-network-utils";
14
14
  import { BaseController } from "@metamask/base-controller";
15
15
  import { ToprfSecureBackup } from "@metamask/toprf-secure-backup";
16
16
  import { base64ToBytes, bytesToBase64, stringToBytes, remove0x, bigIntToHex } from "@metamask/utils";
17
+ import { secp256k1 } from "@noble/curves/secp256k1";
17
18
  import { Mutex } from "async-mutex";
18
- import { controllerName, SecretType, SeedlessOnboardingControllerErrorMessage, Web3AuthNetwork } from "./constants.mjs";
19
- import { RecoveryError } from "./errors.mjs";
19
+ import { controllerName, PASSWORD_OUTDATED_CACHE_TTL_MS, SecretType, SeedlessOnboardingControllerErrorMessage, Web3AuthNetwork } from "./constants.mjs";
20
+ import { PasswordSyncError, RecoveryError } from "./errors.mjs";
20
21
  import { projectLogger, createModuleLogger } from "./logger.mjs";
21
22
  import { SecretMetadata } from "./SecretMetadata.mjs";
22
23
  const log = createModuleLogger(projectLogger, controllerName);
@@ -47,7 +48,7 @@ const seedlessOnboardingMetadata = {
47
48
  anonymous: true,
48
49
  },
49
50
  nodeAuthTokens: {
50
- persist: false,
51
+ persist: true,
51
52
  anonymous: true,
52
53
  },
53
54
  authConnection: {
@@ -78,6 +79,14 @@ const seedlessOnboardingMetadata = {
78
79
  persist: false,
79
80
  anonymous: true,
80
81
  },
82
+ authPubKey: {
83
+ persist: true,
84
+ anonymous: true,
85
+ },
86
+ passwordOutdatedCache: {
87
+ persist: true,
88
+ anonymous: true,
89
+ },
81
90
  };
82
91
  export class SeedlessOnboardingController extends BaseController {
83
92
  /**
@@ -101,6 +110,7 @@ export class SeedlessOnboardingController extends BaseController {
101
110
  });
102
111
  _SeedlessOnboardingController_instances.add(this);
103
112
  _SeedlessOnboardingController_vaultEncryptor.set(this, void 0);
113
+ _SeedlessOnboardingController_controllerOperationMutex.set(this, new Mutex());
104
114
  _SeedlessOnboardingController_vaultOperationMutex.set(this, new Mutex());
105
115
  /**
106
116
  * Controller lock state.
@@ -136,35 +146,37 @@ export class SeedlessOnboardingController extends BaseController {
136
146
  * @returns A promise that resolves to the authentication result.
137
147
  */
138
148
  async authenticate(params) {
139
- try {
140
- const { idTokens, authConnectionId, groupedAuthConnectionId, userId, authConnection, socialLoginEmail, } = params;
141
- const hashedIdTokenHexes = idTokens.map((idToken) => {
142
- return remove0x(keccak256AndHexify(stringToBytes(idToken)));
143
- });
144
- const authenticationResult = await this.toprfClient.authenticate({
145
- authConnectionId: groupedAuthConnectionId || authConnectionId,
146
- userId,
147
- idTokens: hashedIdTokenHexes,
148
- groupedAuthConnectionParams: {
149
- authConnectionId,
150
- idTokens,
151
- },
152
- });
153
- // update the state with the authenticated user info
154
- this.update((state) => {
155
- state.nodeAuthTokens = authenticationResult.nodeAuthTokens;
156
- state.authConnectionId = authConnectionId;
157
- state.groupedAuthConnectionId = groupedAuthConnectionId;
158
- state.userId = userId;
159
- state.authConnection = authConnection;
160
- state.socialLoginEmail = socialLoginEmail;
161
- });
162
- return authenticationResult;
163
- }
164
- catch (error) {
165
- log('Error authenticating user', error);
166
- throw new Error(SeedlessOnboardingControllerErrorMessage.AuthenticationError);
167
- }
149
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
150
+ try {
151
+ const { idTokens, authConnectionId, groupedAuthConnectionId, userId, authConnection, socialLoginEmail, } = params;
152
+ const hashedIdTokenHexes = idTokens.map((idToken) => {
153
+ return remove0x(keccak256AndHexify(stringToBytes(idToken)));
154
+ });
155
+ const authenticationResult = await this.toprfClient.authenticate({
156
+ authConnectionId: groupedAuthConnectionId || authConnectionId,
157
+ userId,
158
+ idTokens: hashedIdTokenHexes,
159
+ groupedAuthConnectionParams: {
160
+ authConnectionId,
161
+ idTokens,
162
+ },
163
+ });
164
+ // update the state with the authenticated user info
165
+ this.update((state) => {
166
+ state.nodeAuthTokens = authenticationResult.nodeAuthTokens;
167
+ state.authConnectionId = authConnectionId;
168
+ state.groupedAuthConnectionId = groupedAuthConnectionId;
169
+ state.userId = userId;
170
+ state.authConnection = authConnection;
171
+ state.socialLoginEmail = socialLoginEmail;
172
+ });
173
+ return authenticationResult;
174
+ }
175
+ catch (error) {
176
+ log('Error authenticating user', error);
177
+ throw new Error(SeedlessOnboardingControllerErrorMessage.AuthenticationError);
178
+ }
179
+ });
168
180
  }
169
181
  /**
170
182
  * Create a new TOPRF encryption key using given password and backups the provided seed phrase.
@@ -178,27 +190,32 @@ export class SeedlessOnboardingController extends BaseController {
178
190
  // to make sure that fail fast,
179
191
  // assert that the user is authenticated before creating the TOPRF key and backing up the seed phrase
180
192
  __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
181
- // locally evaluate the encryption key from the password
182
- const { encKey, authKeyPair, oprfKey } = this.toprfClient.createLocalKey({
183
- password,
184
- });
185
- // encrypt and store the seed phrase backup
186
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup).call(this, {
187
- keyringId,
188
- seedPhrase,
189
- encKey,
190
- authKeyPair,
191
- });
192
- // store/persist the encryption key shares
193
- // We store the seed phrase metadata in the metadata store first. If this operation fails,
194
- // we avoid persisting the encryption key shares to prevent a situation where a user appears
195
- // to have an account but with no associated data.
196
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistOprfKey).call(this, oprfKey, authKeyPair.pk);
197
- // create a new vault with the resulting authentication data
198
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
199
- password,
200
- rawToprfEncryptionKey: encKey,
201
- rawToprfAuthKeyPair: authKeyPair,
193
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
194
+ // locally evaluate the encryption key from the password
195
+ const { encKey, authKeyPair, oprfKey } = this.toprfClient.createLocalKey({
196
+ password,
197
+ });
198
+ // encrypt and store the seed phrase backup
199
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup).call(this, {
200
+ keyringId,
201
+ seedPhrase,
202
+ encKey,
203
+ authKeyPair,
204
+ });
205
+ // store/persist the encryption key shares
206
+ // We store the seed phrase metadata in the metadata store first. If this operation fails,
207
+ // we avoid persisting the encryption key shares to prevent a situation where a user appears
208
+ // to have an account but with no associated data.
209
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistOprfKey).call(this, oprfKey, authKeyPair.pk);
210
+ // create a new vault with the resulting authentication data
211
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
212
+ password,
213
+ rawToprfEncryptionKey: encKey,
214
+ rawToprfAuthKeyPair: authKeyPair,
215
+ });
216
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistAuthPubKey).call(this, {
217
+ authPubKey: authKeyPair.pk,
218
+ });
202
219
  });
203
220
  }
204
221
  /**
@@ -209,15 +226,21 @@ export class SeedlessOnboardingController extends BaseController {
209
226
  * @returns A promise that resolves to the success of the operation.
210
227
  */
211
228
  async addNewSeedPhraseBackup(seedPhrase, keyringId) {
212
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
213
- // verify the password and unlock the vault
214
- const { toprfEncryptionKey, toprfAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this);
215
- // encrypt and store the seed phrase backup
216
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup).call(this, {
217
- keyringId,
218
- seedPhrase,
219
- encKey: toprfEncryptionKey,
220
- authKeyPair: toprfAuthKeyPair,
229
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
230
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
231
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
232
+ skipCache: true,
233
+ skipLock: true, // skip lock since we already have the lock
234
+ });
235
+ // verify the password and unlock the vault
236
+ const { toprfEncryptionKey, toprfAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this);
237
+ // encrypt and store the seed phrase backup
238
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_encryptAndStoreSeedPhraseBackup).call(this, {
239
+ keyringId,
240
+ seedPhrase,
241
+ encKey: toprfEncryptionKey,
242
+ authKeyPair: toprfAuthKeyPair,
243
+ });
221
244
  });
222
245
  }
223
246
  /**
@@ -231,40 +254,45 @@ export class SeedlessOnboardingController extends BaseController {
231
254
  async fetchAllSeedPhrases(password) {
232
255
  // assert that the user is authenticated before fetching the seed phrases
233
256
  __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
234
- let encKey;
235
- let authKeyPair;
236
- if (password) {
237
- const recoverEncKeyResult = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, password);
238
- encKey = recoverEncKeyResult.encKey;
239
- authKeyPair = recoverEncKeyResult.authKeyPair;
240
- }
241
- else {
242
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
243
- // verify the password and unlock the vault
244
- const { toprfEncryptionKey, toprfAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this);
245
- encKey = toprfEncryptionKey;
246
- authKeyPair = toprfAuthKeyPair;
247
- }
248
- try {
249
- const secretData = await this.toprfClient.fetchAllSecretDataItems({
250
- decKey: encKey,
251
- authKeyPair,
252
- });
253
- if (secretData?.length > 0 && password) {
254
- // if password is provided, we need to create a new vault with the auth data. (supposedly the user is trying to rehydrate the wallet)
255
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
256
- password,
257
- rawToprfEncryptionKey: encKey,
258
- rawToprfAuthKeyPair: authKeyPair,
257
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
258
+ let encKey;
259
+ let authKeyPair;
260
+ if (password) {
261
+ const recoverEncKeyResult = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, password);
262
+ encKey = recoverEncKeyResult.encKey;
263
+ authKeyPair = recoverEncKeyResult.authKeyPair;
264
+ }
265
+ else {
266
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
267
+ // verify the password and unlock the vault
268
+ const keysFromVault = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this);
269
+ encKey = keysFromVault.toprfEncryptionKey;
270
+ authKeyPair = keysFromVault.toprfAuthKeyPair;
271
+ }
272
+ try {
273
+ const secretData = await this.toprfClient.fetchAllSecretDataItems({
274
+ decKey: encKey,
275
+ authKeyPair,
259
276
  });
277
+ if (secretData?.length > 0 && password) {
278
+ // if password is provided, we need to create a new vault with the auth data. (supposedly the user is trying to rehydrate the wallet)
279
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
280
+ password,
281
+ rawToprfEncryptionKey: encKey,
282
+ rawToprfAuthKeyPair: authKeyPair,
283
+ });
284
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistAuthPubKey).call(this, {
285
+ authPubKey: authKeyPair.pk,
286
+ });
287
+ }
288
+ const secrets = SecretMetadata.parseSecretsFromMetadataStore(secretData, SecretType.Mnemonic);
289
+ return secrets.map((secret) => secret.data);
260
290
  }
261
- const secrets = SecretMetadata.parseSecretsFromMetadataStore(secretData, SecretType.Mnemonic);
262
- return secrets.map((secret) => secret.data);
263
- }
264
- catch (error) {
265
- log('Error fetching seed phrase metadata', error);
266
- throw new Error(SeedlessOnboardingControllerErrorMessage.FailedToFetchSeedPhraseMetadata);
267
- }
291
+ catch (error) {
292
+ log('Error fetching seed phrase metadata', error);
293
+ throw new Error(SeedlessOnboardingControllerErrorMessage.FailedToFetchSeedPhraseMetadata);
294
+ }
295
+ });
268
296
  }
269
297
  /**
270
298
  * Update the password of the seedless onboarding flow.
@@ -273,25 +301,38 @@ export class SeedlessOnboardingController extends BaseController {
273
301
  *
274
302
  * @param newPassword - The new password to update.
275
303
  * @param oldPassword - The old password to verify.
304
+ * @returns A promise that resolves to the success of the operation.
276
305
  */
277
306
  async changePassword(newPassword, oldPassword) {
278
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
279
- // verify the old password of the encrypted vault
280
- await this.verifyPassword(oldPassword);
281
- try {
282
- // update the encryption key with new password and update the Metadata Store
283
- const { encKey: newEncKey, authKeyPair: newAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_changeEncryptionKey).call(this, newPassword, oldPassword);
284
- // update and encrypt the vault with new password
285
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
286
- password: newPassword,
287
- rawToprfEncryptionKey: newEncKey,
288
- rawToprfAuthKeyPair: newAuthKeyPair,
307
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
308
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
309
+ // verify the old password of the encrypted vault
310
+ await this.verifyVaultPassword(oldPassword, {
311
+ skipLock: true, // skip lock since we already have the lock
289
312
  });
290
- }
291
- catch (error) {
292
- log('Error changing password', error);
293
- throw new Error(SeedlessOnboardingControllerErrorMessage.FailedToChangePassword);
294
- }
313
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
314
+ skipCache: true,
315
+ skipLock: true, // skip lock since we already have the lock
316
+ });
317
+ try {
318
+ // update the encryption key with new password and update the Metadata Store
319
+ const { encKey: newEncKey, authKeyPair: newAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_changeEncryptionKey).call(this, newPassword, oldPassword);
320
+ // update and encrypt the vault with new password
321
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
322
+ password: newPassword,
323
+ rawToprfEncryptionKey: newEncKey,
324
+ rawToprfAuthKeyPair: newAuthKeyPair,
325
+ });
326
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistAuthPubKey).call(this, {
327
+ authPubKey: newAuthKeyPair.pk,
328
+ });
329
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_resetPasswordOutdatedCache).call(this);
330
+ }
331
+ catch (error) {
332
+ log('Error changing password', error);
333
+ throw new Error(SeedlessOnboardingControllerErrorMessage.FailedToChangePassword);
334
+ }
335
+ });
295
336
  }
296
337
  /**
297
338
  * Update the backup metadata state for the given seed phrase.
@@ -308,13 +349,21 @@ export class SeedlessOnboardingController extends BaseController {
308
349
  * Verify the password validity by decrypting the vault.
309
350
  *
310
351
  * @param password - The password to verify.
352
+ * @param options - Optional options object.
353
+ * @param options.skipLock - Whether to skip the lock acquisition.
354
+ * @returns A promise that resolves to the success of the operation.
311
355
  * @throws {Error} If the password is invalid or the vault is not initialized.
312
356
  */
313
- async verifyPassword(password) {
314
- if (!this.state.vault) {
315
- throw new Error(SeedlessOnboardingControllerErrorMessage.VaultError);
316
- }
317
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_vaultEncryptor, "f").decrypt(password, this.state.vault);
357
+ async verifyVaultPassword(password, options) {
358
+ const doVerify = async () => {
359
+ if (!this.state.vault) {
360
+ throw new Error(SeedlessOnboardingControllerErrorMessage.VaultError);
361
+ }
362
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_vaultEncryptor, "f").decrypt(password, this.state.vault);
363
+ };
364
+ return options?.skipLock
365
+ ? await doVerify()
366
+ : await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, doVerify);
318
367
  }
319
368
  /**
320
369
  * Get the hash of the seed phrase backup for the given seed phrase, from the state.
@@ -337,10 +386,13 @@ export class SeedlessOnboardingController extends BaseController {
337
386
  * This operation is useful when user performs some actions that requires the user password/encryption key. e.g. add new srp backup
338
387
  *
339
388
  * @param password - The password to submit.
389
+ * @returns A promise that resolves to the success of the operation.
340
390
  */
341
391
  async submitPassword(password) {
342
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this, password);
343
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_setUnlocked).call(this);
392
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
393
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetBackupEncKey).call(this, password);
394
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_setUnlocked).call(this);
395
+ });
344
396
  }
345
397
  /**
346
398
  * Set the controller to locked state, and deallocate the secrets (vault encryption key and salt).
@@ -354,6 +406,98 @@ export class SeedlessOnboardingController extends BaseController {
354
406
  });
355
407
  __classPrivateFieldSet(this, _SeedlessOnboardingController_isUnlocked, false, "f");
356
408
  }
409
+ /**
410
+ * Sync the latest global password to the controller.
411
+ * reset vault with latest globalPassword,
412
+ * persist the latest global password authPubKey
413
+ *
414
+ * @param params - The parameters for syncing the latest global password.
415
+ * @param params.oldPassword - The old password to verify.
416
+ * @param params.globalPassword - The latest global password.
417
+ * @returns A promise that resolves to the success of the operation.
418
+ */
419
+ async syncLatestGlobalPassword({ oldPassword, globalPassword, }) {
420
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
421
+ // verify correct old password
422
+ await this.verifyVaultPassword(oldPassword, {
423
+ skipLock: true, // skip lock since we already have the lock
424
+ });
425
+ // update vault with latest globalPassword
426
+ const { encKey, authKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, globalPassword);
427
+ // update and encrypt the vault with new password
428
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
429
+ password: globalPassword,
430
+ rawToprfEncryptionKey: encKey,
431
+ rawToprfAuthKeyPair: authKeyPair,
432
+ });
433
+ // persist the latest global password authPubKey
434
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_persistAuthPubKey).call(this, {
435
+ authPubKey: authKeyPair.pk,
436
+ });
437
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_resetPasswordOutdatedCache).call(this);
438
+ });
439
+ }
440
+ /**
441
+ * @description Fetch the password corresponding to the current authPubKey in state (current device password which is already out of sync with the current global password).
442
+ * then we use this recovered old password to unlock the vault and set the password to the new global password.
443
+ *
444
+ * @param params - The parameters for fetching the password.
445
+ * @param params.globalPassword - The latest global password.
446
+ * @returns A promise that resolves to the password corresponding to the current authPubKey in state.
447
+ */
448
+ async recoverCurrentDevicePassword({ globalPassword, }) {
449
+ return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
450
+ const currentDeviceAuthPubKey = __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverAuthPubKey).call(this);
451
+ const { password: currentDevicePassword } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverPassword).call(this, {
452
+ targetPwPubKey: currentDeviceAuthPubKey,
453
+ globalPassword,
454
+ });
455
+ return {
456
+ password: currentDevicePassword,
457
+ };
458
+ });
459
+ }
460
+ /**
461
+ * @description Check if the current password is outdated compare to the global password.
462
+ *
463
+ * @param options - Optional options object.
464
+ * @param options.skipCache - If true, bypass the cache and force a fresh check.
465
+ * @param options.skipLock - Whether to skip the lock acquisition.
466
+ * @returns A promise that resolves to true if the password is outdated, false otherwise.
467
+ */
468
+ async checkIsPasswordOutdated(options) {
469
+ // cache result to reduce load on infra
470
+ // Check cache first unless skipCache is true
471
+ if (!options?.skipCache) {
472
+ const { passwordOutdatedCache } = this.state;
473
+ const now = Date.now();
474
+ const isCacheValid = passwordOutdatedCache &&
475
+ now - passwordOutdatedCache.timestamp < PASSWORD_OUTDATED_CACHE_TTL_MS;
476
+ if (isCacheValid) {
477
+ return passwordOutdatedCache.isExpiredPwd;
478
+ }
479
+ }
480
+ const doCheck = async () => {
481
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
482
+ const { nodeAuthTokens, authConnectionId, groupedAuthConnectionId, userId, } = this.state;
483
+ const currentDeviceAuthPubKey = __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverAuthPubKey).call(this);
484
+ const { authPubKey: globalAuthPubKey } = await this.toprfClient.fetchAuthPubKey({
485
+ nodeAuthTokens,
486
+ authConnectionId: groupedAuthConnectionId || authConnectionId,
487
+ userId,
488
+ });
489
+ // use noble lib to deserialize and compare curve point
490
+ const isExpiredPwd = !secp256k1.ProjectivePoint.fromHex(currentDeviceAuthPubKey).equals(secp256k1.ProjectivePoint.fromHex(globalAuthPubKey));
491
+ // Cache the result in state
492
+ this.update((state) => {
493
+ state.passwordOutdatedCache = { isExpiredPwd, timestamp: Date.now() };
494
+ });
495
+ return isExpiredPwd;
496
+ };
497
+ return options?.skipLock
498
+ ? await doCheck()
499
+ : await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, doCheck);
500
+ }
357
501
  /**
358
502
  * Clears the current state of the SeedlessOnboardingController.
359
503
  */
@@ -364,7 +508,29 @@ export class SeedlessOnboardingController extends BaseController {
364
508
  });
365
509
  }
366
510
  }
367
- _SeedlessOnboardingController_vaultEncryptor = new WeakMap(), _SeedlessOnboardingController_vaultOperationMutex = new WeakMap(), _SeedlessOnboardingController_isUnlocked = new WeakMap(), _SeedlessOnboardingController_instances = new WeakSet(), _SeedlessOnboardingController_setUnlocked = function _SeedlessOnboardingController_setUnlocked() {
511
+ _SeedlessOnboardingController_vaultEncryptor = new WeakMap(), _SeedlessOnboardingController_controllerOperationMutex = new WeakMap(), _SeedlessOnboardingController_vaultOperationMutex = new WeakMap(), _SeedlessOnboardingController_isUnlocked = new WeakMap(), _SeedlessOnboardingController_instances = new WeakSet(), _SeedlessOnboardingController_recoverPassword =
512
+ /**
513
+ * @description Fetch the password corresponding to the targetPwPubKey.
514
+ *
515
+ * @param params - The parameters for fetching the password.
516
+ * @param params.targetPwPubKey - The target public key of the password to recover.
517
+ * @param params.globalPassword - The latest global password.
518
+ * @returns A promise that resolves to the password corresponding to the current authPubKey in state.
519
+ */
520
+ async function _SeedlessOnboardingController_recoverPassword({ targetPwPubKey, globalPassword, }) {
521
+ const { encKey: latestPwEncKey, authKeyPair: latestPwAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, globalPassword);
522
+ try {
523
+ const res = await this.toprfClient.recoverPassword({
524
+ targetPwPubKey,
525
+ curEncKey: latestPwEncKey,
526
+ curAuthKeyPair: latestPwAuthKeyPair,
527
+ });
528
+ return res;
529
+ }
530
+ catch (error) {
531
+ throw PasswordSyncError.getInstance(error);
532
+ }
533
+ }, _SeedlessOnboardingController_setUnlocked = function _SeedlessOnboardingController_setUnlocked() {
368
534
  __classPrivateFieldSet(this, _SeedlessOnboardingController_isUnlocked, true, "f");
369
535
  }, _SeedlessOnboardingController_persistOprfKey =
370
536
  /**
@@ -390,6 +556,14 @@ async function _SeedlessOnboardingController_persistOprfKey(oprfKey, authPubKey)
390
556
  log('Error persisting local encryption key', error);
391
557
  throw new Error(SeedlessOnboardingControllerErrorMessage.FailedToPersistOprfKey);
392
558
  }
559
+ }, _SeedlessOnboardingController_persistAuthPubKey = function _SeedlessOnboardingController_persistAuthPubKey(params) {
560
+ this.update((state) => {
561
+ state.authPubKey = bytesToBase64(params.authPubKey);
562
+ });
563
+ }, _SeedlessOnboardingController_recoverAuthPubKey = function _SeedlessOnboardingController_recoverAuthPubKey() {
564
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsSRPBackedUpUser).call(this, this.state);
565
+ const { authPubKey } = this.state;
566
+ return base64ToBytes(authPubKey);
393
567
  }, _SeedlessOnboardingController_recoverEncKey =
394
568
  /**
395
569
  * Recover the encryption key from password.
@@ -626,6 +800,21 @@ async function _SeedlessOnboardingController_updateVault({ password, serializedV
626
800
  state.vaultEncryptionSalt = JSON.parse(vault).salt;
627
801
  });
628
802
  });
803
+ }, _SeedlessOnboardingController_withControllerLock =
804
+ /**
805
+ * Lock the controller mutex before executing the given function,
806
+ * and release it after the function is resolved or after an
807
+ * error is thrown.
808
+ *
809
+ * This wrapper ensures that each mutable operation that interacts with the
810
+ * controller and that changes its state is executed in a mutually exclusive way,
811
+ * preventing unsafe concurrent access that could lead to unpredictable behavior.
812
+ *
813
+ * @param callback - The function to execute while the controller mutex is locked.
814
+ * @returns The result of the function.
815
+ */
816
+ async function _SeedlessOnboardingController_withControllerLock(callback) {
817
+ return await withLock(__classPrivateFieldGet(this, _SeedlessOnboardingController_controllerOperationMutex, "f"), callback);
629
818
  }, _SeedlessOnboardingController_withVaultLock =
630
819
  /**
631
820
  * Lock the vault mutex before executing the given function,
@@ -693,6 +882,28 @@ async function _SeedlessOnboardingController_withVaultLock(callback) {
693
882
  ) {
694
883
  throw new Error(SeedlessOnboardingControllerErrorMessage.InsufficientAuthToken);
695
884
  }
885
+ }, _SeedlessOnboardingController_assertIsSRPBackedUpUser = function _SeedlessOnboardingController_assertIsSRPBackedUpUser(value) {
886
+ if (!this.state.authPubKey) {
887
+ throw new Error(SeedlessOnboardingControllerErrorMessage.SRPNotBackedUpError);
888
+ }
889
+ }, _SeedlessOnboardingController_assertPasswordInSync =
890
+ /**
891
+ * Assert that the password is in sync with the global password.
892
+ *
893
+ * @param options - The options for asserting the password is in sync.
894
+ * @param options.skipCache - Whether to skip the cache check.
895
+ * @param options.skipLock - Whether to skip the lock acquisition.
896
+ * @throws If the password is outdated.
897
+ */
898
+ async function _SeedlessOnboardingController_assertPasswordInSync(options) {
899
+ const isPasswordOutdated = await this.checkIsPasswordOutdated(options);
900
+ if (isPasswordOutdated) {
901
+ throw new Error(SeedlessOnboardingControllerErrorMessage.OutdatedPassword);
902
+ }
903
+ }, _SeedlessOnboardingController_resetPasswordOutdatedCache = function _SeedlessOnboardingController_resetPasswordOutdatedCache() {
904
+ this.update((state) => {
905
+ delete state.passwordOutdatedCache;
906
+ });
696
907
  }, _SeedlessOnboardingController_assertIsValidVaultData = function _SeedlessOnboardingController_assertIsValidVaultData(value) {
697
908
  // value is not valid vault data if any of the following conditions are true:
698
909
  if (!value || // value is not defined