@metamask-previews/passkey-controller 0.0.0-preview-4c0846313

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 (156) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +155 -0
  4. package/dist/PasskeyController.cjs +448 -0
  5. package/dist/PasskeyController.cjs.map +1 -0
  6. package/dist/PasskeyController.d.cts +168 -0
  7. package/dist/PasskeyController.d.cts.map +1 -0
  8. package/dist/PasskeyController.d.mts +168 -0
  9. package/dist/PasskeyController.d.mts.map +1 -0
  10. package/dist/PasskeyController.mjs +443 -0
  11. package/dist/PasskeyController.mjs.map +1 -0
  12. package/dist/ceremony-manager.cjs +134 -0
  13. package/dist/ceremony-manager.cjs.map +1 -0
  14. package/dist/ceremony-manager.d.cts +71 -0
  15. package/dist/ceremony-manager.d.cts.map +1 -0
  16. package/dist/ceremony-manager.d.mts +71 -0
  17. package/dist/ceremony-manager.d.mts.map +1 -0
  18. package/dist/ceremony-manager.mjs +130 -0
  19. package/dist/ceremony-manager.mjs.map +1 -0
  20. package/dist/constants.cjs +33 -0
  21. package/dist/constants.cjs.map +1 -0
  22. package/dist/constants.d.cts +30 -0
  23. package/dist/constants.d.cts.map +1 -0
  24. package/dist/constants.d.mts +30 -0
  25. package/dist/constants.d.mts.map +1 -0
  26. package/dist/constants.mjs +30 -0
  27. package/dist/constants.mjs.map +1 -0
  28. package/dist/errors.cjs +57 -0
  29. package/dist/errors.cjs.map +1 -0
  30. package/dist/errors.d.cts +34 -0
  31. package/dist/errors.d.cts.map +1 -0
  32. package/dist/errors.d.mts +34 -0
  33. package/dist/errors.d.mts.map +1 -0
  34. package/dist/errors.mjs +53 -0
  35. package/dist/errors.mjs.map +1 -0
  36. package/dist/index.cjs +19 -0
  37. package/dist/index.cjs.map +1 -0
  38. package/dist/index.d.cts +9 -0
  39. package/dist/index.d.cts.map +1 -0
  40. package/dist/index.d.mts +9 -0
  41. package/dist/index.d.mts.map +1 -0
  42. package/dist/index.mjs +5 -0
  43. package/dist/index.mjs.map +1 -0
  44. package/dist/key-derivation.cjs +76 -0
  45. package/dist/key-derivation.cjs.map +1 -0
  46. package/dist/key-derivation.d.cts +43 -0
  47. package/dist/key-derivation.d.cts.map +1 -0
  48. package/dist/key-derivation.d.mts +43 -0
  49. package/dist/key-derivation.d.mts.map +1 -0
  50. package/dist/key-derivation.mjs +71 -0
  51. package/dist/key-derivation.mjs.map +1 -0
  52. package/dist/logger.cjs +9 -0
  53. package/dist/logger.cjs.map +1 -0
  54. package/dist/logger.d.cts +5 -0
  55. package/dist/logger.d.cts.map +1 -0
  56. package/dist/logger.d.mts +5 -0
  57. package/dist/logger.d.mts.map +1 -0
  58. package/dist/logger.mjs +6 -0
  59. package/dist/logger.mjs.map +1 -0
  60. package/dist/types.cjs +3 -0
  61. package/dist/types.cjs.map +1 -0
  62. package/dist/types.d.cts +92 -0
  63. package/dist/types.d.cts.map +1 -0
  64. package/dist/types.d.mts +92 -0
  65. package/dist/types.d.mts.map +1 -0
  66. package/dist/types.mjs +2 -0
  67. package/dist/types.mjs.map +1 -0
  68. package/dist/utils/crypto.cjs +55 -0
  69. package/dist/utils/crypto.cjs.map +1 -0
  70. package/dist/utils/crypto.d.cts +30 -0
  71. package/dist/utils/crypto.d.cts.map +1 -0
  72. package/dist/utils/crypto.d.mts +30 -0
  73. package/dist/utils/crypto.d.mts.map +1 -0
  74. package/dist/utils/crypto.mjs +49 -0
  75. package/dist/utils/crypto.mjs.map +1 -0
  76. package/dist/utils/encoding.cjs +42 -0
  77. package/dist/utils/encoding.cjs.map +1 -0
  78. package/dist/utils/encoding.d.cts +22 -0
  79. package/dist/utils/encoding.d.cts.map +1 -0
  80. package/dist/utils/encoding.d.mts +22 -0
  81. package/dist/utils/encoding.d.mts.map +1 -0
  82. package/dist/utils/encoding.mjs +36 -0
  83. package/dist/utils/encoding.mjs.map +1 -0
  84. package/dist/webauthn/constants.cjs +74 -0
  85. package/dist/webauthn/constants.cjs.map +1 -0
  86. package/dist/webauthn/constants.d.cts +68 -0
  87. package/dist/webauthn/constants.d.cts.map +1 -0
  88. package/dist/webauthn/constants.d.mts +68 -0
  89. package/dist/webauthn/constants.d.mts.map +1 -0
  90. package/dist/webauthn/constants.mjs +71 -0
  91. package/dist/webauthn/constants.mjs.map +1 -0
  92. package/dist/webauthn/decode-attestation-object.cjs +18 -0
  93. package/dist/webauthn/decode-attestation-object.cjs.map +1 -0
  94. package/dist/webauthn/decode-attestation-object.d.cts +10 -0
  95. package/dist/webauthn/decode-attestation-object.d.cts.map +1 -0
  96. package/dist/webauthn/decode-attestation-object.d.mts +10 -0
  97. package/dist/webauthn/decode-attestation-object.d.mts.map +1 -0
  98. package/dist/webauthn/decode-attestation-object.mjs +14 -0
  99. package/dist/webauthn/decode-attestation-object.mjs.map +1 -0
  100. package/dist/webauthn/decode-client-data-json.cjs +17 -0
  101. package/dist/webauthn/decode-client-data-json.cjs.map +1 -0
  102. package/dist/webauthn/decode-client-data-json.d.cts +9 -0
  103. package/dist/webauthn/decode-client-data-json.d.cts.map +1 -0
  104. package/dist/webauthn/decode-client-data-json.d.mts +9 -0
  105. package/dist/webauthn/decode-client-data-json.d.mts.map +1 -0
  106. package/dist/webauthn/decode-client-data-json.mjs +13 -0
  107. package/dist/webauthn/decode-client-data-json.mjs.map +1 -0
  108. package/dist/webauthn/match-expected-rp-id.cjs +43 -0
  109. package/dist/webauthn/match-expected-rp-id.cjs.map +1 -0
  110. package/dist/webauthn/match-expected-rp-id.d.cts +11 -0
  111. package/dist/webauthn/match-expected-rp-id.d.cts.map +1 -0
  112. package/dist/webauthn/match-expected-rp-id.d.mts +11 -0
  113. package/dist/webauthn/match-expected-rp-id.d.mts.map +1 -0
  114. package/dist/webauthn/match-expected-rp-id.mjs +39 -0
  115. package/dist/webauthn/match-expected-rp-id.mjs.map +1 -0
  116. package/dist/webauthn/parse-authenticator-data.cjs +69 -0
  117. package/dist/webauthn/parse-authenticator-data.cjs.map +1 -0
  118. package/dist/webauthn/parse-authenticator-data.d.cts +10 -0
  119. package/dist/webauthn/parse-authenticator-data.d.cts.map +1 -0
  120. package/dist/webauthn/parse-authenticator-data.d.mts +10 -0
  121. package/dist/webauthn/parse-authenticator-data.d.mts.map +1 -0
  122. package/dist/webauthn/parse-authenticator-data.mjs +65 -0
  123. package/dist/webauthn/parse-authenticator-data.mjs.map +1 -0
  124. package/dist/webauthn/types.cjs +3 -0
  125. package/dist/webauthn/types.cjs.map +1 -0
  126. package/dist/webauthn/types.d.cts +113 -0
  127. package/dist/webauthn/types.d.cts.map +1 -0
  128. package/dist/webauthn/types.d.mts +113 -0
  129. package/dist/webauthn/types.d.mts.map +1 -0
  130. package/dist/webauthn/types.mjs +2 -0
  131. package/dist/webauthn/types.mjs.map +1 -0
  132. package/dist/webauthn/verify-authentication-response.cjs +134 -0
  133. package/dist/webauthn/verify-authentication-response.cjs.map +1 -0
  134. package/dist/webauthn/verify-authentication-response.d.cts +63 -0
  135. package/dist/webauthn/verify-authentication-response.d.cts.map +1 -0
  136. package/dist/webauthn/verify-authentication-response.d.mts +63 -0
  137. package/dist/webauthn/verify-authentication-response.d.mts.map +1 -0
  138. package/dist/webauthn/verify-authentication-response.mjs +130 -0
  139. package/dist/webauthn/verify-authentication-response.mjs.map +1 -0
  140. package/dist/webauthn/verify-registration-response.cjs +205 -0
  141. package/dist/webauthn/verify-registration-response.cjs.map +1 -0
  142. package/dist/webauthn/verify-registration-response.d.cts +60 -0
  143. package/dist/webauthn/verify-registration-response.d.cts.map +1 -0
  144. package/dist/webauthn/verify-registration-response.d.mts +60 -0
  145. package/dist/webauthn/verify-registration-response.d.mts.map +1 -0
  146. package/dist/webauthn/verify-registration-response.mjs +201 -0
  147. package/dist/webauthn/verify-registration-response.mjs.map +1 -0
  148. package/dist/webauthn/verify-signature.cjs +176 -0
  149. package/dist/webauthn/verify-signature.cjs.map +1 -0
  150. package/dist/webauthn/verify-signature.d.cts +21 -0
  151. package/dist/webauthn/verify-signature.d.cts.map +1 -0
  152. package/dist/webauthn/verify-signature.d.mts +21 -0
  153. package/dist/webauthn/verify-signature.d.mts.map +1 -0
  154. package/dist/webauthn/verify-signature.mjs +172 -0
  155. package/dist/webauthn/verify-signature.mjs.map +1 -0
  156. package/package.json +78 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - 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.
