@metamask/passkey-controller 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +83 -20
  3. package/dist/PasskeyController-method-action-types.cjs +7 -0
  4. package/dist/PasskeyController-method-action-types.cjs.map +1 -0
  5. package/dist/PasskeyController-method-action-types.d.cts +204 -0
  6. package/dist/PasskeyController-method-action-types.d.cts.map +1 -0
  7. package/dist/PasskeyController-method-action-types.d.mts +204 -0
  8. package/dist/PasskeyController-method-action-types.d.mts.map +1 -0
  9. package/dist/PasskeyController-method-action-types.mjs +6 -0
  10. package/dist/PasskeyController-method-action-types.mjs.map +1 -0
  11. package/dist/PasskeyController.cjs +341 -163
  12. package/dist/PasskeyController.cjs.map +1 -1
  13. package/dist/PasskeyController.d.cts +88 -49
  14. package/dist/PasskeyController.d.cts.map +1 -1
  15. package/dist/PasskeyController.d.mts +88 -49
  16. package/dist/PasskeyController.d.mts.map +1 -1
  17. package/dist/PasskeyController.mjs +341 -163
  18. package/dist/PasskeyController.mjs.map +1 -1
  19. package/dist/constants.cjs +4 -0
  20. package/dist/constants.cjs.map +1 -1
  21. package/dist/constants.d.cts +5 -1
  22. package/dist/constants.d.cts.map +1 -1
  23. package/dist/constants.d.mts +5 -1
  24. package/dist/constants.d.mts.map +1 -1
  25. package/dist/constants.mjs +4 -0
  26. package/dist/constants.mjs.map +1 -1
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.cts.map +1 -1
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/types.cjs +1 -0
  34. package/dist/types.cjs.map +1 -1
  35. package/dist/types.d.cts +38 -0
  36. package/dist/types.d.cts.map +1 -1
  37. package/dist/types.d.mts +38 -0
  38. package/dist/types.d.mts.map +1 -1
  39. package/dist/types.mjs +1 -1
  40. package/dist/types.mjs.map +1 -1
  41. package/package.json +11 -4
@@ -10,11 +10,12 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  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");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _PasskeyController_instances, _PasskeyController_ceremonyManager, _PasskeyController_expectedRPIDs, _PasskeyController_rpId, _PasskeyController_rpName, _PasskeyController_expectedOrigin, _PasskeyController_userName, _PasskeyController_userDisplayName, _PasskeyController_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_verifyAuthenticationResponse;
13
+ var _PasskeyController_instances, _PasskeyController_ceremonyManager, _PasskeyController_expectedRPIDs, _PasskeyController_rpId, _PasskeyController_rpName, _PasskeyController_expectedOrigin, _PasskeyController_userName, _PasskeyController_userDisplayName, _PasskeyController_getIsOnboardingCompleted, _PasskeyController_operationMutex, _PasskeyController_protectVaultKeyWithPasskey, _PasskeyController_retrieveVaultKeyWithPasskey, _PasskeyController_verifyPasskeyAuthentication, _PasskeyController_renewVaultKeyProtection, _PasskeyController_changePasswordWithPasskeyVerification, _PasskeyController_removePasskeyWithPasskeyVerification, _PasskeyController_removePasskeyWithPasswordVerification, _PasskeyController_verifyAuthenticationResponse, _PasskeyController_withOperationLock, _PasskeyController_assertEnrollmentAllowed, _PasskeyController_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_removePasskey;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.PasskeyController = exports.passkeyControllerSelectors = exports.getDefaultPasskeyControllerState = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
17
  const utils_1 = require("@metamask/utils");
18
+ const async_mutex_1 = require("async-mutex");
18
19
  const ceremony_manager_1 = require("./ceremony-manager.cjs");
19
20
  const constants_1 = require("./constants.cjs");
20
21
  const errors_1 = require("./errors.cjs");
@@ -54,6 +55,24 @@ const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, constants_1
54
55
  exports.passkeyControllerSelectors = {
55
56
  selectIsPasskeyEnrolled: (state) => state.passkeyRecord !== null,
56
57
  };
