@metamask/passkey-controller 1.0.0 → 2.0.1

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 +34 -2
  2. package/README.md +30 -13
  3. package/dist/PasskeyController.cjs +182 -125
  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 +184 -127
  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 +2 -2
package/CHANGELOG.md CHANGED
@@ -7,11 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.1]
11
+
12
+ ### Changed
13
+
14
+ - `PasskeyController` verifies registration and authentication responses with `requireUserVerification: true`, so the WebAuthn user verification (UV) flag must be set; assertions with user presence only no longer pass verification ([#8696](https://github.com/MetaMask/core/pull/8696))
15
+
16
+ ### Fixed
17
+
18
+ - `generateAuthenticationOptions` now sets `userVerification: 'required'` so client WebAuthn requests align with server-side verification requirements and do not fail on authenticators that skip UV when set to `'preferred'` ([#8696](https://github.com/MetaMask/core/pull/8696))
19
+
20
+ ## [2.0.0]
21
+
22
+ ### Added
23
+
24
+ - `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))
25
+ - `already_enrolled` (`PasskeyControllerErrorCode.AlreadyEnrolled`) when calling `protectVaultKeyWithPasskey` while a passkey is already enrolled ([#8663](https://github.com/MetaMask/core/pull/8663))
26
+
27
+ ### Changed
28
+
29
+ - **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))
30
+ - **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))
31
+ - **BREAKING:** `verifyRegistrationResponse` and `verifyAuthenticationResponse` now take `expectedRPIDs: string[]` instead of `expectedRPID: string` ([#8663](https://github.com/MetaMask/core/pull/8663))
32
+ - `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))
33
+ - 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))
34
+ - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632))
35
+
36
+ ### Fixed
37
+
38
+ - `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))
39
+
10
40
  ## [1.0.0]
11
41
 
12
42
  ### Added
13
43
 
14
- - Initial `@metamask/passkey-controller` ([#8422](https://github.com/MetaMask/core/pull/8422)): `PasskeyController` for WebAuthn passkey vault key protection (HKDF-derived keys, AES-256-GCM wrap/unwrap), PRF or `userHandle` derivation, challenge-keyed `CeremonyManager`, enrollment/unlock/renewal flows, `verifyPasskeyAuthentication`, selectors, and exported ceremony timing constants.
44
+ - Initial `@metamask/passkey-controller`: `PasskeyController` for WebAuthn passkey vault key protection (HKDF-derived keys, AES-256-GCM wrap/unwrap), PRF or `userHandle` derivation, challenge-keyed `CeremonyManager`, enrollment/unlock/renewal flows, `verifyPasskeyAuthentication`, selectors, and exported ceremony timing constants. ([#8422](https://github.com/MetaMask/core/pull/8422))
15
45
  - `PasskeyControllerError` with stable `code`, optional `cause` / `context`, `toJSON`, and `toString`; `PasskeyControllerErrorCode`, `PasskeyControllerErrorMessage`, and `controllerName`. Replaces `PasskeyAuthenticationRejectedError`—use `PasskeyControllerError` and `code` for auth failures.
16
46
  - **BREAKING:** Operational error messages are prefixed with `PasskeyController - `; prefer `code` or `instanceof PasskeyControllerError` over matching raw strings.
17
47
  - `renewVaultKeyProtection` uses the same `vault_key_decryption_failed` code as `retrieveVaultKeyWithPasskey` when AES-GCM decrypt fails.
@@ -23,5 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
53
  - Registration verification requires the credential `id`/`rawId` to match the credential id in authenticator data; vault wrapping key derivation uses that verified credential id so enrollment keys align with the stored credential.
24
54
  - Registration options request attestation conveyance `'none'` so clients are not asked for direct attestation formats the verifier does not implement (`none` and self-attested `packed` only).
25
55
 
26
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@1.0.0...HEAD
56
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@2.0.1...HEAD
57
+ [2.0.1]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@2.0.0...@metamask/passkey-controller@2.0.1
58
+ [2.0.0]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@1.0.0...@metamask/passkey-controller@2.0.0
27
59
  [1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/passkey-controller@1.0.0
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