@metamask-previews/passkey-controller 2.1.0-preview-eade524 → 2.1.0-preview-355b70c

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