58
+ const MESSENGER_EXPOSED_METHODS = [
59
+ 'isPasskeyEnrolled',
60
+ 'generateRegistrationOptions',
61
+ 'generatePostRegistrationAuthenticationOptions',
62
+ 'generateAuthenticationOptions',
63
+ 'protectVaultKeyWithPasskey',
64
+ 'retrieveVaultKeyWithPasskey',
65
+ 'unlockWithPasskey',
66
+ 'verifyPasskeyAuthentication',
67
+ 'renewVaultKeyProtection',
68
+ 'changePasswordWithPasskeyVerification',
69
+ 'exportSeedPhraseWithPasskey',
70
+ 'exportAccountsWithPasskey',
71
+ 'removePasskeyWithPasskeyVerification',
72
+ 'removePasskeyWithPasswordVerification',
73
+ 'clearState',
74
+ 'destroy',
75
+ ];
57
76
  /**
58
77
  * Controller that enrolls a WebAuthn passkey and uses it to protect and unlock
59
78
  * the vault encryption key.
@@ -62,20 +81,18 @@ class PasskeyController extends base_controller_1.BaseController {
62
81
  /**
63
82
  * Creates a passkey controller with WebAuthn relying-party settings.
64
83
  *
65
- * @param args - Constructor options.
66
- * @param args.messenger - Controller messenger.
67
- * @param args.state - Partial initial state; merged with {@link getDefaultPasskeyControllerState}.
68
- * @param args.expectedRPID - Relying party ID(s) for verification (SHA-256 hash match in
69
- * authenticator data). Pass a string or array of strings; an empty array skips RP ID
70
- * allowlist checks in {@link verifyRegistrationResponse} / {@link verifyAuthenticationResponse}.
71
- * @param args.rpId - When set, included as `rp.id` on registration options and `rpId` on
72
- * authentication options. When omitted, those fields are left unset (client default RP ID).
73
- * @param args.rpName - Relying party name shown in the platform passkey UI.
74
- * @param args.expectedOrigin - Allowed value(s) for the WebAuthn client origin.
75
- * @param args.userName - Optional passkey user name; defaults to `rpName`.
76
- * @param args.userDisplayName - Optional display name; defaults to `rpName`.
84
+ * @param options - Constructor options.
85
+ * @param options.messenger - The messenger to use for communication.
86
+ * @param options.state - The initial state of the controller.
87
+ * @param options.rpId - The relying party ID to use for the passkey.
88
+ * @param options.expectedRPID - The expected relying party ID to use for the passkey.
89
+ * @param options.rpName - The relying party name to use for the passkey.
90
+ * @param options.expectedOrigin - The expected origin to use for the passkey.
91
+ * @param options.userName - The user name to use for the passkey.
92
+ * @param options.userDisplayName - The user display name to use for the passkey.
93
+ * @param options.getIsOnboardingCompleted - The callback to use to check if onboarding is complete.
77
94
  */
