@metamask-previews/passkey-controller 1.0.0-preview-53e60e1a5 → 1.0.0-preview-4e0ae1bc9

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 (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +30 -13
  3. package/dist/PasskeyController.cjs +178 -120
  4. package/dist/PasskeyController.cjs.map +1 -1
  5. package/dist/PasskeyController.d.cts +61 -60
  6. package/dist/PasskeyController.d.cts.map +1 -1
  7. package/dist/PasskeyController.d.mts +61 -60
  8. package/dist/PasskeyController.d.mts.map +1 -1
  9. package/dist/PasskeyController.mjs +180 -122
  10. package/dist/PasskeyController.mjs.map +1 -1
  11. package/dist/ceremony-manager.cjs +3 -2
  12. package/dist/ceremony-manager.cjs.map +1 -1
  13. package/dist/ceremony-manager.d.cts +3 -2
  14. package/dist/ceremony-manager.d.cts.map +1 -1
  15. package/dist/ceremony-manager.d.mts +3 -2
  16. package/dist/ceremony-manager.d.mts.map +1 -1
  17. package/dist/ceremony-manager.mjs +3 -2
  18. package/dist/ceremony-manager.mjs.map +1 -1
  19. package/dist/constants.cjs +2 -0
  20. package/dist/constants.cjs.map +1 -1
  21. package/dist/constants.d.cts +2 -0
  22. package/dist/constants.d.cts.map +1 -1
  23. package/dist/constants.d.mts +2 -0
  24. package/dist/constants.d.mts.map +1 -1
  25. package/dist/constants.mjs +2 -0
  26. package/dist/constants.mjs.map +1 -1
  27. package/dist/key-derivation.cjs +3 -35
  28. package/dist/key-derivation.cjs.map +1 -1
  29. package/dist/key-derivation.d.cts +5 -28
  30. package/dist/key-derivation.d.cts.map +1 -1
  31. package/dist/key-derivation.d.mts +5 -28
  32. package/dist/key-derivation.d.mts.map +1 -1
  33. package/dist/key-derivation.mjs +2 -33
  34. package/dist/key-derivation.mjs.map +1 -1
  35. package/dist/types.cjs.map +1 -1
  36. package/dist/types.d.cts +1 -1
  37. package/dist/types.d.cts.map +1 -1
  38. package/dist/types.d.mts +1 -1
  39. package/dist/types.d.mts.map +1 -1
  40. package/dist/types.mjs.map +1 -1
  41. package/dist/utils/crypto.cjs +12 -1
  42. package/dist/utils/crypto.cjs.map +1 -1
  43. package/dist/utils/crypto.d.cts +7 -0
  44. package/dist/utils/crypto.d.cts.map +1 -1
  45. package/dist/utils/crypto.d.mts +7 -0
  46. package/dist/utils/crypto.d.mts.map +1 -1
  47. package/dist/utils/crypto.mjs +10 -0
  48. package/dist/utils/crypto.mjs.map +1 -1
  49. package/dist/webauthn/types.cjs.map +1 -1
  50. package/dist/webauthn/types.d.cts +1 -1
  51. package/dist/webauthn/types.d.cts.map +1 -1
  52. package/dist/webauthn/types.d.mts +1 -1
  53. package/dist/webauthn/types.d.mts.map +1 -1
  54. package/dist/webauthn/types.mjs.map +1 -1
  55. package/dist/webauthn/verify-authentication-response.cjs +3 -4
  56. package/dist/webauthn/verify-authentication-response.cjs.map +1 -1
  57. package/dist/webauthn/verify-authentication-response.d.cts +3 -2
  58. package/dist/webauthn/verify-authentication-response.d.cts.map +1 -1
  59. package/dist/webauthn/verify-authentication-response.d.mts +3 -2
  60. package/dist/webauthn/verify-authentication-response.d.mts.map +1 -1
  61. package/dist/webauthn/verify-authentication-response.mjs +3 -4
  62. package/dist/webauthn/verify-authentication-response.mjs.map +1 -1
  63. package/dist/webauthn/verify-registration-response.cjs +8 -6
  64. package/dist/webauthn/verify-registration-response.cjs.map +1 -1
  65. package/dist/webauthn/verify-registration-response.d.cts +5 -5
  66. package/dist/webauthn/verify-registration-response.d.cts.map +1 -1
  67. package/dist/webauthn/verify-registration-response.d.mts +5 -5
  68. package/dist/webauthn/verify-registration-response.d.mts.map +1 -1
  69. package/dist/webauthn/verify-registration-response.mjs +8 -6
  70. package/dist/webauthn/verify-registration-response.mjs.map +1 -1
  71. package/package.json +1 -1
@@ -9,16 +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 _PasskeyController_instances, _PasskeyController_ceremonyManager, _PasskeyController_rpID, _PasskeyController_rpName, _PasskeyController_expectedOrigin, _PasskeyController_userName, _PasskeyController_userDisplayName, _PasskeyController_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_verifyAuthenticationResponse;
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 { randomBytes } from "@noble/ciphers/webcrypto";
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";
19
- import { deriveKeyFromAuthenticationResponse, deriveKeyFromRegistrationResponse } from "./key-derivation.mjs";
18
+ import { deriveKeyFromAuthenticationResponse } from "./key-derivation.mjs";
20
19
  import { createModuleLogger, projectLogger } from "./logger.mjs";
21
- import { decryptWithKey, encryptWithKey } from "./utils/crypto.mjs";
20
+ import { decryptWithKey, encryptWithKey, randomBytesToBase64URL } from "./utils/crypto.mjs";
22
21
  import { base64URLToBytes, bytesToBase64URL } from "./utils/encoding.mjs";
23
22
  import { COSEALG } from "./webauthn/constants.mjs";
24
23
  import { decodeClientDataJSON } from "./webauthn/decode-client-data-json.mjs";
@@ -52,32 +51,27 @@ export const passkeyControllerSelectors = {
52
51
  selectIsPasskeyEnrolled: (state) => state.passkeyRecord !== null,
53
52
  };
54
53
  /**
55
- * Passkey-based protection for the vault encryption key (WebAuthn).
56
- *
57
- * Uses PRF-backed derivation when available; otherwise uses the credential
58
- * `userHandle`.
54
+ * Controller that enrolls a WebAuthn passkey and uses it to protect and unlock
55
+ * the vault encryption key.
59
56
  */
60
57
  export class PasskeyController extends BaseController {
61
58
  /**
62
- * Constructs a new {@link PasskeyController}.
59
+ * Creates a passkey controller with WebAuthn relying-party settings.
63
60
  *
64
- * @param args - The constructor arguments.
65
- * @param args.messenger - The messenger suited for this controller.
66
- * @param args.state - Initial state. Missing properties are filled in with
67
- * defaults from {@link getDefaultPasskeyControllerState}.
68
- * @param args.rpID - WebAuthn Relying Party ID (typically the eTLD+1 of the
69
- * client origin, or `localhost` in dev).
70
- * @param args.rpName - Human-readable Relying Party name shown by the OS
71
- * passkey UI.
72
- * @param args.expectedOrigin - One or more acceptable origins for the
73
- * `clientDataJSON.origin` check (e.g. `chrome-extension://...`).
74
- * @param args.userName - Optional `user.name` shown by the OS passkey UI.
75
- * Defaults to `rpName` so client builds (Stable, Flask, etc.) can
76
- * differentiate without changes here.
77
- * @param args.userDisplayName - Optional `user.displayName` shown by the OS
78
- * passkey UI. Defaults to `rpName`.
61
+ * @param args - Constructor options.
62
+ * @param args.messenger - Controller messenger.
63
+ * @param args.state - Partial initial state; merged with {@link getDefaultPasskeyControllerState}.
64
+ * @param args.expectedRPID - Relying party ID(s) for verification (SHA-256 hash match in
65
+ * authenticator data). Pass a string or array of strings; an empty array skips RP ID
66
+ * allowlist checks in {@link verifyRegistrationResponse} / {@link verifyAuthenticationResponse}.
67
+ * @param args.rpId - When set, included as `rp.id` on registration options and `rpId` on
68
+ * authentication options. When omitted, those fields are left unset (client default RP ID).
69
+ * @param args.rpName - Relying party name shown in the platform passkey UI.
70
+ * @param args.expectedOrigin - Allowed value(s) for the WebAuthn client origin.
71
+ * @param args.userName - Optional passkey user name; defaults to `rpName`.
72
+ * @param args.userDisplayName - Optional display name; defaults to `rpName`.
79
73
  */
80
- constructor({ messenger, state = {}, rpID, rpName, expectedOrigin, userName, userDisplayName, }) {
74
+ constructor({ messenger, state = {}, rpId, expectedRPID, rpName, expectedOrigin, userName, userDisplayName, }) {
81
75
  super({
82
76
  messenger,
83
77
  metadata: passkeyControllerMetadata,
@@ -86,47 +80,54 @@ export class PasskeyController extends BaseController {
86
80
  });
87
81
  _PasskeyController_instances.add(this);
88
82
  _PasskeyController_ceremonyManager.set(this, new CeremonyManager());
89
- _PasskeyController_rpID.set(this, void 0);
83
+ _PasskeyController_expectedRPIDs.set(this, void 0);
84
+ _PasskeyController_rpId.set(this, void 0);
90
85
  _PasskeyController_rpName.set(this, void 0);
91
86
  _PasskeyController_expectedOrigin.set(this, void 0);
92
87
  _PasskeyController_userName.set(this, void 0);
93
88
  _PasskeyController_userDisplayName.set(this, void 0);
94
- __classPrivateFieldSet(this, _PasskeyController_rpID, rpID, "f");
89
+ const expectedRPIDs = Array.isArray(expectedRPID)
90
+ ? expectedRPID
91
+ : [expectedRPID];
92
+ __classPrivateFieldSet(this, _PasskeyController_expectedRPIDs, [...expectedRPIDs], "f");
93
+ __classPrivateFieldSet(this, _PasskeyController_rpId, rpId, "f");
95
94
  __classPrivateFieldSet(this, _PasskeyController_rpName, rpName, "f");
96
95
  __classPrivateFieldSet(this, _PasskeyController_expectedOrigin, expectedOrigin, "f");
97
96
  __classPrivateFieldSet(this, _PasskeyController_userName, userName ?? rpName, "f");
98
97
  __classPrivateFieldSet(this, _PasskeyController_userDisplayName, userDisplayName ?? rpName, "f");
99
98
  }
100
99
  /**
101
- * Checks if the passkey is enrolled.
100
+ * Whether a passkey is enrolled and vault key material is stored.
102
101
  *
103
- * @returns Whether the passkey is enrolled.
102
+ * @returns `true` if enrolled, otherwise `false`.
104
103
  */
105
104
  isPasskeyEnrolled() {
106
105
  return passkeyControllerSelectors.selectIsPasskeyEnrolled(this.state);
107
106
  }
108
107
  /**
109
- * Registration options for enrolling a passkey.
110
- *
111
- * Call before {@link protectVaultKeyWithPasskey}.
108
+ * Builds WebAuthn credential creation options for passkey enrollment.
112
109
  *
113
- * @param creationOptionsConfig - Optional configuration.
114
- * @param creationOptionsConfig.prfAvailable - Omit PRF when `false`. Default `true`.
115
- * @returns Options for `navigator.credentials.create()`.
110
+ * @param creationOptionsConfig - Optional creation behavior.
111
+ * @param creationOptionsConfig.prfAvailable - Request the PRF extension unless `false`. Defaults to `true`.
112
+ * @returns Public key credential creation options for `navigator.credentials.create()`.
116
113
  */
117
114
  generateRegistrationOptions(creationOptionsConfig) {
115
+ if (this.isPasskeyEnrolled()) {
116
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.AlreadyEnrolled, { code: PasskeyControllerErrorCode.AlreadyEnrolled });
117
+ }
118
118
  const includePrf = creationOptionsConfig?.prfAvailable !== false;
119
- const prfSalt = includePrf
120
- ? bytesToBase64URL(randomBytes(32).slice())
121
- : undefined;
122
- const userHandle = bytesToBase64URL(randomBytes(64).slice());
123
- const challenge = bytesToBase64URL(randomBytes(32).slice());
119
+ const prfSalt = includePrf ? randomBytesToBase64URL(32) : undefined;
120
+ const userHandle = randomBytesToBase64URL(64);
121
+ const challenge = randomBytesToBase64URL(32);
124
122
  const extensions = {};
125
123
  if (prfSalt) {
126
124
  extensions.prf = { eval: { first: prfSalt } };
127
125
  }
128
126
  const options = {
129
- rp: { name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"), id: __classPrivateFieldGet(this, _PasskeyController_rpID, "f") },
127
+ rp: {
128
+ name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"),
129
+ id: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
130
+ },
130
131
  user: {
131
132
  id: userHandle,
132
133
  name: __classPrivateFieldGet(this, _PasskeyController_userName, "f"),
@@ -150,30 +151,72 @@ export class PasskeyController extends BaseController {
150
151
  };
151
152
  __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveRegistrationCeremony(challenge, {
152
153
  userHandle,
153
- prfSalt: prfSalt ?? '',
154
+ prfSalt,
154
155
  challenge,
155
156
  createdAt: Date.now(),
156
157
  });
157
158
  return options;
158
159
  }
159
160
  /**
160
- * WebAuthn request options for authenticating with the enrolled passkey.
161
+ * Builds WebAuthn credential request options for the post-registration
162
+ * authentication step (between `create` and {@link protectVaultKeyWithPasskey}).
161
163
  *
162
- * Call before {@link retrieveVaultKeyWithPasskey},
163
- * {@link verifyPasskeyAuthentication}, or {@link renewVaultKeyProtection}.
164
+ * @param params - Input for the pending registration ceremony.
165
+ * @param params.registrationResponse - Result of `navigator.credentials.create()`.
166
+ * @returns Public key credential request options for `navigator.credentials.get()`.
167
+ */
168
+ generatePostRegistrationAuthenticationOptions(params) {
169
+ // get registration ceremony
170
+ const { registrationResponse } = params;
171
+ const regChallenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
172
+ const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(regChallenge);
173
+ if (!registrationCeremony) {
174
+ log('No active passkey registration ceremony for challenge');
175
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: PasskeyControllerErrorCode.NoRegistrationCeremony });
176
+ }
177
+ // build auth options
178
+ const challenge = randomBytesToBase64URL(32);
179
+ const extensions = {};
180
+ if (registrationCeremony.prfSalt) {
181
+ extensions.prf = { eval: { first: registrationCeremony.prfSalt } };
182
+ }
183
+ const options = {
184
+ challenge,
185
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
186
+ allowCredentials: [
187
+ {
188
+ id: registrationResponse.id,
189
+ type: 'public-key',
190
+ transports: registrationResponse.response.transports,
191
+ },
192
+ ],
193
+ userVerification: 'preferred',
194
+ hints: ['client-device', 'hybrid'],
195
+ timeout: WEBAUTHN_TIMEOUT_MS,
196
+ extensions,
197
+ };
198
+ // save auth ceremony
199
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveAuthenticationCeremony(challenge, {
200
+ challenge,
201
+ createdAt: Date.now(),
202
+ });
203
+ return options;
204
+ }
205
+ /**
206
+ * Builds WebAuthn credential request options for the enrolled passkey.
164
207
  *
165
- * @returns Options for `navigator.credentials.get()`.
208
+ * @returns Public key credential request options for `navigator.credentials.get()`.
166
209
  */
167
210
  generateAuthenticationOptions() {
168
211
  const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
169
- const challenge = bytesToBase64URL(randomBytes(32).slice());
212
+ const challenge = randomBytesToBase64URL(32);
170
213
  const extensions = {};
171
214
  if (record.keyDerivation.method === 'prf') {
172
215
  extensions.prf = { eval: { first: record.keyDerivation.prfSalt } };
173
216
  }
174
217
  const options = {
175
218
  challenge,
176
- rpId: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
219
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
177
220
  allowCredentials: [
178
221
  {
179
222
  id: record.credential.id,
@@ -193,15 +236,20 @@ export class PasskeyController extends BaseController {
193
236
  return options;
194
237
  }
195
238
  /**
196
- * Completes enrollment and binds the vault key to the new passkey.
239
+ * Verifies registration and post-registration authentication, then stores the
240
+ * vault key encrypted under the new passkey.
197
241
  *
198
- * @param params - Protection parameters.
199
- * @param params.registrationResponse - Credential from `navigator.credentials.create()`.
200
- * @param params.vaultKey - Vault encryption key to protect.
242
+ * @param params - Enrollment completion inputs.
243
+ * @param params.registrationResponse - Result of `navigator.credentials.create()`.
244
+ * @param params.authenticationResponse - Result of `navigator.credentials.get()` after {@link generatePostRegistrationAuthenticationOptions}.
245
+ * @param params.vaultKey - Vault encryption key to encrypt and persist.
201
246
  */
202
247
  async protectVaultKeyWithPasskey(params) {
203
- const { registrationResponse, vaultKey } = params;
204
- // get challenge
248
+ if (this.isPasskeyEnrolled()) {
249
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.AlreadyEnrolled, { code: PasskeyControllerErrorCode.AlreadyEnrolled });
250
+ }
251
+ const { registrationResponse, authenticationResponse, vaultKey } = params;
252
+ // get registration ceremony
205
253
  const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
206
254
  const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
207
255
  if (!registrationCeremony) {
@@ -214,7 +262,7 @@ export class PasskeyController extends BaseController {
214
262
  response: registrationResponse,
215
263
  expectedChallenge: registrationCeremony.challenge,
216
264
  expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
217
- expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
265
+ expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
218
266
  requireUserVerification: false,
219
267
  }).catch((error) => {
220
268
  log('Error verifying passkey registration response', error);
@@ -227,19 +275,36 @@ export class PasskeyController extends BaseController {
227
275
  log('Passkey registration verification returned unverified or missing registration info');
228
276
  throw new PasskeyControllerError(PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: PasskeyControllerErrorCode.RegistrationVerificationFailed });
229
277
  }
230
- // derive key
231
- const { encKey, keyDerivation } = deriveKeyFromRegistrationResponse(registrationResponse, registrationCeremony, registrationInfo.credentialId);
232
- // encrypt vault key
278
+ // verify authentication response
279
+ const credential = {
280
+ id: registrationInfo.credentialId,
281
+ publicKey: bytesToBase64URL(registrationInfo.publicKey),
282
+ counter: registrationInfo.counter,
283
+ transports: registrationInfo.transports,
284
+ aaguid: registrationInfo.aaguid,
285
+ };
286
+ const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, credential);
287
+ // determine key derivation method
288
+ const prfFirst = authenticationResponse.clientExtensionResults?.prf?.results?.first;
289
+ const authHasPrfOutput = typeof prfFirst === 'string' && prfFirst.length > 0;
290
+ const keyDerivation = authHasPrfOutput && registrationCeremony.prfSalt
291
+ ? { method: 'prf', prfSalt: registrationCeremony.prfSalt }
292
+ : { method: 'userHandle' };
293
+ if (keyDerivation.method === 'userHandle' &&
294
+ authenticationResponse.response.userHandle !==
295
+ registrationCeremony.userHandle) {
296
+ log('Post-registration assertion userHandle does not match registration ceremony');
297
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.AuthenticationVerificationFailed, { code: PasskeyControllerErrorCode.AuthenticationVerificationFailed });
298
+ }
299
+ // derive key and encrypt vault key
300
+ const encKey = deriveKeyFromAuthenticationResponse(authenticationResponse, { credential, keyDerivation });
233
301
  const { ciphertext, iv } = encryptWithKey(vaultKey, encKey);
234
302
  // persist passkey record
235
303
  this.update((state) => {
236
304
  state.passkeyRecord = {
237
305
  credential: {
238
- id: registrationInfo.credentialId,
239
- publicKey: bytesToBase64URL(registrationInfo.publicKey),
240
- counter: registrationInfo.counter,
241
- transports: registrationInfo.transports,
242
- aaguid: registrationInfo.aaguid,
306
+ ...credential,
307
+ counter: Math.max(newCounter, credential.counter),
243
308
  },
244
309
  encryptedVaultKey: { ciphertext, iv },
245
310
  keyDerivation,
@@ -247,26 +312,32 @@ export class PasskeyController extends BaseController {
247
312
  });
248
313
  }
249
314
  finally {
315
+ // delete registration ceremony
250
316
  __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
251
317
  }
252
318
  }
253
319
  /**
254
- * Returns the decrypted vault encryption key from the passkey authentication
255
- * response.
320
+ * Verifies an authentication assertion and returns the decrypted vault key.
256
321
  *
257
- * @param authenticationResponse - Credential from `navigator.credentials.get()`.
258
- * @returns The vault encryption key.
322
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
323
+ * @returns The plaintext vault encryption key.
259
324
  */
260
325
  async retrieveVaultKeyWithPasskey(authenticationResponse) {
261
- // verify authentication response
262
- await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse);
263
- // derive key (#verifyAuthenticationResponse guarantees enrolled)
264
326
  const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
327
+ // verify authentication response and update counter
328
+ const { newCounter } = await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse, passkeyRecord.credential);
329
+ this.update((state) => {
330
+ if (!state.passkeyRecord) {
331
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, { code: PasskeyControllerErrorCode.NotEnrolled });
332
+ }
333
+ state.passkeyRecord.credential.counter = Math.max(newCounter, state.passkeyRecord.credential.counter);
334
+ });
335
+ // derive key
265
336
  const encKey = deriveKeyFromAuthenticationResponse(authenticationResponse, passkeyRecord);
266
337
  // decrypt vault key
267
- let vaultKey;
268
338
  try {
269
- vaultKey = decryptWithKey(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
339
+ const vaultKey = decryptWithKey(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
340
+ return vaultKey;
270
341
  }
271
342
  catch (cause) {
272
343
  log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
@@ -275,18 +346,15 @@ export class PasskeyController extends BaseController {
275
346
  cause: cause instanceof Error ? cause : new Error(String(cause)),
276
347
  });
277
348
  }
278
- return vaultKey;
279
349
  }
280
350
  /**
281
- * Returns whether passkey authentication succeeds for this credential (same
282
- * work as {@link retrieveVaultKeyWithPasskey} without exposing the vault key).
351
+ * Checks whether the given authentication assertion is valid for the enrolled passkey.
283
352
  *
284
- * Returns `false` only when the failure is a {@link PasskeyControllerError}
285
- * with a defined `code`. Unexpected errors (e.g. malformed `clientDataJSON`,
286
- * internal bugs) are rethrown.
353
+ * On failure, returns `false` for {@link PasskeyControllerError} with a `code`;
354
+ * other errors propagate.
287
355
  *
288
- * @param authenticationResponse - Credential from `navigator.credentials.get()`.
289
- * @returns `true` if authentication succeeds, otherwise `false`.
356
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
357
+ * @returns `true` if verification succeeds, otherwise `false`.
290
358
  */
291
359
  async verifyPasskeyAuthentication(authenticationResponse) {
292
360
  try {
@@ -301,19 +369,17 @@ export class PasskeyController extends BaseController {
301
369
  }
302
370
  }
303
371
  /**
304
- * Updates the vault encryption key for the same passkey (e.g. after a password change).
372
+ * Re-wraps the vault key after rotation. Updates persisted `encryptedVaultKey` on success.
305
373
  *
306
- * Caller MUST first verify the assertion via {@link verifyPasskeyAuthentication}
307
- * or {@link retrieveVaultKeyWithPasskey}. This method does not re-verify
308
- * because the ceremony is single-use (deleted on verify) and the signature
309
- * counter is advanced (replay would be rejected). Authentication here is
310
- * enforced by the prior verification plus the `oldVaultKey` match below.
374
+ * Does not verify WebAuthn or ceremony state—call only after your layer has authenticated
375
+ * the user (passkey `get()` + verified assertion, or verified password). On passkey paths,
376
+ * pass the same `authenticationResponse` you just verified (e.g. from
377
+ * {@link retrieveVaultKeyWithPasskey} / {@link verifyPasskeyAuthentication}).
311
378
  *
312
- * @param params - Renewal parameters.
313
- * @param params.authenticationResponse - Credential from `navigator.credentials.get()`,
314
- * already verified by the caller.
379
+ * @param params - Re-wrap inputs.
380
+ * @param params.authenticationResponse - Used to derive the wrapping key.
315
381
  * @param params.oldVaultKey - Expected current vault key.
316
- * @param params.newVaultKey - New vault key to protect.
382
+ * @param params.newVaultKey - New vault key to encrypt under the passkey.
317
383
  */
318
384
  async renewVaultKeyProtection(params) {
319
385
  const { authenticationResponse } = params;
@@ -351,7 +417,8 @@ export class PasskeyController extends BaseController {
351
417
  });
352
418
  }
353
419
  /**
354
- * Unenrolls the passkey, removing the protected vault key material.
420
+ * Clears enrolled passkey state and in-flight ceremonies. Call only after the same
421
+ * auth gate as renewal (verified passkey assertion or password).
355
422
  */
356
423
  removePasskey() {
357
424
  this.update(() => getDefaultPasskeyControllerState());
@@ -371,7 +438,7 @@ export class PasskeyController extends BaseController {
371
438
  super.destroy();
372
439
  }
373
440
  }
374
- _PasskeyController_ceremonyManager = 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() {
441
+ _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() {
375
442
  const record = this.state.passkeyRecord;
376
443
  if (!record) {
377
444
  throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, {
@@ -383,34 +450,33 @@ _PasskeyController_ceremonyManager = new WeakMap(), _PasskeyController_rpID = ne
383
450
  return decodeClientDataJSON(clientDataJSON).challenge;
384
451
  }, _PasskeyController_verifyAuthenticationResponse =
385
452
  /**
386
- * Verifies an authentication response for the enrolled passkey.
453
+ * Validates a WebAuthn authentication response against stored credential data.
387
454
  *
388
- * @param authenticationResponse - Authentication result JSON.
455
+ * @param authenticationResponse - Parsed authentication response from the client.
456
+ * @param credential - Credential identifiers and public key material for verification.
457
+ * @returns Updated authenticator signature counter.
389
458
  */
390
- async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse) {
391
- let challenge;
459
+ async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse, credential) {
460
+ // get challenge
461
+ const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
462
+ // get authentication ceremony
463
+ const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
464
+ if (!authenticationCeremony) {
465
+ log('No active passkey authentication ceremony for challenge');
466
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: PasskeyControllerErrorCode.NoAuthenticationCeremony });
467
+ }
392
468
  try {
393
- // get challenge
394
- challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
395
- // get passkey record
396
- const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
397
- // get authentication ceremony
398
- const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
399
- if (!authenticationCeremony) {
400
- log('No active passkey authentication ceremony for challenge');
401
- throw new PasskeyControllerError(PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: PasskeyControllerErrorCode.NoAuthenticationCeremony });
402
- }
403
469
  // verify authentication response
404
470
  const result = await verifyAuthenticationResponse({
405
471
  response: authenticationResponse,
406
472
  expectedChallenge: authenticationCeremony.challenge,
407
473
  expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
408
- expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
474
+ expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
409
475
  credential: {
410
- id: record.credential.id,
411
- publicKey: base64URLToBytes(record.credential.publicKey),
412
- counter: record.credential.counter,
413
- transports: record.credential.transports,
476
+ id: credential.id,
477
+ publicKey: base64URLToBytes(credential.publicKey),
478
+ counter: credential.counter,
479
+ transports: credential.transports,
414
480
  },
415
481
  // UV optional for device compatibility; vault key remains password-gated.
416
482
  requireUserVerification: false,
@@ -427,19 +493,11 @@ async function _PasskeyController_verifyAuthenticationResponse(authenticationRes
427
493
  code: PasskeyControllerErrorCode.AuthenticationVerificationFailed,
428
494
  });
429
495
  }
430
- // persist passkey record with updated counter without clobbering concurrent updates
431
- this.update((state) => {
432
- if (!state.passkeyRecord) {
433
- throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, { code: PasskeyControllerErrorCode.NotEnrolled });
434
- }
435
- const latest = state.passkeyRecord;
436
- latest.credential.counter = Math.max(result.authenticationInfo.newCounter, latest.credential.counter);
437
- });
496
+ return { newCounter: result.authenticationInfo.newCounter };
438
497
  }
439
498
  finally {
440
- if (challenge) {
441
- __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
442
- }
499
+ // delete authentication ceremony
500
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
443
501
  }
444
502
  };
445
503
  //# sourceMappingURL=PasskeyController.mjs.map