@metamask-previews/passkey-controller 1.0.0-preview-95a687acf → 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
package/CHANGELOG.md CHANGED
@@ -7,10 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - `generatePostRegistrationAuthenticationOptions` to issue `navigator.credentials.get()` options after `navigator.credentials.create()`, keyed to the in-flight registration ceremony (including PRF eval when a salt was used) ([#8663](https://github.com/MetaMask/core/pull/8663))
13
+ - `already_enrolled` (`PasskeyControllerErrorCode.AlreadyEnrolled`) when calling `protectVaultKeyWithPasskey` while a passkey is already enrolled ([#8663](https://github.com/MetaMask/core/pull/8663))
14
+
10
15
  ### Changed
11
16
 
17
+ - **BREAKING:** Enrollment completes in three steps: `generateRegistrationOptions` → `create()` → `generatePostRegistrationAuthenticationOptions` → `get()` → `protectVaultKeyWithPasskey`; `protectVaultKeyWithPasskey` now **requires** `authenticationResponse`, and the vault wrapping key is derived from that post-registration assertion (same path as unlock: PRF when present, otherwise `userHandle`) ([#8663](https://github.com/MetaMask/core/pull/8663))
18
+ - **BREAKING:** `PasskeyController` constructor option `rpID` is replaced with `expectedRPID: string | string[]` (normalized to a string array, which may be empty). Optional `rpId` sets `rp.id` / `rpId` in generated WebAuthn options; when omitted, those fields are omitted. Verification passes that array to `verifyRegistrationResponse` / `verifyAuthenticationResponse` as `expectedRPIDs` ([#8663](https://github.com/MetaMask/core/pull/8663))
19
+ - **BREAKING:** `verifyRegistrationResponse` and `verifyAuthenticationResponse` now take `expectedRPIDs: string[]` instead of `expectedRPID: string` ([#8663](https://github.com/MetaMask/core/pull/8663))
20
+ - `verifyRegistrationResponse` / `verifyAuthenticationResponse` accept an empty `expectedRPIDs` array to skip RP ID hash allowlist matching; successful authentication then reports `authenticationInfo.rpID` as an empty string ([#8663](https://github.com/MetaMask/core/pull/8663))
21
+ - Increase `CEREMONY_TTL_SLACK_MS` to 2 minutes so in-flight ceremony state (`CEREMONY_MAX_AGE_MS`, 3 minutes including WebAuthn timeout) tolerates longer gaps between WebAuthn options and completion (e.g. post-registration authentication) ([#8663](https://github.com/MetaMask/core/pull/8663))
12
22
  - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632))
13
23
 
24
+ ### Fixed
25
+
26
+ - `protectVaultKeyWithPasskey` rejects post-registration assertions whose `userHandle` is missing or does not match the in-flight registration ceremony when using `userHandle` key derivation (assertion `userHandle` is not signature-bound) ([#8663](https://github.com/MetaMask/core/pull/8663))
27
+
14
28
  ## [1.0.0]
15
29
 
16
30
  ### Added
package/README.md CHANGED
@@ -12,19 +12,21 @@ or
12
12
 
13
13
  ## Overview
14
14
 
15
- The controller follows a two-phase ceremony pattern for both enrollment and authentication:
15
+ The controller follows a two-phase ceremony pattern for unlock (authentication) and a three-step pattern for enrollment: registration options → post-registration authentication options → combined verify and protect.
16
16
 
17
17
  1. **Generate options** — call a synchronous method that returns options JSON and records **in-flight ceremony** state (challenge-keyed; not a user login session).
18
18
  2. **Verify response** — pass the authenticator's response back to the controller, which verifies the WebAuthn signature and performs the cryptographic operation (protect or retrieve the vault key).
19
19
 
20
+ For enrollment, the wrapping key is always derived from the **post-registration** `get()` response (same path as unlock), not from the `create()` response alone.
21
+
20
22
  ### Key derivation strategies
21
23
 
22
24
  The controller supports two key derivation methods, selected automatically during enrollment:
23
25
 
24
- | Strategy | When used | Input key material |
25
- | -------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
26
- | **PRF** | Authenticator supports the [WebAuthn PRF extension](https://w3c.github.io/webauthn/#prf-extension) | PRF evaluation output |
27
- | **userHandle** | PRF is unavailable | Random `userHandle` generated during registration |
26
+ | Strategy | When used | Input key material |
27
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
28
+ | **PRF** | Post-registration assertion includes non-empty [PRF extension](https://w3c.github.io/webauthn/#prf-extension) output and registration used PRF salt | PRF evaluation output from the assertion (ceremony `prfSalt` is stored on the record) |
29
+ | **userHandle** | Otherwise | Random `userHandle` from registration (asserted on the post-registration `get()`) |
28
30
 
29
31
  Both strategies feed the input key material through **HKDF-SHA256** with the credential ID as salt and a fixed info string to produce the 32-byte AES-256 wrapping key.
30
32
 
@@ -40,7 +42,9 @@ const messenger: PasskeyControllerMessenger = /* create via root messenger */;
40
42
 
41
43
  const controller = new PasskeyController({
42
44
  messenger,
43
- rpID: 'example.com',
45
+ rpId: 'example.com',
46
+ // Or multiple verification candidates: expectedRPID: ['a.example', 'b.example']
47
+ expectedRPID: 'example.com',
44
48
  rpName: 'My Wallet',
45
49
  expectedOrigin: 'chrome-extension://abcdef1234567890',
46
50
  // Optional — both default to `rpName` when omitted.
@@ -49,18 +53,31 @@ const controller = new PasskeyController({
49
53
  });
50
54
  ```
51
55
 
56
+ `expectedRPID` is a string or string array used to verify the authenticator `rpIdHash`. Optional `rpId`, when set, is sent as `rp.id` / `rpId` in generated WebAuthn options; when omitted, those fields are omitted so the client uses its default RP ID behavior.
57
+
52
58
  ### Passkey enrollment (registration)
53
59
 
54
60
  ```typescript
55
61
  // 1. Generate registration options (synchronous)
56
- const options = controller.generateRegistrationOptions();
62
+ const regOptions = controller.generateRegistrationOptions();
57
63
 
58
- // 2. Pass options to the browser WebAuthn API
59
- const response = await navigator.credentials.create({ publicKey: options });
64
+ // 2. Create the passkey in the browser
65
+ const regResponse = await navigator.credentials.create({
66
+ publicKey: regOptions,
67
+ });
68
+
69
+ // 3. Post-registration authentication (same wrapping-key path as unlock)
70
+ const authOptions = controller.generatePostRegistrationAuthenticationOptions({
71
+ registrationResponse: regResponse,
72
+ });
73
+ const authResponse = await navigator.credentials.get({
74
+ publicKey: authOptions,
75
+ });
60
76
 
61
- // 3. Verify and protect the vault key
77
+ // 4. Verify registration + post-registration auth once, then persist
62
78
  await controller.protectVaultKeyWithPasskey({
63
- registrationResponse: response,
79
+ registrationResponse: regResponse,
80
+ authenticationResponse: authResponse,
64
81
  vaultKey: myVaultEncryptionKey,
65
82
  });
66
83
  ```
@@ -116,8 +133,8 @@ passkeyControllerSelectors.selectIsPasskeyEnrolled(state); // boolean
116
133
 
117
134
  `PasskeyControllerError` is thrown for controller failures. Expected operational
118
135
  cases use a stable `code` from `PasskeyControllerErrorCode` (for example:
119
- `not_enrolled`, `no_registration_ceremony`, `authentication_verification_failed`,
120
- `missing_key_material`, `vault_key_decryption_failed`). Human-readable strings
136
+ `not_enrolled`, `already_enrolled`, `no_registration_ceremony`,
137
+ `authentication_verification_failed`, `missing_key_material`, `vault_key_decryption_failed`). Human-readable strings
121
138
  live on `PasskeyControllerErrorMessage`. Use `instanceof PasskeyControllerError`
122
139
  and a defined `error.code` to tell these apart from malformed WebAuthn payloads
123
140
  and other `Error` values. Thrown errors from the internal WebAuthn verify helpers
@@ -10,12 +10,11 @@ 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_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_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_verifyAuthenticationResponse;
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 webcrypto_1 = require("@noble/ciphers/webcrypto");
19
18
  const ceremony_manager_1 = require("./ceremony-manager.cjs");
20
19
  const constants_1 = require("./constants.cjs");
21
20
  const errors_1 = require("./errors.cjs");
@@ -56,32 +55,27 @@ exports.passkeyControllerSelectors = {
56
55
  selectIsPasskeyEnrolled: (state) => state.passkeyRecord !== null,
57
56
  };
58
57
  /**
59
- * Passkey-based protection for the vault encryption key (WebAuthn).
60
- *
61
- * Uses PRF-backed derivation when available; otherwise uses the credential
62
- * `userHandle`.
58
+ * Controller that enrolls a WebAuthn passkey and uses it to protect and unlock
59
+ * the vault encryption key.
63
60
  */
64
61
  class PasskeyController extends base_controller_1.BaseController {
65
62
  /**
66
- * Constructs a new {@link PasskeyController}.
63
+ * Creates a passkey controller with WebAuthn relying-party settings.
67
64
  *
68
- * @param args - The constructor arguments.
69
- * @param args.messenger - The messenger suited for this controller.
70
- * @param args.state - Initial state. Missing properties are filled in with
71
- * defaults from {@link getDefaultPasskeyControllerState}.
72
- * @param args.rpID - WebAuthn Relying Party ID (typically the eTLD+1 of the
73
- * client origin, or `localhost` in dev).
74
- * @param args.rpName - Human-readable Relying Party name shown by the OS
75
- * passkey UI.
76
- * @param args.expectedOrigin - One or more acceptable origins for the
77
- * `clientDataJSON.origin` check (e.g. `chrome-extension://...`).
78
- * @param args.userName - Optional `user.name` shown by the OS passkey UI.
79
- * Defaults to `rpName` so client builds (Stable, Flask, etc.) can
80
- * differentiate without changes here.
81
- * @param args.userDisplayName - Optional `user.displayName` shown by the OS
82
- * passkey UI. Defaults to `rpName`.
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`.
83
77
  */
84
- constructor({ messenger, state = {}, rpID, rpName, expectedOrigin, userName, userDisplayName, }) {
78
+ constructor({ messenger, state = {}, rpId, expectedRPID, rpName, expectedOrigin, userName, userDisplayName, }) {
85
79
  super({
86
80
  messenger,
87
81
  metadata: passkeyControllerMetadata,
@@ -90,47 +84,54 @@ class PasskeyController extends base_controller_1.BaseController {
90
84
  });
91
85
  _PasskeyController_instances.add(this);
92
86
  _PasskeyController_ceremonyManager.set(this, new ceremony_manager_1.CeremonyManager());
93
- _PasskeyController_rpID.set(this, void 0);
87
+ _PasskeyController_expectedRPIDs.set(this, void 0);
88
+ _PasskeyController_rpId.set(this, void 0);
94
89
  _PasskeyController_rpName.set(this, void 0);
95
90
  _PasskeyController_expectedOrigin.set(this, void 0);
96
91
  _PasskeyController_userName.set(this, void 0);
97
92
  _PasskeyController_userDisplayName.set(this, void 0);
98
- __classPrivateFieldSet(this, _PasskeyController_rpID, rpID, "f");
93
+ const expectedRPIDs = Array.isArray(expectedRPID)
94
+ ? expectedRPID
95
+ : [expectedRPID];
96
+ __classPrivateFieldSet(this, _PasskeyController_expectedRPIDs, [...expectedRPIDs], "f");
97
+ __classPrivateFieldSet(this, _PasskeyController_rpId, rpId, "f");
99
98
  __classPrivateFieldSet(this, _PasskeyController_rpName, rpName, "f");
100
99
  __classPrivateFieldSet(this, _PasskeyController_expectedOrigin, expectedOrigin, "f");
101
100
  __classPrivateFieldSet(this, _PasskeyController_userName, userName ?? rpName, "f");
102
101
  __classPrivateFieldSet(this, _PasskeyController_userDisplayName, userDisplayName ?? rpName, "f");
103
102
  }
104
103
  /**
105
- * Checks if the passkey is enrolled.
104
+ * Whether a passkey is enrolled and vault key material is stored.
106
105
  *
107
- * @returns Whether the passkey is enrolled.
106
+ * @returns `true` if enrolled, otherwise `false`.
108
107
  */
109
108
  isPasskeyEnrolled() {
110
109
  return exports.passkeyControllerSelectors.selectIsPasskeyEnrolled(this.state);
111
110
  }
112
111
  /**
113
- * Registration options for enrolling a passkey.
114
- *
115
- * Call before {@link protectVaultKeyWithPasskey}.
112
+ * Builds WebAuthn credential creation options for passkey enrollment.
116
113
  *
117
- * @param creationOptionsConfig - Optional configuration.
118
- * @param creationOptionsConfig.prfAvailable - Omit PRF when `false`. Default `true`.
119
- * @returns Options for `navigator.credentials.create()`.
114
+ * @param creationOptionsConfig - Optional creation behavior.
115
+ * @param creationOptionsConfig.prfAvailable - Request the PRF extension unless `false`. Defaults to `true`.
116
+ * @returns Public key credential creation options for `navigator.credentials.create()`.
120
117
  */
121
118
  generateRegistrationOptions(creationOptionsConfig) {
119
+ if (this.isPasskeyEnrolled()) {
120
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AlreadyEnrolled, { code: constants_1.PasskeyControllerErrorCode.AlreadyEnrolled });
121
+ }
122
122
  const includePrf = creationOptionsConfig?.prfAvailable !== false;
123
- const prfSalt = includePrf
124
- ? (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice())
125
- : undefined;
126
- const userHandle = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(64).slice());
127
- const challenge = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice());
123
+ const prfSalt = includePrf ? (0, crypto_1.randomBytesToBase64URL)(32) : undefined;
124
+ const userHandle = (0, crypto_1.randomBytesToBase64URL)(64);
125
+ const challenge = (0, crypto_1.randomBytesToBase64URL)(32);
128
126
  const extensions = {};
129
127
  if (prfSalt) {
130
128
  extensions.prf = { eval: { first: prfSalt } };
131
129
  }
132
130
  const options = {
133
- rp: { name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"), id: __classPrivateFieldGet(this, _PasskeyController_rpID, "f") },
131
+ rp: {
132
+ name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"),
133
+ id: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
134
+ },
134
135
  user: {
135
136
  id: userHandle,
136
137
  name: __classPrivateFieldGet(this, _PasskeyController_userName, "f"),
@@ -154,30 +155,72 @@ class PasskeyController extends base_controller_1.BaseController {
154
155
  };
155
156
  __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveRegistrationCeremony(challenge, {
156
157
  userHandle,
157
- prfSalt: prfSalt ?? '',
158
+ prfSalt,
158
159
  challenge,
159
160
  createdAt: Date.now(),
160
161
  });
161
162
  return options;
162
163
  }
163
164
  /**
164
- * WebAuthn request options for authenticating with the enrolled passkey.
165
+ * Builds WebAuthn credential request options for the post-registration
166
+ * authentication step (between `create` and {@link protectVaultKeyWithPasskey}).
165
167
  *
166
- * Call before {@link retrieveVaultKeyWithPasskey},
167
- * {@link verifyPasskeyAuthentication}, or {@link renewVaultKeyProtection}.
168
+ * @param params - Input for the pending registration ceremony.
169
+ * @param params.registrationResponse - Result of `navigator.credentials.create()`.
170
+ * @returns Public key credential request options for `navigator.credentials.get()`.
171
+ */
172
+ generatePostRegistrationAuthenticationOptions(params) {
173
+ // get registration ceremony
174
+ const { registrationResponse } = params;
175
+ const regChallenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
176
+ const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(regChallenge);
177
+ if (!registrationCeremony) {
178
+ log('No active passkey registration ceremony for challenge');
179
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoRegistrationCeremony });
180
+ }
181
+ // build auth options
182
+ const challenge = (0, crypto_1.randomBytesToBase64URL)(32);
183
+ const extensions = {};
184
+ if (registrationCeremony.prfSalt) {
185
+ extensions.prf = { eval: { first: registrationCeremony.prfSalt } };
186
+ }
187
+ const options = {
188
+ challenge,
189
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
190
+ allowCredentials: [
191
+ {
192
+ id: registrationResponse.id,
193
+ type: 'public-key',
194
+ transports: registrationResponse.response.transports,
195
+ },
196
+ ],
197
+ userVerification: 'preferred',
198
+ hints: ['client-device', 'hybrid'],
199
+ timeout: ceremony_manager_1.WEBAUTHN_TIMEOUT_MS,
200
+ extensions,
201
+ };
202
+ // save auth ceremony
203
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveAuthenticationCeremony(challenge, {
204
+ challenge,
205
+ createdAt: Date.now(),
206
+ });
207
+ return options;
208
+ }
209
+ /**
210
+ * Builds WebAuthn credential request options for the enrolled passkey.
168
211
  *
169
- * @returns Options for `navigator.credentials.get()`.
212
+ * @returns Public key credential request options for `navigator.credentials.get()`.
170
213
  */
171
214
  generateAuthenticationOptions() {
172
215
  const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
173
- const challenge = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice());
216
+ const challenge = (0, crypto_1.randomBytesToBase64URL)(32);
174
217
  const extensions = {};
175
218
  if (record.keyDerivation.method === 'prf') {
176
219
  extensions.prf = { eval: { first: record.keyDerivation.prfSalt } };
177
220
  }
178
221
  const options = {
179
222
  challenge,
180
- rpId: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
223
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpId, "f"),
181
224
  allowCredentials: [
182
225
  {
183
226
  id: record.credential.id,
@@ -197,15 +240,20 @@ class PasskeyController extends base_controller_1.BaseController {
197
240
  return options;
198
241
  }
199
242
  /**
200
- * Completes enrollment and binds the vault key to the new passkey.
243
+ * Verifies registration and post-registration authentication, then stores the
244
+ * vault key encrypted under the new passkey.
201
245
  *
202
- * @param params - Protection parameters.
203
- * @param params.registrationResponse - Credential from `navigator.credentials.create()`.
204
- * @param params.vaultKey - Vault encryption key to protect.
246
+ * @param params - Enrollment completion inputs.
247
+ * @param params.registrationResponse - Result of `navigator.credentials.create()`.
248
+ * @param params.authenticationResponse - Result of `navigator.credentials.get()` after {@link generatePostRegistrationAuthenticationOptions}.
249
+ * @param params.vaultKey - Vault encryption key to encrypt and persist.
205
250
  */
206
251
  async protectVaultKeyWithPasskey(params) {
207
- const { registrationResponse, vaultKey } = params;
208
- // get challenge
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
209
257
  const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
210
258
  const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
211
259
  if (!registrationCeremony) {
@@ -218,7 +266,7 @@ class PasskeyController extends base_controller_1.BaseController {
218
266
  response: registrationResponse,
219
267
  expectedChallenge: registrationCeremony.challenge,
220
268
  expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
221
- expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
269
+ expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
222
270
  requireUserVerification: false,
223
271
  }).catch((error) => {
224
272
  log('Error verifying passkey registration response', error);
@@ -231,19 +279,36 @@ class PasskeyController extends base_controller_1.BaseController {
231
279
  log('Passkey registration verification returned unverified or missing registration info');
232
280
  throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed });
233
281
  }
234
- // derive key
235
- const { encKey, keyDerivation } = (0, key_derivation_1.deriveKeyFromRegistrationResponse)(registrationResponse, registrationCeremony, registrationInfo.credentialId);
236
- // encrypt vault key
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 });
237
305
  const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(vaultKey, encKey);
238
306
  // persist passkey record
239
307
  this.update((state) => {
240
308
  state.passkeyRecord = {
241
309
  credential: {
242
- id: registrationInfo.credentialId,
243
- publicKey: (0, encoding_1.bytesToBase64URL)(registrationInfo.publicKey),
244
- counter: registrationInfo.counter,
245
- transports: registrationInfo.transports,
246
- aaguid: registrationInfo.aaguid,
310
+ ...credential,
311
+ counter: Math.max(newCounter, credential.counter),
247
312
  },
248
313
  encryptedVaultKey: { ciphertext, iv },
249
314
  keyDerivation,
@@ -251,26 +316,32 @@ class PasskeyController extends base_controller_1.BaseController {
251
316
  });
252
317
  }
253
318
  finally {
319
+ // delete registration ceremony
254
320
  __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
255
321
  }
256
322
  }
257
323
  /**
258
- * Returns the decrypted vault encryption key from the passkey authentication
259
- * response.
324
+ * Verifies an authentication assertion and returns the decrypted vault key.
260
325
  *
261
- * @param authenticationResponse - Credential from `navigator.credentials.get()`.
262
- * @returns The vault encryption key.
326
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
327
+ * @returns The plaintext vault encryption key.
263
328
  */
264
329
  async retrieveVaultKeyWithPasskey(authenticationResponse) {
265
- // verify authentication response
266
- await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse);
267
- // derive key (#verifyAuthenticationResponse guarantees enrolled)
268
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 });
336
+ }
337
+ state.passkeyRecord.credential.counter = Math.max(newCounter, state.passkeyRecord.credential.counter);
338
+ });
339
+ // derive key
269
340
  const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
270
341
  // decrypt vault key
271
- let vaultKey;
272
342
  try {
273
- vaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
343
+ const vaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
344
+ return vaultKey;
274
345
  }
275
346
  catch (cause) {
276
347
  log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
@@ -279,18 +350,15 @@ class PasskeyController extends base_controller_1.BaseController {
279
350
  cause: cause instanceof Error ? cause : new Error(String(cause)),
280
351
  });
281
352
  }
282
- return vaultKey;
283
353
  }
284
354
  /**
285
- * Returns whether passkey authentication succeeds for this credential (same
286
- * work as {@link retrieveVaultKeyWithPasskey} without exposing the vault key).
355
+ * Checks whether the given authentication assertion is valid for the enrolled passkey.
287
356
  *
288
- * Returns `false` only when the failure is a {@link PasskeyControllerError}
289
- * with a defined `code`. Unexpected errors (e.g. malformed `clientDataJSON`,
290
- * internal bugs) are rethrown.
357
+ * On failure, returns `false` for {@link PasskeyControllerError} with a `code`;
358
+ * other errors propagate.
291
359
  *
292
- * @param authenticationResponse - Credential from `navigator.credentials.get()`.
293
- * @returns `true` if authentication succeeds, otherwise `false`.
360
+ * @param authenticationResponse - Result of `navigator.credentials.get()`.
361
+ * @returns `true` if verification succeeds, otherwise `false`.
294
362
  */
295
363
  async verifyPasskeyAuthentication(authenticationResponse) {
296
364
  try {
@@ -305,19 +373,17 @@ class PasskeyController extends base_controller_1.BaseController {
305
373
  }
306
374
  }
307
375
  /**
308
- * Updates the vault encryption key for the same passkey (e.g. after a password change).
376
+ * Re-wraps the vault key after rotation. Updates persisted `encryptedVaultKey` on success.
309
377
  *
310
- * Caller MUST first verify the assertion via {@link verifyPasskeyAuthentication}
311
- * or {@link retrieveVaultKeyWithPasskey}. This method does not re-verify
312
- * because the ceremony is single-use (deleted on verify) and the signature
313
- * counter is advanced (replay would be rejected). Authentication here is
314
- * enforced by the prior verification plus the `oldVaultKey` match below.
378
+ * Does not verify WebAuthn or ceremony state—call only after your layer has authenticated
379
+ * the user (passkey `get()` + verified assertion, or verified password). On passkey paths,
380
+ * pass the same `authenticationResponse` you just verified (e.g. from
381
+ * {@link retrieveVaultKeyWithPasskey} / {@link verifyPasskeyAuthentication}).
315
382
  *
316
- * @param params - Renewal parameters.
317
- * @param params.authenticationResponse - Credential from `navigator.credentials.get()`,
318
- * already verified by the caller.
383
+ * @param params - Re-wrap inputs.
384
+ * @param params.authenticationResponse - Used to derive the wrapping key.
319
385
  * @param params.oldVaultKey - Expected current vault key.
320
- * @param params.newVaultKey - New vault key to protect.
386
+ * @param params.newVaultKey - New vault key to encrypt under the passkey.
321
387
  */
322
388
  async renewVaultKeyProtection(params) {
323
389
  const { authenticationResponse } = params;
@@ -355,7 +421,8 @@ class PasskeyController extends base_controller_1.BaseController {
355
421
  });
356
422
  }
357
423
  /**
358
- * Unenrolls the passkey, removing the protected vault key material.
424
+ * Clears enrolled passkey state and in-flight ceremonies. Call only after the same
425
+ * auth gate as renewal (verified passkey assertion or password).
359
426
  */
360
427
  removePasskey() {
361
428
  this.update(() => getDefaultPasskeyControllerState());
@@ -376,7 +443,7 @@ class PasskeyController extends base_controller_1.BaseController {
376
443
  }
377
444
  }
378
445
  exports.PasskeyController = PasskeyController;
379
- _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() {
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() {
380
447
  const record = this.state.passkeyRecord;
381
448
  if (!record) {
382
449
  throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
@@ -388,34 +455,33 @@ _PasskeyController_ceremonyManager = new WeakMap(), _PasskeyController_rpID = ne
388
455
  return (0, decode_client_data_json_1.decodeClientDataJSON)(clientDataJSON).challenge;
389
456
  }, _PasskeyController_verifyAuthenticationResponse =
390
457
  /**
391
- * Verifies an authentication response for the enrolled passkey.
458
+ * Validates a WebAuthn authentication response against stored credential data.
392
459
  *
393
- * @param authenticationResponse - Authentication result JSON.
460
+ * @param authenticationResponse - Parsed authentication response from the client.
461
+ * @param credential - Credential identifiers and public key material for verification.
462
+ * @returns Updated authenticator signature counter.
394
463
  */
395
- async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse) {
396
- let challenge;
464
+ async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse, credential) {
465
+ // get challenge
466
+ const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
467
+ // get authentication ceremony
468
+ const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
469
+ if (!authenticationCeremony) {
470
+ log('No active passkey authentication ceremony for challenge');
471
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoAuthenticationCeremony });
472
+ }
397
473
  try {
398
- // get challenge
399
- challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
400
- // get passkey record
401
- const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
402
- // get authentication ceremony
403
- const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
404
- if (!authenticationCeremony) {
405
- log('No active passkey authentication ceremony for challenge');
406
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoAuthenticationCeremony });
407
- }
408
474
  // verify authentication response
409
475
  const result = await (0, verify_authentication_response_1.verifyAuthenticationResponse)({
410
476
  response: authenticationResponse,
411
477
  expectedChallenge: authenticationCeremony.challenge,
412
478
  expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
413
- expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
479
+ expectedRPIDs: __classPrivateFieldGet(this, _PasskeyController_expectedRPIDs, "f"),
414
480
  credential: {
415
- id: record.credential.id,
416
- publicKey: (0, encoding_1.base64URLToBytes)(record.credential.publicKey),
417
- counter: record.credential.counter,
418
- transports: record.credential.transports,
481
+ id: credential.id,
482
+ publicKey: (0, encoding_1.base64URLToBytes)(credential.publicKey),
483
+ counter: credential.counter,
484
+ transports: credential.transports,
419
485
  },
420
486
  // UV optional for device compatibility; vault key remains password-gated.
421
487
  requireUserVerification: false,
@@ -432,19 +498,11 @@ async function _PasskeyController_verifyAuthenticationResponse(authenticationRes
432
498
  code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed,
433
499
  });
434
500
  }
435
- // persist passkey record with updated counter without clobbering concurrent updates
436
- this.update((state) => {
437
- if (!state.passkeyRecord) {
438
- throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, { code: constants_1.PasskeyControllerErrorCode.NotEnrolled });
439
- }
440
- const latest = state.passkeyRecord;
441
- latest.credential.counter = Math.max(result.authenticationInfo.newCounter, latest.credential.counter);
442
- });
501
+ return { newCounter: result.authenticationInfo.newCounter };
443
502
  }
444
503
  finally {
445
- if (challenge) {
446
- __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
447
- }
504
+ // delete authentication ceremony
505
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
448
506
  }
449
507
  };
450
508
  //# sourceMappingURL=PasskeyController.cjs.map