78
- constructor({ messenger, state = {}, rpId, expectedRPID, rpName, expectedOrigin, userName, userDisplayName, }) {
95
+ constructor({ messenger, state = {}, rpId, expectedRPID, rpName, expectedOrigin, userName, userDisplayName, getIsOnboardingCompleted, }) {
79
96
  super({
80
97
  messenger,
81
98
  metadata: passkeyControllerMetadata,
@@ -90,6 +107,8 @@ class PasskeyController extends base_controller_1.BaseController {
90
107
  _PasskeyController_expectedOrigin.set(this, void 0);
91
108
  _PasskeyController_userName.set(this, void 0);
92
109
  _PasskeyController_userDisplayName.set(this, void 0);
110
+ _PasskeyController_getIsOnboardingCompleted.set(this, void 0);
111
+ _PasskeyController_operationMutex.set(this, new async_mutex_1.Mutex());
93
112
  const expectedRPIDs = Array.isArray(expectedRPID)
94
113
  ? expectedRPID
95
114
  : [expectedRPID];
@@ -99,6 +118,8 @@ class PasskeyController extends base_controller_1.BaseController {
99
118
  __classPrivateFieldSet(this, _PasskeyController_expectedOrigin, expectedOrigin, "f");
100
119
  __classPrivateFieldSet(this, _PasskeyController_userName, userName ?? rpName, "f");
101
120
  __classPrivateFieldSet(this, _PasskeyController_userDisplayName, userDisplayName ?? rpName, "f");
121
+ __classPrivateFieldSet(this, _PasskeyController_getIsOnboardingCompleted, getIsOnboardingCompleted, "f");
122
+ this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
102
123
  }
103
124
  /**
104
125
  * Whether a passkey is enrolled and vault key material is stored.
@@ -243,113 +264,72 @@ class PasskeyController extends base_controller_1.BaseController {
243
264
  * Verifies registration and post-registration authentication, then stores the
244
265
  * vault key encrypted under the new passkey.
245
266
  *
267
+ * Fetches the current vault encryption key from KeyringController before wrapping.
268
+ * When onboarding is complete, requires `password` for step-up verification first.
269
+ *
246
270
  * @param params - Enrollment completion inputs.
247
271
  * @param params.registrationResponse - Result of `navigator.credentials.create()`.
248
272
  * @param params.authenticationResponse - Result of `navigator.credentials.get()` after {@link generatePostRegistrationAuthenticationOptions}.
249
- * @param params.vaultKey - Vault encryption key to encrypt and persist.
273
+ * @param params.password - Wallet password when onboarding is complete (step-up).
274
+ * @returns Resolves when enrollment completes.
250
275
  */
251
276
  async protectVaultKeyWithPasskey(params) {
252
- if (this.isPasskeyEnrolled()) {
253
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AlreadyEnrolled, { code: constants_1.PasskeyControllerErrorCode.AlreadyEnrolled });
254
- }
255
- const { registrationResponse, authenticationResponse, vaultKey } = params;
256
- // get registration ceremony
257
- const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
258
- const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
259
- if (!registrationCeremony) {
260
- log('No active passkey registration ceremony for challenge');
261
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoRegistrationCeremony });
262
- }
263
- try {
264
- // verify registration response
265
- const { verified, registrationInfo } = await (0, verify_registration_response_1.verifyRegistrationResponse)({
266
- response: registrationResponse,
267
- expectedChallenge: registrationCeremony.challenge,
268
- expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
269
- expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
270
- requireUserVerification: true,
271
- }).catch((error) => {
272
- log('Error verifying passkey registration response', error);
273
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, {
274
- code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed,
275
- cause: error instanceof Error ? error : new Error(String(error)),
276
- });
277
- });
278
- if (!verified || !registrationInfo) {
279
- log('Passkey registration verification returned unverified or missing registration info');
280
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed });
281
- }
282
- // verify authentication response
283
- const credential = {
284
- id: registrationInfo.credentialId,
285
- publicKey: (0, encoding_1.bytesToBase64URL)(registrationInfo.publicKey),
286
- counter: registrationInfo.counter,
287
- transports: registrationInfo.transports,
288
- aaguid: registrationInfo.aaguid,
289
- };
290
- const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, credential);
291
- // determine key derivation method
292
- const prfFirst = authenticationResponse.clientExtensionResults?.prf?.results?.first;
293
- const authHasPrfOutput = typeof prfFirst === 'string' && prfFirst.length > 0;
294
- const keyDerivation = authHasPrfOutput && registrationCeremony.prfSalt
295
- ? { method: 'prf', prfSalt: registrationCeremony.prfSalt }
296
- : { method: 'userHandle' };
297
- if (keyDerivation.method === 'userHandle' &&
298
- authenticationResponse.response.userHandle !==
299
- registrationCeremony.userHandle) {
300
- log('Post-registration assertion userHandle does not match registration ceremony');
301
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed });
302
- }
303
- // derive key and encrypt vault key
304
- const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, { credential, keyDerivation });
305
- const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(vaultKey, encKey);
306
- // persist passkey record
307
- this.update((state) => {
308
- state.passkeyRecord = {
309
- credential: {
310
- ...credential,
311
- counter: Math.max(newCounter, credential.counter),
312
- },
313
- encryptedVaultKey: { ciphertext, iv },
314
- keyDerivation,
315
- };
316
- });
317
- }
318
- finally {
319
- // delete registration ceremony
320
- __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
321
- }
277
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_protectVaultKeyWithPasskey).call(this, params));
322
278
  }