13
+ - `PasskeyControllerError` with stable `code`, optional `cause` / `context`, `toJSON`, and `toString`; `PasskeyControllerErrorCode`, `PasskeyControllerErrorMessage`, and `controllerName`. Replaces `PasskeyAuthenticationRejectedError`—use `PasskeyControllerError` and `code` for auth failures.
14
+ - **BREAKING:** Operational error messages are prefixed with `PasskeyController - `; prefer `code` or `instanceof PasskeyControllerError` over matching raw strings.
15
+ - `renewVaultKeyProtection` uses the same `vault_key_decryption_failed` code as `retrieveVaultKeyWithPasskey` when AES-GCM decrypt fails.
16
+ - Thrown failures from `verifyRegistrationResponse` / `verifyAuthenticationResponse` are wrapped in `PasskeyControllerError` with `registration_verification_failed` / `authentication_verification_failed` and the underlying error as `cause` (aligned with the `verified: false` path).
17
+ - Debug logging (via `@metamask/utils`) for registration/authentication verification failures, missing ceremony state, vault decrypt failures, and vault key mismatch during renewal.
18
+
19
+ ### Fixed
20
+
21
+ - 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.
22
+ - 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).
23
+
24
+ [Unreleased]: https://github.com/MetaMask/core/
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MetaMask
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # `@metamask/passkey-controller`
2
+
3
+ Manages passkey-based vault key protection using [WebAuthn](https://www.w3.org/TR/webauthn-3/). Orchestrates the full passkey lifecycle: generating WebAuthn ceremony options, verifying authenticator responses, and protecting/retrieving the vault encryption key via AES-256-GCM wrapping with HKDF-derived keys.
4
+
5
+ ## Installation
6
+
7
+ `yarn add @metamask/passkey-controller`
8
+
9
+ or
10
+
11
+ `npm install @metamask/passkey-controller`
12
+
13
+ ## Overview
14
+
15
+ The controller follows a two-phase ceremony pattern for both enrollment and authentication:
16
+
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
+ 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
+
20
+ ### Key derivation strategies
21
+
22
+ The controller supports two key derivation methods, selected automatically during enrollment:
23
+
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 |
28
+
29
+ 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
+
31
+ ## Usage
32
+
33
+ ### Setting up the controller
34
+
35
+ ```typescript
36
+ import { PasskeyController } from '@metamask/passkey-controller';
37
+ import type { PasskeyControllerMessenger } from '@metamask/passkey-controller';
38
+
39
+ const messenger: PasskeyControllerMessenger = /* create via root messenger */;
40
+
41
+ const controller = new PasskeyController({
42
+ messenger,
43
+ rpID: 'example.com',
44
+ rpName: 'My Wallet',
45
+ expectedOrigin: 'chrome-extension://abcdef1234567890',
46
+ // Optional — both default to `rpName` when omitted.
47
+ userName: 'My Wallet',
48
+ userDisplayName: 'My Wallet',
49
+ });
50
+ ```
51
+
52
+ ### Passkey enrollment (registration)
53
+
54
+ ```typescript
55
+ // 1. Generate registration options (synchronous)
56
+ const options = controller.generateRegistrationOptions();
57
+
58
+ // 2. Pass options to the browser WebAuthn API
59
+ const response = await navigator.credentials.create({ publicKey: options });
60
+
61
+ // 3. Verify and protect the vault key
62
+ await controller.protectVaultKeyWithPasskey({
63
+ registrationResponse: response,
64
+ vaultKey: myVaultEncryptionKey,
65
+ });
66
+ ```
67
+
68
+ ### Passkey unlock (authentication)
69
+
70
+ ```typescript
71
+ // 1. Generate authentication options (synchronous)
72
+ const options = controller.generateAuthenticationOptions();
73
+
74
+ // 2. Pass options to the browser WebAuthn API
75
+ const response = await navigator.credentials.get({ publicKey: options });
76
+
77
+ // 3. Verify and retrieve the vault key
78
+ const vaultKey = await controller.retrieveVaultKeyWithPasskey(response);
79
+ ```
80
+
81
+ ### Password change (vault key renewal)
82
+
83
+ ```typescript
84
+ const options = controller.generateAuthenticationOptions();
85
+ const response = await navigator.credentials.get({ publicKey: options });
86
+
87
+ await controller.renewVaultKeyProtection({
88
+ authenticationResponse: response,
89
+ oldVaultKey: currentVaultKey,
90
+ newVaultKey: newVaultKey,
91
+ });
92
+ ```
93
+
94
+ ### Checking enrollment and removing a passkey
95
+
96
+ ```typescript
97
+ controller.isPasskeyEnrolled(); // boolean
98
+
99
+ controller.removePasskey(); // user-facing unenroll; clears persisted passkey and in-flight ceremonies
100
+
101
+ controller.clearState(); // same persisted reset + clears in-flight ceremony state; use for app lifecycle (e.g. wallet reset)
102
+ ```
103
+
104
+ ### Selectors
105
+
106
+ For Redux selectors and other code paths without access to the controller
107
+ instance, use the exported selector(s):
108
+
109
+ ```typescript
110
+ import { passkeyControllerSelectors } from '@metamask/passkey-controller';
111
+
112
+ passkeyControllerSelectors.selectIsPasskeyEnrolled(state); // boolean
113
+ ```
114
+
115
+ ### Errors
116
+
117
+ `PasskeyControllerError` is thrown for controller failures. Expected operational
118
+ 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
121
+ live on `PasskeyControllerErrorMessage`. Use `instanceof PasskeyControllerError`
122
+ and a defined `error.code` to tell these apart from malformed WebAuthn payloads
123
+ and other `Error` values. Thrown errors from the internal WebAuthn verify helpers
124
+ are also surfaced as `PasskeyControllerError` with the same `registration_verification_failed`
125
+ or `authentication_verification_failed` code and the original error as `cause`.
126
+ `verifyPasskeyAuthentication` returns `false` only for
127
+ those controller errors (with `code`) and rethrows everything else.
128
+
129
+ ## API
130
+
131
+ ### State
132
+
133
+ | Property | Type | Description |
134
+ | --------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
135
+ | `passkeyRecord` | `PasskeyRecord \| null` | Enrolled passkey credential data and encrypted vault key. `null` when no passkey is enrolled. |
136
+
137
+ ### Messenger actions
138
+
139
+ | Action | Handler |
140
+ | ---------------------------- | ------------------------------------ |
141
+ | `PasskeyController:getState` | Returns the current controller state |
142
+
143
+ For derived enrollment status outside of components that hold a controller
144
+ reference, use `passkeyControllerSelectors.selectIsPasskeyEnrolled` (see
145
+ [Selectors](#selectors)).
146
+
147
+ ### Messenger events
148
+
149
+ | Event | Payload |
150
+ | -------------------------------- | ------------------------------------------------------------ |
151
+ | `PasskeyController:stateChanged` | Emitted when state changes (standard `BaseController` event) |
152
+
153
+ ## Contributing
154
+
155
+ This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
@@ -0,0 +1,448 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
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
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _PasskeyController_instances, _PasskeyController_ceremonyManager, _PasskeyController_rpID, _PasskeyController_rpName, _PasskeyController_expectedOrigin, _PasskeyController_userName, _PasskeyController_userDisplayName, _PasskeyController_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_verifyAuthenticationResponse;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PasskeyController = exports.passkeyControllerSelectors = exports.getDefaultPasskeyControllerState = void 0;
16
+ const base_controller_1 = require("@metamask/base-controller");
17
+ const webcrypto_1 = require("@noble/ciphers/webcrypto");
18
+ const ceremony_manager_1 = require("./ceremony-manager.cjs");
19
+ const constants_1 = require("./constants.cjs");
20
+ const errors_1 = require("./errors.cjs");
21
+ const key_derivation_1 = require("./key-derivation.cjs");
22
+ const logger_1 = require("./logger.cjs");
23
+ const crypto_1 = require("./utils/crypto.cjs");
24
+ const encoding_1 = require("./utils/encoding.cjs");
25
+ const constants_2 = require("./webauthn/constants.cjs");
26
+ const decode_client_data_json_1 = require("./webauthn/decode-client-data-json.cjs");
27
+ const verify_authentication_response_1 = require("./webauthn/verify-authentication-response.cjs");
28
+ const verify_registration_response_1 = require("./webauthn/verify-registration-response.cjs");
29
+ /**
30
+ * Returns the default (empty) state for {@link PasskeyController}.
31
+ *
32
+ * @returns A fresh state object with no enrolled passkey.
33
+ */
34
+ function getDefaultPasskeyControllerState() {
35
+ return { passkeyRecord: null };
36
+ }
37
+ exports.getDefaultPasskeyControllerState = getDefaultPasskeyControllerState;
38
+ const passkeyControllerMetadata = {
39
+ passkeyRecord: {
40
+ persist: true,
41
+ includeInDebugSnapshot: false,
42
+ includeInStateLogs: false,
43
+ usedInUi: true,
44
+ },
45
+ };
46
+ const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, constants_1.controllerName);
47
+ /**
48
+ * Selectors for {@link PasskeyControllerState}.
49
+ *
50
+ * Use these instead of dedicated getter methods on the controller, so that
51
+ * derived values can be consumed from Redux selectors and other places that
52
+ * only have access to a state object.
53
+ */
54
+ exports.passkeyControllerSelectors = {
55
+ selectIsPasskeyEnrolled: (state) => state.passkeyRecord !== null,
56
+ };
57
+ /**
58
+ * Passkey-based protection for the vault encryption key (WebAuthn).
59
+ *
60
+ * Uses PRF-backed derivation when available; otherwise uses the credential
61
+ * `userHandle`.
62
+ */
63
+ class PasskeyController extends base_controller_1.BaseController {
64
+ /**
65
+ * Constructs a new {@link PasskeyController}.
66
+ *
67
+ * @param args - The constructor arguments.
68
+ * @param args.messenger - The messenger suited for this controller.
69
+ * @param args.state - Initial state. Missing properties are filled in with
70
+ * defaults from {@link getDefaultPasskeyControllerState}.
71
+ * @param args.rpID - WebAuthn Relying Party ID (typically the eTLD+1 of the
72
+ * client origin, or `localhost` in dev).
73
+ * @param args.rpName - Human-readable Relying Party name shown by the OS
74
+ * passkey UI.
75
+ * @param args.expectedOrigin - One or more acceptable origins for the
76
+ * `clientDataJSON.origin` check (e.g. `chrome-extension://...`).
77
+ * @param args.userName - Optional `user.name` shown by the OS passkey UI.
78
+ * Defaults to `rpName` so client builds (Stable, Flask, etc.) can
79
+ * differentiate without changes here.
80
+ * @param args.userDisplayName - Optional `user.displayName` shown by the OS
81
+ * passkey UI. Defaults to `rpName`.
82
+ */
83
+ constructor({ messenger, state = {}, rpID, rpName, expectedOrigin, userName, userDisplayName, }) {
84
+ super({
85
+ messenger,
86
+ metadata: passkeyControllerMetadata,
87
+ name: constants_1.controllerName,
88
+ state: { ...getDefaultPasskeyControllerState(), ...state },
89
+ });
90
+ _PasskeyController_instances.add(this);
91
+ _PasskeyController_ceremonyManager.set(this, new ceremony_manager_1.CeremonyManager());
92
+ _PasskeyController_rpID.set(this, void 0);
93
+ _PasskeyController_rpName.set(this, void 0);
94
+ _PasskeyController_expectedOrigin.set(this, void 0);
95
+ _PasskeyController_userName.set(this, void 0);
96
+ _PasskeyController_userDisplayName.set(this, void 0);
97
+ __classPrivateFieldSet(this, _PasskeyController_rpID, rpID, "f");
98
+ __classPrivateFieldSet(this, _PasskeyController_rpName, rpName, "f");
99
+ __classPrivateFieldSet(this, _PasskeyController_expectedOrigin, expectedOrigin, "f");
100
+ __classPrivateFieldSet(this, _PasskeyController_userName, userName ?? rpName, "f");
101
+ __classPrivateFieldSet(this, _PasskeyController_userDisplayName, userDisplayName ?? rpName, "f");
102
+ }
103
+ /**
104
+ * Checks if the passkey is enrolled.
105
+ *
106
+ * @returns Whether the passkey is enrolled.
107
+ */
108
+ isPasskeyEnrolled() {
109
+ return exports.passkeyControllerSelectors.selectIsPasskeyEnrolled(this.state);
110
+ }
111
+ /**
112
+ * Registration options for enrolling a passkey.
113
+ *
114
+ * Call before {@link protectVaultKeyWithPasskey}.
115
+ *
116
+ * @param creationOptionsConfig - Optional configuration.
117
+ * @param creationOptionsConfig.prfAvailable - Omit PRF when `false`. Default `true`.
118
+ * @returns Options for `navigator.credentials.create()`.
119
+ */
120
+ generateRegistrationOptions(creationOptionsConfig) {
121
+ const includePrf = creationOptionsConfig?.prfAvailable !== false;
122
+ const prfSalt = includePrf
123
+ ? (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice())
124
+ : undefined;
125
+ const userHandle = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(64).slice());
126
+ const challenge = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice());
127
+ const extensions = {};
128
+ if (prfSalt) {
129
+ extensions.prf = { eval: { first: prfSalt } };
130
+ }
131
+ const options = {
132
+ rp: { name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"), id: __classPrivateFieldGet(this, _PasskeyController_rpID, "f") },
133
+ user: {
134
+ id: userHandle,
135
+ name: __classPrivateFieldGet(this, _PasskeyController_userName, "f"),
136
+ displayName: __classPrivateFieldGet(this, _PasskeyController_userDisplayName, "f"),
137
+ },
138
+ challenge,
139
+ pubKeyCredParams: [
140
+ { alg: constants_2.COSEALG.EdDSA, type: 'public-key' },
141
+ { alg: constants_2.COSEALG.ES256, type: 'public-key' },
142
+ { alg: constants_2.COSEALG.RS256, type: 'public-key' },
143
+ ],
144
+ timeout: ceremony_manager_1.WEBAUTHN_TIMEOUT_MS,
145
+ authenticatorSelection: {
146
+ userVerification: 'preferred',
147
+ authenticatorAttachment: 'platform',
148
+ residentKey: 'preferred',
149
+ },
150
+ hints: ['client-device', 'hybrid'],
151
+ attestation: 'none',
152
+ ...(Object.keys(extensions).length > 0 ? { extensions } : {}),
153
+ };
154
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveRegistrationCeremony(challenge, {
155
+ userHandle,
156
+ prfSalt: prfSalt ?? '',
157
+ challenge,
158
+ createdAt: Date.now(),
159
+ });
160
+ return options;
161
+ }
162
+ /**
163
+ * WebAuthn request options for authenticating with the enrolled passkey.
164
+ *
165
+ * Call before {@link retrieveVaultKeyWithPasskey},
166
+ * {@link verifyPasskeyAuthentication}, or {@link renewVaultKeyProtection}.
167
+ *
168
+ * @returns Options for `navigator.credentials.get()`.
169
+ */
170
+ generateAuthenticationOptions() {
171
+ const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
172
+ const challenge = (0, encoding_1.bytesToBase64URL)((0, webcrypto_1.randomBytes)(32).slice());
173
+ const extensions = {};
174
+ if (record.keyDerivation.method === 'prf') {
175
+ extensions.prf = { eval: { first: record.keyDerivation.prfSalt } };
176
+ }
177
+ const options = {
178
+ challenge,
179
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
180
+ allowCredentials: [
181
+ {
182
+ id: record.credential.id,
183
+ type: 'public-key',
184
+ transports: record.credential.transports,
185
+ },
186
+ ],
187
+ userVerification: 'preferred',
188
+ hints: ['client-device', 'hybrid'],
189
+ timeout: ceremony_manager_1.WEBAUTHN_TIMEOUT_MS,
190
+ extensions,
191
+ };
192
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveAuthenticationCeremony(challenge, {
193
+ challenge,
194
+ createdAt: Date.now(),
195
+ });
196
+ return options;
197
+ }
198
+ /**
199
+ * Completes enrollment and binds the vault key to the new passkey.
200
+ *
201
+ * @param params - Protection parameters.
202
+ * @param params.registrationResponse - Credential from `navigator.credentials.create()`.
203
+ * @param params.vaultKey - Vault encryption key to protect.
204
+ */
205
+ async protectVaultKeyWithPasskey(params) {
206
+ const { registrationResponse, vaultKey } = params;
207
+ // get challenge
208
+ const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
209
+ const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
210
+ if (!registrationCeremony) {
211
+ log('No active passkey registration ceremony for challenge');
212
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoRegistrationCeremony });
213
+ }
214
+ try {
215
+ // verify registration response
216
+ const { verified, registrationInfo } = await (0, verify_registration_response_1.verifyRegistrationResponse)({
217
+ response: registrationResponse,
218
+ expectedChallenge: registrationCeremony.challenge,
219
+ expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
220
+ expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
221
+ requireUserVerification: false,
222
+ }).catch((error) => {
223
+ log('Error verifying passkey registration response', error);
224
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, {
225
+ code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed,
226
+ cause: error instanceof Error ? error : new Error(String(error)),
227
+ });
228
+ });
229
+ if (!verified || !registrationInfo) {
230
+ log('Passkey registration verification returned unverified or missing registration info');
231
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: constants_1.PasskeyControllerErrorCode.RegistrationVerificationFailed });
232
+ }
233
+ // derive key
234
+ const { encKey, keyDerivation } = (0, key_derivation_1.deriveKeyFromRegistrationResponse)(registrationResponse, registrationCeremony, registrationInfo.credentialId);
235
+ // encrypt vault key
236
+ const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(vaultKey, encKey);
237
+ // persist passkey record
238
+ this.update((state) => {
239
+ state.passkeyRecord = {
240
+ credential: {
241
+ id: registrationInfo.credentialId,
242
+ publicKey: (0, encoding_1.bytesToBase64URL)(registrationInfo.publicKey),
243
+ counter: registrationInfo.counter,
244
+ transports: registrationInfo.transports,
245
+ },
246
+ encryptedVaultKey: { ciphertext, iv },
247
+ keyDerivation,
248
+ };
249
+ });
250
+ }
251
+ finally {
252
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
253
+ }
254
+ }
255
+ /**
256
+ * Returns the decrypted vault encryption key from the passkey authentication
257
+ * response.
258
+ *
259
+ * @param authenticationResponse - Credential from `navigator.credentials.get()`.
260
+ * @returns The vault encryption key.
261
+ */
262
+ async retrieveVaultKeyWithPasskey(authenticationResponse) {
263
+ // verify authentication response
264
+ await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse);
265
+ // derive key (#verifyAuthenticationResponse guarantees enrolled)
266
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
267
+ const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
268
+ // decrypt vault key
269
+ let vaultKey;
270
+ try {
271
+ vaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
272
+ }
273
+ catch (cause) {
274
+ log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
275
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
276
+ code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
277
+ cause: cause instanceof Error ? cause : new Error(String(cause)),
278
+ });
279
+ }
280
+ return vaultKey;
281
+ }
282
+ /**
283
+ * Returns whether passkey authentication succeeds for this credential (same
284
+ * work as {@link retrieveVaultKeyWithPasskey} without exposing the vault key).
285
+ *
286
+ * Returns `false` only when the failure is a {@link PasskeyControllerError}
287
+ * with a defined `code`. Unexpected errors (e.g. malformed `clientDataJSON`,
288
+ * internal bugs) are rethrown.
289
+ *
290
+ * @param authenticationResponse - Credential from `navigator.credentials.get()`.
291
+ * @returns `true` if authentication succeeds, otherwise `false`.
292
+ */
293
+ async verifyPasskeyAuthentication(authenticationResponse) {
294
+ try {
295
+ await this.retrieveVaultKeyWithPasskey(authenticationResponse);
296
+ return true;
297
+ }
298
+ catch (error) {
299
+ if (error instanceof errors_1.PasskeyControllerError && error.code !== undefined) {
300
+ return false;
301
+ }
302
+ throw error;
303
+ }
304
+ }
305
+ /**
306
+ * Updates the vault encryption key for the same passkey (e.g. after a password change).
307
+ *
308
+ * Caller MUST first verify the assertion via {@link verifyPasskeyAuthentication}
309
+ * or {@link retrieveVaultKeyWithPasskey}. This method does not re-verify
310
+ * because the ceremony is single-use (deleted on verify) and the signature
311
+ * counter is advanced (replay would be rejected). Authentication here is
312
+ * enforced by the prior verification plus the `oldVaultKey` match below.
313
+ *
314
+ * @param params - Renewal parameters.
315
+ * @param params.authenticationResponse - Credential from `navigator.credentials.get()`,
316
+ * already verified by the caller.
317
+ * @param params.oldVaultKey - Expected current vault key.
318
+ * @param params.newVaultKey - New vault key to protect.
319
+ */
320
+ async renewVaultKeyProtection(params) {
321
+ const { authenticationResponse } = params;
322
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
323
+ // derive key
324
+ const encKey = (0, key_derivation_1.deriveKeyFromAuthenticationResponse)(authenticationResponse, passkeyRecord);
325
+ // decrypt vault key
326
+ let decryptedVaultKey;
327
+ try {
328
+ decryptedVaultKey = (0, crypto_1.decryptWithKey)(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
329
+ }
330
+ catch (error) {
331
+ log('Error decrypting vault key during passkey vault key renewal', error instanceof Error ? error : new Error(String(error)));
332
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
333
+ code: constants_1.PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
334
+ cause: error instanceof Error ? error : new Error(String(error)),
335
+ });
336
+ }
337
+ // check if vault key matches
338
+ const { oldVaultKey, newVaultKey } = params;
339
+ if (decryptedVaultKey !== oldVaultKey) {
340
+ log('Passkey renewal rejected: decrypted vault key does not match oldVaultKey');
341
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.VaultKeyMismatch, { code: constants_1.PasskeyControllerErrorCode.VaultKeyMismatch });
342
+ }
343
+ // encrypt new vault key
344
+ const { ciphertext, iv } = (0, crypto_1.encryptWithKey)(newVaultKey, encKey);
345
+ // persist passkey record (mutate current state only for vault key material)
346
+ this.update((state) => {
347
+ if (!state.passkeyRecord) {
348
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
349
+ code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
350
+ });
351
+ }
352
+ state.passkeyRecord.encryptedVaultKey = { ciphertext, iv };
353
+ });
354
+ }
355
+ /**
356
+ * Unenrolls the passkey, removing the protected vault key material.
357
+ */
358
+ removePasskey() {
359
+ this.update(() => getDefaultPasskeyControllerState());
360
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
361
+ }
362
+ /**
363
+ * Resets state and clears in-flight registration/authentication ceremonies.
364
+ */
365
+ clearState() {
366
+ this.removePasskey();
367
+ }
368
+ /**
369
+ * Releases all in-flight ceremony state and tears down the messenger.
370
+ */
371
+ destroy() {
372
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
373
+ super.destroy();
374
+ }
375
+ }
376
+ exports.PasskeyController = PasskeyController;
377
+ _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() {
378
+ const record = this.state.passkeyRecord;
379
+ if (!record) {
380
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, {
381
+ code: constants_1.PasskeyControllerErrorCode.NotEnrolled,
382
+ });
383
+ }
384
+ return record;
385
+ }, _PasskeyController_getChallengeFromClientData = function _PasskeyController_getChallengeFromClientData(clientDataJSON) {
386
+ return (0, decode_client_data_json_1.decodeClientDataJSON)(clientDataJSON).challenge;
387
+ }, _PasskeyController_verifyAuthenticationResponse =
388
+ /**
389
+ * Verifies an authentication response for the enrolled passkey.
390
+ *
391
+ * @param authenticationResponse - Authentication result JSON.
392
+ */
393
+ async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse) {
394
+ let challenge;
395
+ try {
396
+ // get challenge
397
+ challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
398
+ // get passkey record
399
+ const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
400
+ // get authentication ceremony
401
+ const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
402
+ if (!authenticationCeremony) {
403
+ log('No active passkey authentication ceremony for challenge');
404
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: constants_1.PasskeyControllerErrorCode.NoAuthenticationCeremony });
405
+ }
406
+ // verify authentication response
407
+ const result = await (0, verify_authentication_response_1.verifyAuthenticationResponse)({
408
+ response: authenticationResponse,
409
+ expectedChallenge: authenticationCeremony.challenge,
410
+ expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
411
+ expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
412
+ credential: {
413
+ id: record.credential.id,
414
+ publicKey: (0, encoding_1.base64URLToBytes)(record.credential.publicKey),
415
+ counter: record.credential.counter,
416
+ transports: record.credential.transports,
417
+ },
418
+ // UV optional for device compatibility; vault key remains password-gated.
419
+ requireUserVerification: false,
420
+ }).catch((error) => {
421
+ log('Error verifying passkey authentication response', error instanceof Error ? error : new Error(String(error)));
422
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, {
423
+ code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed,
424
+ cause: error instanceof Error ? error : new Error(String(error)),
425
+ });
426
+ });
427
+ if (!result.verified) {
428
+ log('Passkey authentication verification returned unverified');
429
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.AuthenticationVerificationFailed, {
430
+ code: constants_1.PasskeyControllerErrorCode.AuthenticationVerificationFailed,
431
+ });
432
+ }
433
+ // persist passkey record with updated counter without clobbering concurrent updates
434
+ this.update((state) => {
435
+ if (!state.passkeyRecord) {
436
+ throw new errors_1.PasskeyControllerError(constants_1.PasskeyControllerErrorMessage.NotEnrolled, { code: constants_1.PasskeyControllerErrorCode.NotEnrolled });
437
+ }
438
+ const latest = state.passkeyRecord;
439
+ latest.credential.counter = Math.max(result.authenticationInfo.newCounter, latest.credential.counter);
440
+ });
441
+ }
442
+ finally {
443
+ if (challenge) {
444
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
445
+ }
446
+ }
447
+ };
448
+ //# sourceMappingURL=PasskeyController.cjs.map