323
279
  /**
324
280
  * Verifies an authentication assertion and returns the decrypted vault key.
325
281
  *
282
+ * Prefer orchestrated methods ({@link unlockWithPasskey},
283
+ * {@link exportSeedPhraseWithPasskey}, {@link exportAccountsWithPasskey}) for product
284
+ * flows instead of calling KeyringController with the returned key manually.
285
+ *
326
286
  * @param authenticationResponse - Result of `navigator.credentials.get()`.
327
287
  * @returns The plaintext vault encryption key.
328
288
  */
329
289
  async retrieveVaultKeyWithPasskey(authenticationResponse) {
330
- const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
331
- // verify authentication response and update counter
332
- const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, passkeyRecord.credential);
333
- this.update((state) => {
334
- if (!state.passkeyRecord) {
335
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, { code: constants_1.PasskeyControllerErrorCode.NotEnrolled });
290
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_retrieveVaultKeyWithPasskey).call(this, authenticationResponse));
291
+ }
292
+ /**
293
+ * Unlocks the keyring using a passkey authentication assertion.
294
+ *
295
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
296
+ * @returns Resolves when the keyring is unlocked.
297
+ */
298
+ async unlockWithPasskey(authenticationResponse) {
299
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, async () => {
300
+ const vaultKey = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_retrieveVaultKeyWithPasskey).call(this, authenticationResponse);
301
+ await this.messenger.call('KeyringController:submitEncryptionKey', vaultKey);
302
+ });
303
+ }
304
+ /**
305
+ * Exports the seed phrase after passkey step-up authentication.
306
+ *
307
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
308
+ * @param keyringId - Optional keyring id; defaults to the primary HD keyring.
309
+ * @returns Raw seed phrase bytes from KeyringController.
310
+ */
311
+ async exportSeedPhraseWithPasskey(authenticationResponse, keyringId) {
312
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, async () => {
313
+ const vaultKey = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_retrieveVaultKeyWithPasskey).call(this, authenticationResponse);
314
+ return await this.messenger.call('KeyringController:exportSeedPhrase', { encryptionKey: vaultKey }, keyringId);
315
+ });
316
+ }
317
+ /**
318
+ * Exports private keys for the given addresses after passkey step-up authentication.
319
+ *
320
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
321
+ * @param addresses - Account addresses to export.
322
+ * @returns Private keys in the same order as `addresses`.
323
+ */
324
+ async exportAccountsWithPasskey(authenticationResponse, addresses) {
325
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, async () => {
326
+ const vaultKey = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_retrieveVaultKeyWithPasskey).call(this, authenticationResponse);
327
+ const privateKeys = [];
328
+ for (const address of addresses) {
329
+ privateKeys.push(await this.messenger.call('KeyringController:exportAccount', { encryptionKey: vaultKey }, address));
336
330
  }
337
- state.passkeyRecord.credential.counter = Math.max(newCounter, state.passkeyRecord.credential.counter);
331
+ return privateKeys;
338
332
  });
339
- // derive key
340
- const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
341
- // decrypt vault key
342
- try {
343
- const vaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
344
- return vaultKey;
345
- }
346
- catch (cause) {
347
- log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
348
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
349
- code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
350
- cause: cause instanceof Error ? cause : new Error(String(cause)),
351
- });
352
- }
353
333
  }
354
334
  /**
355
335
  * Checks whether the given authentication assertion is valid for the enrolled passkey.
@@ -361,16 +341,7 @@ class PasskeyController extends base_controller_1.BaseController {
361
341
  * @returns `true` if verification succeeds, otherwise `false`.
362
342
  */
363
343
  async verifyPasskeyAuthentication(authenticationResponse) {
364
- try {
365
- await this.retrieveVaultKeyWithPasskey(authenticationResponse);
366
- return true;
367
- }
368
- catch (error) {
369
- if (error instanceof errors_1.PasskeyControllerError && error.code !== undefined) {
370
- return false;
371
- }
372
- throw error;
373
- }
344
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyPasskeyAuthentication).call(this, authenticationResponse));
374
345
  }
375
346
  /**
376
347
  * Re-wraps the vault key after rotation. Updates persisted `encryptedVaultKey` on success.
@@ -380,59 +351,62 @@ class PasskeyController extends base_controller_1.BaseController {
380
351
  * pass the same `authenticationResponse` you just verified (e.g. from
381
352
  * {@link retrieveVaultKeyWithPasskey} / {@link verifyPasskeyAuthentication}).
382
353
  *
354
+ * For password change with passkey step-up, prefer
355
+ * {@link changePasswordWithPasskeyVerification}, which orchestrates keyring export,
356
+ * `changePassword`, and re-wrap in one call.
357
+ *
383
358
  * @param params - Re-wrap inputs.
384
359
  * @param params.authenticationResponse - Used to derive the wrapping key.
385
360
  * @param params.oldVaultKey - Expected current vault key.
386
361
  * @param params.newVaultKey - New vault key to encrypt under the passkey.
362
+ * @returns Resolves when the passkey record is updated.
387
363
  */
388
364
  async renewVaultKeyProtection(params) {
389
- const { authenticationResponse } = params;
390
- const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
391
- // derive key
392
- const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
393
- // decrypt vault key
394
- let decryptedVaultKey;
395
- try {
396
- decryptedVaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
397
- }
398
- catch (error) {
399
- log('Error decrypting vault key during passkey vault key renewal', error instanceof Error ? error : new Error(String(error)));
400
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
401
- code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
402
- cause: error instanceof Error ? error : new Error(String(error)),
403
- });
404
- }
405
- // check if vault key matches
406
- const { oldVaultKey, newVaultKey } = params;
407
- if (!(0, utils_1.areUint8ArraysEqual)((0, utils_1.stringToBytes)(decryptedVaultKey), (0, utils_1.stringToBytes)(oldVaultKey))) {
408
- log('Passkey renewal rejected: decrypted vault key does not match oldVaultKey');
409
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyMismatch, { code: constants_1.PasskeyControllerErrorCode.VaultKeyMismatch });
410
- }
411
- // encrypt new vault key
412
- const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(newVaultKey, encKey);
413
- // persist passkey record (mutate current state only for vault key material)
414
- this.update((state) => {
415
- if (!state.passkeyRecord) {
416
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
417
- code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
418
- });
419
- }
420
- state.passkeyRecord.encryptedVaultKey = { ciphertext, iv };
421
- });
365
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_renewVaultKeyProtection).call(this, params));
422
366
  }
423
367
  /**
424
- * Clears enrolled passkey state and in-flight ceremonies. Call only after the same
425
- * auth gate as renewal (verified passkey assertion or password).
368
+ * Changes the wallet password after passkey step-up authentication.
369
+ *
370
+ * When `renewVaultKeyProtection` is `true` (default), re-wraps the vault key under the
371
+ * passkey after rotation. When `false`, removes the passkey instead.
372
+ *
373
+ * @param params - Change-password inputs.
374
+ * @param params.newPassword - New wallet password.
375
+ * @param params.authenticationResponse - Result of `navigator.credentials.get()`.
376
+ * @param params.options - Optional flow controls.
377
+ * @param params.options.renewVaultKeyProtection - Re-wrap vault key after password change.
378
+ * @returns Resolves when the password change completes.
426
379
  */
427
- removePasskey() {
428
- this.update(() => getDefaultPasskeyControllerState());
429
- __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
380
+ async changePasswordWithPasskeyVerification(params) {
381
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_changePasswordWithPasskeyVerification).call(this, params));
382
+ }
383
+ /**
384
+ * Removes the enrolled passkey after verifying a passkey authentication assertion.
385
+ *
386
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
387
+ * @returns Resolves when the passkey is removed.
388
+ */
389
+ async removePasskeyWithPasskeyVerification(authenticationResponse) {
390
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskeyWithPasskeyVerification).call(this, authenticationResponse));
391
+ }
392
+ /**
393
+ * Removes the enrolled passkey after verifying the wallet password.
394
+ *
395
+ * @param password - Wallet password for step-up verification.
396
+ * @returns Resolves when the passkey is removed.
397
+ */
398
+ async removePasskeyWithPasswordVerification(password) {
399
+ return __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_withOperationLock).call(this, () => __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskeyWithPasswordVerification).call(this, password));
430
400
  }
431
401
  /**
432
402
  * Resets state and clears in-flight registration/authentication ceremonies.
403
+ *
404
+ * For user-facing passkey removal with step-up, use
405
+ * {@link removePasskeyWithPasskeyVerification} or
406
+ * {@link removePasskeyWithPasswordVerification}.
433
407
  */
434
408
  clearState() {
435
- this.removePasskey();
409
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskey).call(this);
436
410
  }
437
411
  /**
438
412
  * Releases all in-flight ceremony state and tears down the messenger.
@@ -443,16 +417,188 @@ class PasskeyController extends base_controller_1.BaseController {
443
417
  }
444
418
  }
445
419
  exports.PasskeyController = PasskeyController;
446
- _PasskeyController_ceremonyManager = new WeakMap(), _PasskeyController_expectedRPIDs = new WeakMap(), _PasskeyController_rpId = new WeakMap(), _PasskeyController_rpName = new WeakMap(), _PasskeyController_expectedOrigin = new WeakMap(), _PasskeyController_userName = new WeakMap(), _PasskeyController_userDisplayName = new WeakMap(), _PasskeyController_instances = new WeakSet(), _PasskeyController_requireEnrolled = function _PasskeyController_requireEnrolled() {
447
- const record = this.state.passkeyRecord;
448
- if (!record) {
449
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
450
- code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
420
+ _PasskeyController_ceremonyManager = new WeakMap(), _PasskeyController_expectedRPIDs = new WeakMap(), _PasskeyController_rpId = new WeakMap(), _PasskeyController_rpName = new WeakMap(), _PasskeyController_expectedOrigin = new WeakMap(), _PasskeyController_userName = new WeakMap(), _PasskeyController_userDisplayName = new WeakMap(), _PasskeyController_getIsOnboardingCompleted = new WeakMap(), _PasskeyController_operationMutex = new WeakMap(), _PasskeyController_instances = new WeakSet(), _PasskeyController_protectVaultKeyWithPasskey = async function _PasskeyController_protectVaultKeyWithPasskey(params) {
421
+ if (this.isPasskeyEnrolled()) {
422
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AlreadyEnrolled, { code: constants_1.PasskeyControllerErrorCode.AlreadyEnrolled });
423
+ }
424
+ await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_assertEnrollmentAllowed).call(this, params.password);
425
+ const vaultKey = await this.messenger.call('KeyringController:exportEncryptionKey');
426
+ const { registrationResponse, authenticationResponse } = params;
427
+ // get registration ceremony
428
+ const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
429
+ const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
430
+ if (!registrationCeremony) {
431
+ log('No active passkey registration ceremony for challenge');
432
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoRegistrationCeremony });
433
+ }
434
+ try {
435
+ // verify registration response
436
+ const { verified, registrationInfo } = await (0, verify_registration_response_1.verifyRegistrationResponse)({
437
+ response: registrationResponse,
438
+ expectedChallenge: registrationCeremony.challenge,
439
+ expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
440
+ expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
441
+ requireUserVerification: true,
442
+ }).catch((error) => {
443
+ log('Error verifying passkey registration response', error);
444
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, {
445
+ code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed,
446
+ cause: error instanceof Error ? error : new Error(String(error)),
447
+ });
448
+ });
449
+ if (!verified || !registrationInfo) {
450
+ log('Passkey registration verification returned unverified or missing registration info');
451
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed });
452
+ }
453
+ // verify authentication response
454
+ const credential = {
455
+ id: registrationInfo.credentialId,
456
+ publicKey: (0, encoding_1.bytesToBase64URL)(registrationInfo.publicKey),
457
+ counter: registrationInfo.counter,
458
+ transports: registrationInfo.transports,
459
+ aaguid: registrationInfo.aaguid,
460
+ };
461
+ const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, credential);
462
+ // determine key derivation method
463
+ const prfFirst = authenticationResponse.clientExtensionResults?.prf?.results?.first;
464
+ const authHasPrfOutput = typeof prfFirst === 'string' && prfFirst.length > 0;
465
+ const keyDerivation = authHasPrfOutput && registrationCeremony.prfSalt
466
+ ? { method: 'prf', prfSalt: registrationCeremony.prfSalt }
467
+ : { method: 'userHandle' };
468
+ if (keyDerivation.method === 'userHandle' &&
469
+ authenticationResponse.response.userHandle !==
470
+ registrationCeremony.userHandle) {
471
+ log('Post-registration assertion userHandle does not match registration ceremony');
472
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed });
473
+ }
474
+ // derive key and encrypt vault key
475
+ const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, { credential, keyDerivation });
476
+ const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(vaultKey, encKey);
477
+ // persist passkey record
478
+ this.update((state) => {
479
+ state.passkeyRecord = {
480
+ credential: {
481
+ ...credential,
482
+ counter: Math.max(newCounter, credential.counter),
483
+ },
484
+ encryptedVaultKey: { ciphertext, iv },
485
+ keyDerivation,
486
+ };
451
487
  });
452
488
  }
453
- return record;
454
- }, _PasskeyController_getChallengeFromClientData = function _PasskeyController_getChallengeFromClientData(clientDataJSON) {
455
- return (0, decode_client_data_json_1.decodeClientDataJSON)(clientDataJSON).challenge;
489
+ finally {
490
+ // delete registration ceremony
491
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
492
+ }
493
+ }, _PasskeyController_retrieveVaultKeyWithPasskey = async function _PasskeyController_retrieveVaultKeyWithPasskey(authenticationResponse) {
494
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
495
+ // verify authentication response and update counter
496
+ const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, passkeyRecord.credential);
497
+ this.update((state) => {
498
+ if (!state.passkeyRecord) {
499
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, { code: constants_1.PasskeyControllerErrorCode.NotEnrolled });
500
+ }
501
+ state.passkeyRecord.credential.counter = Math.max(newCounter, state.passkeyRecord.credential.counter);
502
+ });
503
+ // derive key
504
+ const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
505
+ // decrypt vault key
506
+ try {
507
+ const vaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
508
+ return vaultKey;
509
+ }
510
+ catch (cause) {
511
+ log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
512
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
513
+ code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
514
+ cause: cause instanceof Error ? cause : new Error(String(cause)),
515
+ });
516
+ }
517
+ }, _PasskeyController_verifyPasskeyAuthentication = async function _PasskeyController_verifyPasskeyAuthentication(authenticationResponse) {
518
+ try {
519
+ await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_retrieveVaultKeyWithPasskey).call(this, authenticationResponse);
520
+ return true;
521
+ }
522
+ catch (error) {
523
+ if (error instanceof errors_1.PasskeyControllerError && error.code !== undefined) {
524
+ return false;
525
+ }
526
+ throw error;
527
+ }
528
+ }, _PasskeyController_renewVaultKeyProtection = async function _PasskeyController_renewVaultKeyProtection(params) {
529
+ const { authenticationResponse } = params;
530
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
531
+ // derive key
532
+ const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
533
+ // decrypt vault key
534
+ let decryptedVaultKey;
535
+ try {
536
+ decryptedVaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
537
+ }
538
+ catch (error) {
539
+ log('Error decrypting vault key during passkey vault key renewal', error instanceof Error ? error : new Error(String(error)));
540
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
541
+ code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
542
+ cause: error instanceof Error ? error : new Error(String(error)),
543
+ });
544
+ }
545
+ // check if vault key matches
546
+ const { oldVaultKey, newVaultKey } = params;
547
+ if (!(0, utils_1.areUint8ArraysEqual)((0, utils_1.stringToBytes)(decryptedVaultKey), (0, utils_1.stringToBytes)(oldVaultKey))) {
548
+ log('Passkey renewal rejected: decrypted vault key does not match oldVaultKey');
549
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyMismatch, { code: constants_1.PasskeyControllerErrorCode.VaultKeyMismatch });
550
+ }
551
+ // encrypt new vault key
552
+ const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(newVaultKey, encKey);
553
+ // persist passkey record (mutate current state only for vault key material)
554
+ this.update((state) => {
555
+ if (!state.passkeyRecord) {
556
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
557
+ code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
558
+ });
559
+ }
560
+ state.passkeyRecord.encryptedVaultKey = { ciphertext, iv };
561
+ });
562
+ }, _PasskeyController_changePasswordWithPasskeyVerification = async function _PasskeyController_changePasswordWithPasskeyVerification(params) {
563
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
564
+ const verified = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyPasskeyAuthentication).call(this, params.authenticationResponse);
565
+ if (!verified) {
566
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed });
567
+ }
568
+ const renewVaultKeyProtection = params.options?.renewVaultKeyProtection ?? true;
569
+ if (!renewVaultKeyProtection) {
570
+ await this.messenger.call('KeyringController:changePassword', params.newPassword);
571
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskey).call(this);
572
+ return;
573
+ }
574
+ const vaultKeyBefore = await this.messenger.call('KeyringController:exportEncryptionKey');
575
+ await this.messenger.call('KeyringController:changePassword', params.newPassword);
576
+ try {
577
+ const vaultKeyAfter = await this.messenger.call('KeyringController:exportEncryptionKey');
578
+ await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_renewVaultKeyProtection).call(this, {
579
+ authenticationResponse: params.authenticationResponse,
580
+ oldVaultKey: vaultKeyBefore,
581
+ newVaultKey: vaultKeyAfter,
582
+ });
583
+ }
584
+ catch (error) {
585
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskey).call(this);
586
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyRenewalFailed, {
587
+ code: constants_1.PasskeyControllerErrorCode.VaultKeyRenewalFailed,
588
+ cause: error instanceof Error ? error : new Error(String(error)),
589
+ });
590
+ }
591
+ }, _PasskeyController_removePasskeyWithPasskeyVerification = async function _PasskeyController_removePasskeyWithPasskeyVerification(authenticationResponse) {
592
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
593
+ const verified = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyPasskeyAuthentication).call(this, authenticationResponse);
594
+ if (!verified) {
595
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed });
596
+ }
597
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskey).call(this);
598
+ }, _PasskeyController_removePasskeyWithPasswordVerification = async function _PasskeyController_removePasskeyWithPasswordVerification(password) {
599
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
600
+ await this.messenger.call('KeyringController:verifyPassword', password);
601
+ __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_removePasskey).call(this);
456
602
  }, _PasskeyController_verifyAuthenticationResponse =
457
603
  /**
458
604
  * Validates a WebAuthn authentication response against stored credential data.
@@ -503,5 +649,37 @@ async function _PasskeyController_verifyAuthenticationResponse(authenticationRes
503
649
  // delete authentication ceremony
504
650
  __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
505
651
  }
652
+ }, _PasskeyController_withOperationLock =
653
+ /**
654
+ * Serializes orchestrated passkey operations that mutate state or call KeyringController.
655
+ *
656
+ * @param callback - Operation to run while the mutex is held.
657
+ * @returns The result of the callback.
658
+ */
659
+ async function _PasskeyController_withOperationLock(callback) {
660
+ return __classPrivateFieldGet(this, _PasskeyController_operationMutex, "f").runExclusive(callback);
661
+ }, _PasskeyController_assertEnrollmentAllowed = async function _PasskeyController_assertEnrollmentAllowed(password) {
662
+ if (!__classPrivateFieldGet(this, _PasskeyController_getIsOnboardingCompleted, "f").call(this)) {
663
+ return;
664
+ }
665
+ if (!password) {
666
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.EnrollmentPasswordRequired, {
667
+ code: constants_1.PasskeyControllerErrorCode.EnrollmentPasswordRequired,
668
+ });
669
+ }
670
+ await this.messenger.call('KeyringController:verifyPassword', password);
671
+ }, _PasskeyController_requireEnrolled = function _PasskeyController_requireEnrolled() {
672
+ const record = this.state.passkeyRecord;
673
+ if (!record) {
674
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
675
+ code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
676
+ });
677
+ }
678
+ return record;
679
+ }, _PasskeyController_getChallengeFromClientData = function _PasskeyController_getChallengeFromClientData(clientDataJSON) {
680
+ return (0, decode_client_data_json_1.decodeClientDataJSON)(clientDataJSON).challenge;
681
+ }, _PasskeyController_removePasskey = function _PasskeyController_removePasskey() {
682
+ this.update(() => getDefaultPasskeyControllerState());
683
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
506
684
  };
507
685
  //# sourceMappingURL=PasskeyController.cjs.map