@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
@@ -0,0 +1,443 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ 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");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _PasskeyController_instances, _PasskeyController_ceremonyManager, _PasskeyController_rpID, _PasskeyController_rpName, _PasskeyController_expectedOrigin, _PasskeyController_userName, _PasskeyController_userDisplayName, _PasskeyController_requireEnrolled, _PasskeyController_getChallengeFromClientData, _PasskeyController_verifyAuthenticationResponse;
13
+ import { BaseController } from "@metamask/base-controller";
14
+ import { randomBytes } from "@noble/ciphers/webcrypto";
15
+ import { WEBAUTHN_TIMEOUT_MS, CeremonyManager } from "./ceremony-manager.mjs";
16
+ import { controllerName, PasskeyControllerErrorCode, PasskeyControllerErrorMessage } from "./constants.mjs";
17
+ import { PasskeyControllerError } from "./errors.mjs";
18
+ import { deriveKeyFromAuthenticationResponse, deriveKeyFromRegistrationResponse } from "./key-derivation.mjs";
19
+ import { createModuleLogger, projectLogger } from "./logger.mjs";
20
+ import { decryptWithKey, encryptWithKey } from "./utils/crypto.mjs";
21
+ import { base64URLToBytes, bytesToBase64URL } from "./utils/encoding.mjs";
22
+ import { COSEALG } from "./webauthn/constants.mjs";
23
+ import { decodeClientDataJSON } from "./webauthn/decode-client-data-json.mjs";
24
+ import { verifyAuthenticationResponse } from "./webauthn/verify-authentication-response.mjs";
25
+ import { verifyRegistrationResponse } from "./webauthn/verify-registration-response.mjs";
26
+ /**
27
+ * Returns the default (empty) state for {@link PasskeyController}.
28
+ *
29
+ * @returns A fresh state object with no enrolled passkey.
30
+ */
31
+ export function getDefaultPasskeyControllerState() {
32
+ return { passkeyRecord: null };
33
+ }
34
+ const passkeyControllerMetadata = {
35
+ passkeyRecord: {
36
+ persist: true,
37
+ includeInDebugSnapshot: false,
38
+ includeInStateLogs: false,
39
+ usedInUi: true,
40
+ },
41
+ };
42
+ const log = createModuleLogger(projectLogger, controllerName);
43
+ /**
44
+ * Selectors for {@link PasskeyControllerState}.
45
+ *
46
+ * Use these instead of dedicated getter methods on the controller, so that
47
+ * derived values can be consumed from Redux selectors and other places that
48
+ * only have access to a state object.
49
+ */
50
+ export const passkeyControllerSelectors = {
51
+ selectIsPasskeyEnrolled: (state) => state.passkeyRecord !== null,
52
+ };
53
+ /**
54
+ * Passkey-based protection for the vault encryption key (WebAuthn).
55
+ *
56
+ * Uses PRF-backed derivation when available; otherwise uses the credential
57
+ * `userHandle`.
58
+ */
59
+ export class PasskeyController extends BaseController {
60
+ /**
61
+ * Constructs a new {@link PasskeyController}.
62
+ *
63
+ * @param args - The constructor arguments.
64
+ * @param args.messenger - The messenger suited for this controller.
65
+ * @param args.state - Initial state. Missing properties are filled in with
66
+ * defaults from {@link getDefaultPasskeyControllerState}.
67
+ * @param args.rpID - WebAuthn Relying Party ID (typically the eTLD+1 of the
68
+ * client origin, or `localhost` in dev).
69
+ * @param args.rpName - Human-readable Relying Party name shown by the OS
70
+ * passkey UI.
71
+ * @param args.expectedOrigin - One or more acceptable origins for the
72
+ * `clientDataJSON.origin` check (e.g. `chrome-extension://...`).
73
+ * @param args.userName - Optional `user.name` shown by the OS passkey UI.
74
+ * Defaults to `rpName` so client builds (Stable, Flask, etc.) can
75
+ * differentiate without changes here.
76
+ * @param args.userDisplayName - Optional `user.displayName` shown by the OS
77
+ * passkey UI. Defaults to `rpName`.
78
+ */
79
+ constructor({ messenger, state = {}, rpID, rpName, expectedOrigin, userName, userDisplayName, }) {
80
+ super({
81
+ messenger,
82
+ metadata: passkeyControllerMetadata,
83
+ name: controllerName,
84
+ state: { ...getDefaultPasskeyControllerState(), ...state },
85
+ });
86
+ _PasskeyController_instances.add(this);
87
+ _PasskeyController_ceremonyManager.set(this, new CeremonyManager());
88
+ _PasskeyController_rpID.set(this, void 0);
89
+ _PasskeyController_rpName.set(this, void 0);
90
+ _PasskeyController_expectedOrigin.set(this, void 0);
91
+ _PasskeyController_userName.set(this, void 0);
92
+ _PasskeyController_userDisplayName.set(this, void 0);
93
+ __classPrivateFieldSet(this, _PasskeyController_rpID, rpID, "f");
94
+ __classPrivateFieldSet(this, _PasskeyController_rpName, rpName, "f");
95
+ __classPrivateFieldSet(this, _PasskeyController_expectedOrigin, expectedOrigin, "f");
96
+ __classPrivateFieldSet(this, _PasskeyController_userName, userName ?? rpName, "f");
97
+ __classPrivateFieldSet(this, _PasskeyController_userDisplayName, userDisplayName ?? rpName, "f");
98
+ }
99
+ /**
100
+ * Checks if the passkey is enrolled.
101
+ *
102
+ * @returns Whether the passkey is enrolled.
103
+ */
104
+ isPasskeyEnrolled() {
105
+ return passkeyControllerSelectors.selectIsPasskeyEnrolled(this.state);
106
+ }
107
+ /**
108
+ * Registration options for enrolling a passkey.
109
+ *
110
+ * Call before {@link protectVaultKeyWithPasskey}.
111
+ *
112
+ * @param creationOptionsConfig - Optional configuration.
113
+ * @param creationOptionsConfig.prfAvailable - Omit PRF when `false`. Default `true`.
114
+ * @returns Options for `navigator.credentials.create()`.
115
+ */
116
+ generateRegistrationOptions(creationOptionsConfig) {
117
+ const includePrf = creationOptionsConfig?.prfAvailable !== false;
118
+ const prfSalt = includePrf
119
+ ? bytesToBase64URL(randomBytes(32).slice())
120
+ : undefined;
121
+ const userHandle = bytesToBase64URL(randomBytes(64).slice());
122
+ const challenge = bytesToBase64URL(randomBytes(32).slice());
123
+ const extensions = {};
124
+ if (prfSalt) {
125
+ extensions.prf = { eval: { first: prfSalt } };
126
+ }
127
+ const options = {
128
+ rp: { name: __classPrivateFieldGet(this, _PasskeyController_rpName, "f"), id: __classPrivateFieldGet(this, _PasskeyController_rpID, "f") },
129
+ user: {
130
+ id: userHandle,
131
+ name: __classPrivateFieldGet(this, _PasskeyController_userName, "f"),
132
+ displayName: __classPrivateFieldGet(this, _PasskeyController_userDisplayName, "f"),
133
+ },
134
+ challenge,
135
+ pubKeyCredParams: [
136
+ { alg: COSEALG.EdDSA, type: 'public-key' },
137
+ { alg: COSEALG.ES256, type: 'public-key' },
138
+ { alg: COSEALG.RS256, type: 'public-key' },
139
+ ],
140
+ timeout: WEBAUTHN_TIMEOUT_MS,
141
+ authenticatorSelection: {
142
+ userVerification: 'preferred',
143
+ authenticatorAttachment: 'platform',
144
+ residentKey: 'preferred',
145
+ },
146
+ hints: ['client-device', 'hybrid'],
147
+ attestation: 'none',
148
+ ...(Object.keys(extensions).length > 0 ? { extensions } : {}),
149
+ };
150
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveRegistrationCeremony(challenge, {
151
+ userHandle,
152
+ prfSalt: prfSalt ?? '',
153
+ challenge,
154
+ createdAt: Date.now(),
155
+ });
156
+ return options;
157
+ }
158
+ /**
159
+ * WebAuthn request options for authenticating with the enrolled passkey.
160
+ *
161
+ * Call before {@link retrieveVaultKeyWithPasskey},
162
+ * {@link verifyPasskeyAuthentication}, or {@link renewVaultKeyProtection}.
163
+ *
164
+ * @returns Options for `navigator.credentials.get()`.
165
+ */
166
+ generateAuthenticationOptions() {
167
+ const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
168
+ const challenge = bytesToBase64URL(randomBytes(32).slice());
169
+ const extensions = {};
170
+ if (record.keyDerivation.method === 'prf') {
171
+ extensions.prf = { eval: { first: record.keyDerivation.prfSalt } };
172
+ }
173
+ const options = {
174
+ challenge,
175
+ rpId: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
176
+ allowCredentials: [
177
+ {
178
+ id: record.credential.id,
179
+ type: 'public-key',
180
+ transports: record.credential.transports,
181
+ },
182
+ ],
183
+ userVerification: 'preferred',
184
+ hints: ['client-device', 'hybrid'],
185
+ timeout: WEBAUTHN_TIMEOUT_MS,
186
+ extensions,
187
+ };
188
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").saveAuthenticationCeremony(challenge, {
189
+ challenge,
190
+ createdAt: Date.now(),
191
+ });
192
+ return options;
193
+ }
194
+ /**
195
+ * Completes enrollment and binds the vault key to the new passkey.
196
+ *
197
+ * @param params - Protection parameters.
198
+ * @param params.registrationResponse - Credential from `navigator.credentials.create()`.
199
+ * @param params.vaultKey - Vault encryption key to protect.
200
+ */
201
+ async protectVaultKeyWithPasskey(params) {
202
+ const { registrationResponse, vaultKey } = params;
203
+ // get challenge
204
+ const challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, registrationResponse.response.clientDataJSON);
205
+ const registrationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getRegistrationCeremony(challenge);
206
+ if (!registrationCeremony) {
207
+ log('No active passkey registration ceremony for challenge');
208
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NoRegistrationCeremony, { code: PasskeyControllerErrorCode.NoRegistrationCeremony });
209
+ }
210
+ try {
211
+ // verify registration response
212
+ const { verified, registrationInfo } = await verifyRegistrationResponse({
213
+ response: registrationResponse,
214
+ expectedChallenge: registrationCeremony.challenge,
215
+ expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
216
+ expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
217
+ requireUserVerification: false,
218
+ }).catch((error) => {
219
+ log('Error verifying passkey registration response', error);
220
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.RegistrationVerificationFailed, {
221
+ code: PasskeyControllerErrorCode.RegistrationVerificationFailed,
222
+ cause: error instanceof Error ? error : new Error(String(error)),
223
+ });
224
+ });
225
+ if (!verified || !registrationInfo) {
226
+ log('Passkey registration verification returned unverified or missing registration info');
227
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.RegistrationVerificationFailed, { code: PasskeyControllerErrorCode.RegistrationVerificationFailed });
228
+ }
229
+ // derive key
230
+ const { encKey, keyDerivation } = deriveKeyFromRegistrationResponse(registrationResponse, registrationCeremony, registrationInfo.credentialId);
231
+ // encrypt vault key
232
+ const { ciphertext, iv } = encryptWithKey(vaultKey, encKey);
233
+ // persist passkey record
234
+ this.update((state) => {
235
+ state.passkeyRecord = {
236
+ credential: {
237
+ id: registrationInfo.credentialId,
238
+ publicKey: bytesToBase64URL(registrationInfo.publicKey),
239
+ counter: registrationInfo.counter,
240
+ transports: registrationInfo.transports,
241
+ },
242
+ encryptedVaultKey: { ciphertext, iv },
243
+ keyDerivation,
244
+ };
245
+ });
246
+ }
247
+ finally {
248
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteRegistrationCeremony(challenge);
249
+ }
250
+ }
251
+ /**
252
+ * Returns the decrypted vault encryption key from the passkey authentication
253
+ * response.
254
+ *
255
+ * @param authenticationResponse - Credential from `navigator.credentials.get()`.
256
+ * @returns The vault encryption key.
257
+ */
258
+ async retrieveVaultKeyWithPasskey(authenticationResponse) {
259
+ // verify authentication response
260
+ await __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_verifyAuthenticationResponse).call(this, authenticationResponse);
261
+ // derive key (#verifyAuthenticationResponse guarantees enrolled)
262
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
263
+ const encKey = deriveKeyFromAuthenticationResponse(authenticationResponse, passkeyRecord);
264
+ // decrypt vault key
265
+ let vaultKey;
266
+ try {
267
+ vaultKey = decryptWithKey(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
268
+ }
269
+ catch (cause) {
270
+ log('Error decrypting vault key with passkey', cause instanceof Error ? cause : new Error(String(cause)));
271
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
272
+ code: PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
273
+ cause: cause instanceof Error ? cause : new Error(String(cause)),
274
+ });
275
+ }
276
+ return vaultKey;
277
+ }
278
+ /**
279
+ * Returns whether passkey authentication succeeds for this credential (same
280
+ * work as {@link retrieveVaultKeyWithPasskey} without exposing the vault key).
281
+ *
282
+ * Returns `false` only when the failure is a {@link PasskeyControllerError}
283
+ * with a defined `code`. Unexpected errors (e.g. malformed `clientDataJSON`,
284
+ * internal bugs) are rethrown.
285
+ *
286
+ * @param authenticationResponse - Credential from `navigator.credentials.get()`.
287
+ * @returns `true` if authentication succeeds, otherwise `false`.
288
+ */
289
+ async verifyPasskeyAuthentication(authenticationResponse) {
290
+ try {
291
+ await this.retrieveVaultKeyWithPasskey(authenticationResponse);
292
+ return true;
293
+ }
294
+ catch (error) {
295
+ if (error instanceof PasskeyControllerError && error.code !== undefined) {
296
+ return false;
297
+ }
298
+ throw error;
299
+ }
300
+ }
301
+ /**
302
+ * Updates the vault encryption key for the same passkey (e.g. after a password change).
303
+ *
304
+ * Caller MUST first verify the assertion via {@link verifyPasskeyAuthentication}
305
+ * or {@link retrieveVaultKeyWithPasskey}. This method does not re-verify
306
+ * because the ceremony is single-use (deleted on verify) and the signature
307
+ * counter is advanced (replay would be rejected). Authentication here is
308
+ * enforced by the prior verification plus the `oldVaultKey` match below.
309
+ *
310
+ * @param params - Renewal parameters.
311
+ * @param params.authenticationResponse - Credential from `navigator.credentials.get()`,
312
+ * already verified by the caller.
313
+ * @param params.oldVaultKey - Expected current vault key.
314
+ * @param params.newVaultKey - New vault key to protect.
315
+ */
316
+ async renewVaultKeyProtection(params) {
317
+ const { authenticationResponse } = params;
318
+ const passkeyRecord = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
319
+ // derive key
320
+ const encKey = deriveKeyFromAuthenticationResponse(authenticationResponse, passkeyRecord);
321
+ // decrypt vault key
322
+ let decryptedVaultKey;
323
+ try {
324
+ decryptedVaultKey = decryptWithKey(passkeyRecord.encryptedVaultKey.ciphertext, passkeyRecord.encryptedVaultKey.iv, encKey);
325
+ }
326
+ catch (error) {
327
+ log('Error decrypting vault key during passkey vault key renewal', error instanceof Error ? error : new Error(String(error)));
328
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.VaultKeyDecryptionFailed, {
329
+ code: PasskeyControllerErrorCode.VaultKeyDecryptionFailed,
330
+ cause: error instanceof Error ? error : new Error(String(error)),
331
+ });
332
+ }
333
+ // check if vault key matches
334
+ const { oldVaultKey, newVaultKey } = params;
335
+ if (decryptedVaultKey !== oldVaultKey) {
336
+ log('Passkey renewal rejected: decrypted vault key does not match oldVaultKey');
337
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.VaultKeyMismatch, { code: PasskeyControllerErrorCode.VaultKeyMismatch });
338
+ }
339
+ // encrypt new vault key
340
+ const { ciphertext, iv } = encryptWithKey(newVaultKey, encKey);
341
+ // persist passkey record (mutate current state only for vault key material)
342
+ this.update((state) => {
343
+ if (!state.passkeyRecord) {
344
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, {
345
+ code: PasskeyControllerErrorCode.NotEnrolled,
346
+ });
347
+ }
348
+ state.passkeyRecord.encryptedVaultKey = { ciphertext, iv };
349
+ });
350
+ }
351
+ /**
352
+ * Unenrolls the passkey, removing the protected vault key material.
353
+ */
354
+ removePasskey() {
355
+ this.update(() => getDefaultPasskeyControllerState());
356
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
357
+ }
358
+ /**
359
+ * Resets state and clears in-flight registration/authentication ceremonies.
360
+ */
361
+ clearState() {
362
+ this.removePasskey();
363
+ }
364
+ /**
365
+ * Releases all in-flight ceremony state and tears down the messenger.
366
+ */
367
+ destroy() {
368
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").clear();
369
+ super.destroy();
370
+ }
371
+ }
372
+ _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() {
373
+ const record = this.state.passkeyRecord;
374
+ if (!record) {
375
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, {
376
+ code: PasskeyControllerErrorCode.NotEnrolled,
377
+ });
378
+ }
379
+ return record;
380
+ }, _PasskeyController_getChallengeFromClientData = function _PasskeyController_getChallengeFromClientData(clientDataJSON) {
381
+ return decodeClientDataJSON(clientDataJSON).challenge;
382
+ }, _PasskeyController_verifyAuthenticationResponse =
383
+ /**
384
+ * Verifies an authentication response for the enrolled passkey.
385
+ *
386
+ * @param authenticationResponse - Authentication result JSON.
387
+ */
388
+ async function _PasskeyController_verifyAuthenticationResponse(authenticationResponse) {
389
+ let challenge;
390
+ try {
391
+ // get challenge
392
+ challenge = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_getChallengeFromClientData).call(this, authenticationResponse.response.clientDataJSON);
393
+ // get passkey record
394
+ const record = __classPrivateFieldGet(this, _PasskeyController_instances, "m", _PasskeyController_requireEnrolled).call(this);
395
+ // get authentication ceremony
396
+ const authenticationCeremony = __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").getAuthenticationCeremony(challenge);
397
+ if (!authenticationCeremony) {
398
+ log('No active passkey authentication ceremony for challenge');
399
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NoAuthenticationCeremony, { code: PasskeyControllerErrorCode.NoAuthenticationCeremony });
400
+ }
401
+ // verify authentication response
402
+ const result = await verifyAuthenticationResponse({
403
+ response: authenticationResponse,
404
+ expectedChallenge: authenticationCeremony.challenge,
405
+ expectedOrigin: __classPrivateFieldGet(this, _PasskeyController_expectedOrigin, "f"),
406
+ expectedRPID: __classPrivateFieldGet(this, _PasskeyController_rpID, "f"),
407
+ credential: {
408
+ id: record.credential.id,
409
+ publicKey: base64URLToBytes(record.credential.publicKey),
410
+ counter: record.credential.counter,
411
+ transports: record.credential.transports,
412
+ },
413
+ // UV optional for device compatibility; vault key remains password-gated.
414
+ requireUserVerification: false,
415
+ }).catch((error) => {
416
+ log('Error verifying passkey authentication response', error instanceof Error ? error : new Error(String(error)));
417
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.AuthenticationVerificationFailed, {
418
+ code: PasskeyControllerErrorCode.AuthenticationVerificationFailed,
419
+ cause: error instanceof Error ? error : new Error(String(error)),
420
+ });
421
+ });
422
+ if (!result.verified) {
423
+ log('Passkey authentication verification returned unverified');
424
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.AuthenticationVerificationFailed, {
425
+ code: PasskeyControllerErrorCode.AuthenticationVerificationFailed,
426
+ });
427
+ }
428
+ // persist passkey record with updated counter without clobbering concurrent updates
429
+ this.update((state) => {
430
+ if (!state.passkeyRecord) {
431
+ throw new PasskeyControllerError(PasskeyControllerErrorMessage.NotEnrolled, { code: PasskeyControllerErrorCode.NotEnrolled });
432
+ }
433
+ const latest = state.passkeyRecord;
434
+ latest.credential.counter = Math.max(result.authenticationInfo.newCounter, latest.credential.counter);
435
+ });
436
+ }
437
+ finally {
438
+ if (challenge) {
439
+ __classPrivateFieldGet(this, _PasskeyController_ceremonyManager, "f").deleteAuthenticationCeremony(challenge);
440
+ }
441
+ }
442
+ };
443
+ //# sourceMappingURL=PasskeyController.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasskeyController.mjs","sourceRoot":"","sources":["../src/PasskeyController.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport { randomBytes } from '@noble/ciphers/webcrypto';\n\nimport { WEBAUTHN_TIMEOUT_MS, CeremonyManager } from './ceremony-manager';\nimport {\n controllerName,\n PasskeyControllerErrorCode,\n PasskeyControllerErrorMessage,\n} from './constants';\nimport { PasskeyControllerError } from './errors';\nimport {\n deriveKeyFromAuthenticationResponse,\n deriveKeyFromRegistrationResponse,\n} from './key-derivation';\nimport { createModuleLogger, projectLogger } from './logger';\nimport type { PasskeyRecord } from './types';\nimport { decryptWithKey, encryptWithKey } from './utils/crypto';\nimport { base64URLToBytes, bytesToBase64URL } from './utils/encoding';\nimport { COSEALG } from './webauthn/constants';\nimport { decodeClientDataJSON } from './webauthn/decode-client-data-json';\nimport type {\n PasskeyAuthenticationOptions,\n PasskeyAuthenticationResponse,\n PasskeyRegistrationOptions,\n PasskeyRegistrationResponse,\n} from './webauthn/types';\nimport { verifyAuthenticationResponse } from './webauthn/verify-authentication-response';\nimport { verifyRegistrationResponse } from './webauthn/verify-registration-response';\n\nexport type PasskeyControllerState = {\n passkeyRecord: PasskeyRecord | null;\n};\n\nexport type PasskeyControllerGetStateAction = ControllerGetStateAction<\n typeof controllerName,\n PasskeyControllerState\n>;\n\n/**\n * Actions exposed by {@link PasskeyController} on its messenger.\n *\n * Only `:getState` is exposed. Derived enrollment status is available via\n * {@link passkeyControllerSelectors.selectIsPasskeyEnrolled}, and lifecycle\n * methods ({@link PasskeyController.generateRegistrationOptions},\n * {@link PasskeyController.protectVaultKeyWithPasskey}, etc.) accept or\n * return non-`Json` runtime values (WebAuthn `PublicKeyCredential` objects\n * and the vault key string), so they require a direct controller reference.\n */\nexport type PasskeyControllerActions = PasskeyControllerGetStateAction;\n\nexport type PasskeyControllerStateChangedEvent = ControllerStateChangedEvent<\n typeof controllerName,\n PasskeyControllerState\n>;\n\nexport type PasskeyControllerEvents = PasskeyControllerStateChangedEvent;\n\nexport type PasskeyControllerMessenger = Messenger<\n typeof controllerName,\n PasskeyControllerActions,\n PasskeyControllerEvents\n>;\n\n/**\n * Returns the default (empty) state for {@link PasskeyController}.\n *\n * @returns A fresh state object with no enrolled passkey.\n */\nexport function getDefaultPasskeyControllerState(): PasskeyControllerState {\n return { passkeyRecord: null };\n}\n\nconst passkeyControllerMetadata = {\n passkeyRecord: {\n persist: true,\n includeInDebugSnapshot: false,\n includeInStateLogs: false,\n usedInUi: true,\n },\n} satisfies StateMetadata<PasskeyControllerState>;\n\nconst log = createModuleLogger(projectLogger, controllerName);\n\n/**\n * Selectors for {@link PasskeyControllerState}.\n *\n * Use these instead of dedicated getter methods on the controller, so that\n * derived values can be consumed from Redux selectors and other places that\n * only have access to a state object.\n */\nexport const passkeyControllerSelectors = {\n selectIsPasskeyEnrolled: (state: PasskeyControllerState): boolean =>\n state.passkeyRecord !== null,\n};\n\n/**\n * Passkey-based protection for the vault encryption key (WebAuthn).\n *\n * Uses PRF-backed derivation when available; otherwise uses the credential\n * `userHandle`.\n */\nexport class PasskeyController extends BaseController<\n typeof controllerName,\n PasskeyControllerState,\n PasskeyControllerMessenger\n> {\n readonly #ceremonyManager = new CeremonyManager();\n\n readonly #rpID: string;\n\n readonly #rpName: string;\n\n readonly #expectedOrigin: string | string[];\n\n readonly #userName: string;\n\n readonly #userDisplayName: string;\n\n /**\n * Constructs a new {@link PasskeyController}.\n *\n * @param args - The constructor arguments.\n * @param args.messenger - The messenger suited for this controller.\n * @param args.state - Initial state. Missing properties are filled in with\n * defaults from {@link getDefaultPasskeyControllerState}.\n * @param args.rpID - WebAuthn Relying Party ID (typically the eTLD+1 of the\n * client origin, or `localhost` in dev).\n * @param args.rpName - Human-readable Relying Party name shown by the OS\n * passkey UI.\n * @param args.expectedOrigin - One or more acceptable origins for the\n * `clientDataJSON.origin` check (e.g. `chrome-extension://...`).\n * @param args.userName - Optional `user.name` shown by the OS passkey UI.\n * Defaults to `rpName` so client builds (Stable, Flask, etc.) can\n * differentiate without changes here.\n * @param args.userDisplayName - Optional `user.displayName` shown by the OS\n * passkey UI. Defaults to `rpName`.\n */\n constructor({\n messenger,\n state = {},\n rpID,\n rpName,\n expectedOrigin,\n userName,\n userDisplayName,\n }: {\n messenger: PasskeyControllerMessenger;\n state?: Partial<PasskeyControllerState>;\n rpID: string;\n rpName: string;\n expectedOrigin: string | string[];\n userName?: string;\n userDisplayName?: string;\n }) {\n super({\n messenger,\n metadata: passkeyControllerMetadata,\n name: controllerName,\n state: { ...getDefaultPasskeyControllerState(), ...state },\n });\n\n this.#rpID = rpID;\n this.#rpName = rpName;\n this.#expectedOrigin = expectedOrigin;\n this.#userName = userName ?? rpName;\n this.#userDisplayName = userDisplayName ?? rpName;\n }\n\n #requireEnrolled(): PasskeyRecord {\n const record = this.state.passkeyRecord;\n if (!record) {\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.NotEnrolled,\n {\n code: PasskeyControllerErrorCode.NotEnrolled,\n },\n );\n }\n return record;\n }\n\n #getChallengeFromClientData(clientDataJSON: string): string {\n return decodeClientDataJSON(clientDataJSON).challenge;\n }\n\n /**\n * Checks if the passkey is enrolled.\n *\n * @returns Whether the passkey is enrolled.\n */\n isPasskeyEnrolled(): boolean {\n return passkeyControllerSelectors.selectIsPasskeyEnrolled(this.state);\n }\n\n /**\n * Registration options for enrolling a passkey.\n *\n * Call before {@link protectVaultKeyWithPasskey}.\n *\n * @param creationOptionsConfig - Optional configuration.\n * @param creationOptionsConfig.prfAvailable - Omit PRF when `false`. Default `true`.\n * @returns Options for `navigator.credentials.create()`.\n */\n generateRegistrationOptions(creationOptionsConfig?: {\n prfAvailable?: boolean;\n }): PasskeyRegistrationOptions {\n const includePrf = creationOptionsConfig?.prfAvailable !== false;\n const prfSalt = includePrf\n ? bytesToBase64URL(randomBytes(32).slice())\n : undefined;\n const userHandle = bytesToBase64URL(randomBytes(64).slice());\n const challenge = bytesToBase64URL(randomBytes(32).slice());\n\n const extensions: Record<string, unknown> = {};\n if (prfSalt) {\n extensions.prf = { eval: { first: prfSalt } };\n }\n\n const options: PasskeyRegistrationOptions = {\n rp: { name: this.#rpName, id: this.#rpID },\n user: {\n id: userHandle,\n name: this.#userName,\n displayName: this.#userDisplayName,\n },\n challenge,\n pubKeyCredParams: [\n { alg: COSEALG.EdDSA, type: 'public-key' },\n { alg: COSEALG.ES256, type: 'public-key' },\n { alg: COSEALG.RS256, type: 'public-key' },\n ],\n timeout: WEBAUTHN_TIMEOUT_MS,\n authenticatorSelection: {\n userVerification: 'preferred',\n authenticatorAttachment: 'platform',\n residentKey: 'preferred',\n },\n hints: ['client-device', 'hybrid'],\n attestation: 'none',\n ...(Object.keys(extensions).length > 0 ? { extensions } : {}),\n };\n\n this.#ceremonyManager.saveRegistrationCeremony(challenge, {\n userHandle,\n prfSalt: prfSalt ?? '',\n challenge,\n createdAt: Date.now(),\n });\n\n return options;\n }\n\n /**\n * WebAuthn request options for authenticating with the enrolled passkey.\n *\n * Call before {@link retrieveVaultKeyWithPasskey},\n * {@link verifyPasskeyAuthentication}, or {@link renewVaultKeyProtection}.\n *\n * @returns Options for `navigator.credentials.get()`.\n */\n generateAuthenticationOptions(): PasskeyAuthenticationOptions {\n const record = this.#requireEnrolled();\n\n const challenge = bytesToBase64URL(randomBytes(32).slice());\n\n const extensions: Record<string, unknown> = {};\n if (record.keyDerivation.method === 'prf') {\n extensions.prf = { eval: { first: record.keyDerivation.prfSalt } };\n }\n\n const options: PasskeyAuthenticationOptions = {\n challenge,\n rpId: this.#rpID,\n allowCredentials: [\n {\n id: record.credential.id,\n type: 'public-key',\n transports: record.credential.transports,\n },\n ],\n userVerification: 'preferred',\n hints: ['client-device', 'hybrid'],\n timeout: WEBAUTHN_TIMEOUT_MS,\n extensions,\n };\n\n this.#ceremonyManager.saveAuthenticationCeremony(challenge, {\n challenge,\n createdAt: Date.now(),\n });\n\n return options;\n }\n\n /**\n * Completes enrollment and binds the vault key to the new passkey.\n *\n * @param params - Protection parameters.\n * @param params.registrationResponse - Credential from `navigator.credentials.create()`.\n * @param params.vaultKey - Vault encryption key to protect.\n */\n async protectVaultKeyWithPasskey(params: {\n registrationResponse: PasskeyRegistrationResponse;\n vaultKey: string;\n }): Promise<void> {\n const { registrationResponse, vaultKey } = params;\n\n // get challenge\n const challenge = this.#getChallengeFromClientData(\n registrationResponse.response.clientDataJSON,\n );\n const registrationCeremony =\n this.#ceremonyManager.getRegistrationCeremony(challenge);\n if (!registrationCeremony) {\n log('No active passkey registration ceremony for challenge');\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.NoRegistrationCeremony,\n { code: PasskeyControllerErrorCode.NoRegistrationCeremony },\n );\n }\n\n try {\n // verify registration response\n const { verified, registrationInfo } = await verifyRegistrationResponse({\n response: registrationResponse,\n expectedChallenge: registrationCeremony.challenge,\n expectedOrigin: this.#expectedOrigin,\n expectedRPID: this.#rpID,\n requireUserVerification: false,\n }).catch((error) => {\n log('Error verifying passkey registration response', error);\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.RegistrationVerificationFailed,\n {\n code: PasskeyControllerErrorCode.RegistrationVerificationFailed,\n cause: error instanceof Error ? error : new Error(String(error)),\n },\n );\n });\n if (!verified || !registrationInfo) {\n log(\n 'Passkey registration verification returned unverified or missing registration info',\n );\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.RegistrationVerificationFailed,\n { code: PasskeyControllerErrorCode.RegistrationVerificationFailed },\n );\n }\n\n // derive key\n const { encKey, keyDerivation } = deriveKeyFromRegistrationResponse(\n registrationResponse,\n registrationCeremony,\n registrationInfo.credentialId,\n );\n\n // encrypt vault key\n const { ciphertext, iv } = encryptWithKey(vaultKey, encKey);\n\n // persist passkey record\n this.update((state) => {\n state.passkeyRecord = {\n credential: {\n id: registrationInfo.credentialId,\n publicKey: bytesToBase64URL(registrationInfo.publicKey),\n counter: registrationInfo.counter,\n transports: registrationInfo.transports,\n },\n encryptedVaultKey: { ciphertext, iv },\n keyDerivation,\n };\n });\n } finally {\n this.#ceremonyManager.deleteRegistrationCeremony(challenge);\n }\n }\n\n /**\n * Returns the decrypted vault encryption key from the passkey authentication\n * response.\n *\n * @param authenticationResponse - Credential from `navigator.credentials.get()`.\n * @returns The vault encryption key.\n */\n async retrieveVaultKeyWithPasskey(\n authenticationResponse: PasskeyAuthenticationResponse,\n ): Promise<string> {\n // verify authentication response\n await this.#verifyAuthenticationResponse(authenticationResponse);\n\n // derive key (#verifyAuthenticationResponse guarantees enrolled)\n const passkeyRecord = this.#requireEnrolled();\n const encKey = deriveKeyFromAuthenticationResponse(\n authenticationResponse,\n passkeyRecord,\n );\n\n // decrypt vault key\n let vaultKey: string;\n try {\n vaultKey = decryptWithKey(\n passkeyRecord.encryptedVaultKey.ciphertext,\n passkeyRecord.encryptedVaultKey.iv,\n encKey,\n );\n } catch (cause) {\n log(\n 'Error decrypting vault key with passkey',\n cause instanceof Error ? cause : new Error(String(cause)),\n );\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.VaultKeyDecryptionFailed,\n {\n code: PasskeyControllerErrorCode.VaultKeyDecryptionFailed,\n cause: cause instanceof Error ? cause : new Error(String(cause)),\n },\n );\n }\n\n return vaultKey;\n }\n\n /**\n * Returns whether passkey authentication succeeds for this credential (same\n * work as {@link retrieveVaultKeyWithPasskey} without exposing the vault key).\n *\n * Returns `false` only when the failure is a {@link PasskeyControllerError}\n * with a defined `code`. Unexpected errors (e.g. malformed `clientDataJSON`,\n * internal bugs) are rethrown.\n *\n * @param authenticationResponse - Credential from `navigator.credentials.get()`.\n * @returns `true` if authentication succeeds, otherwise `false`.\n */\n async verifyPasskeyAuthentication(\n authenticationResponse: PasskeyAuthenticationResponse,\n ): Promise<boolean> {\n try {\n await this.retrieveVaultKeyWithPasskey(authenticationResponse);\n return true;\n } catch (error: unknown) {\n if (error instanceof PasskeyControllerError && error.code !== undefined) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Updates the vault encryption key for the same passkey (e.g. after a password change).\n *\n * Caller MUST first verify the assertion via {@link verifyPasskeyAuthentication}\n * or {@link retrieveVaultKeyWithPasskey}. This method does not re-verify\n * because the ceremony is single-use (deleted on verify) and the signature\n * counter is advanced (replay would be rejected). Authentication here is\n * enforced by the prior verification plus the `oldVaultKey` match below.\n *\n * @param params - Renewal parameters.\n * @param params.authenticationResponse - Credential from `navigator.credentials.get()`,\n * already verified by the caller.\n * @param params.oldVaultKey - Expected current vault key.\n * @param params.newVaultKey - New vault key to protect.\n */\n async renewVaultKeyProtection(params: {\n authenticationResponse: PasskeyAuthenticationResponse;\n oldVaultKey: string;\n newVaultKey: string;\n }): Promise<void> {\n const { authenticationResponse } = params;\n const passkeyRecord = this.#requireEnrolled();\n\n // derive key\n const encKey = deriveKeyFromAuthenticationResponse(\n authenticationResponse,\n passkeyRecord,\n );\n\n // decrypt vault key\n let decryptedVaultKey: string;\n try {\n decryptedVaultKey = decryptWithKey(\n passkeyRecord.encryptedVaultKey.ciphertext,\n passkeyRecord.encryptedVaultKey.iv,\n encKey,\n );\n } catch (error) {\n log(\n 'Error decrypting vault key during passkey vault key renewal',\n error instanceof Error ? error : new Error(String(error)),\n );\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.VaultKeyDecryptionFailed,\n {\n code: PasskeyControllerErrorCode.VaultKeyDecryptionFailed,\n cause: error instanceof Error ? error : new Error(String(error)),\n },\n );\n }\n\n // check if vault key matches\n const { oldVaultKey, newVaultKey } = params;\n if (decryptedVaultKey !== oldVaultKey) {\n log(\n 'Passkey renewal rejected: decrypted vault key does not match oldVaultKey',\n );\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.VaultKeyMismatch,\n { code: PasskeyControllerErrorCode.VaultKeyMismatch },\n );\n }\n\n // encrypt new vault key\n const { ciphertext, iv } = encryptWithKey(newVaultKey, encKey);\n\n // persist passkey record (mutate current state only for vault key material)\n this.update((state) => {\n if (!state.passkeyRecord) {\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.NotEnrolled,\n {\n code: PasskeyControllerErrorCode.NotEnrolled,\n },\n );\n }\n state.passkeyRecord.encryptedVaultKey = { ciphertext, iv };\n });\n }\n\n /**\n * Unenrolls the passkey, removing the protected vault key material.\n */\n removePasskey(): void {\n this.update(() => getDefaultPasskeyControllerState());\n this.#ceremonyManager.clear();\n }\n\n /**\n * Resets state and clears in-flight registration/authentication ceremonies.\n */\n clearState(): void {\n this.removePasskey();\n }\n\n /**\n * Releases all in-flight ceremony state and tears down the messenger.\n */\n destroy(): void {\n this.#ceremonyManager.clear();\n super.destroy();\n }\n\n /**\n * Verifies an authentication response for the enrolled passkey.\n *\n * @param authenticationResponse - Authentication result JSON.\n */\n async #verifyAuthenticationResponse(\n authenticationResponse: PasskeyAuthenticationResponse,\n ): Promise<void> {\n let challenge: string | undefined;\n try {\n // get challenge\n challenge = this.#getChallengeFromClientData(\n authenticationResponse.response.clientDataJSON,\n );\n\n // get passkey record\n const record = this.#requireEnrolled();\n\n // get authentication ceremony\n const authenticationCeremony =\n this.#ceremonyManager.getAuthenticationCeremony(challenge);\n if (!authenticationCeremony) {\n log('No active passkey authentication ceremony for challenge');\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.NoAuthenticationCeremony,\n { code: PasskeyControllerErrorCode.NoAuthenticationCeremony },\n );\n }\n\n // verify authentication response\n const result = await verifyAuthenticationResponse({\n response: authenticationResponse,\n expectedChallenge: authenticationCeremony.challenge,\n expectedOrigin: this.#expectedOrigin,\n expectedRPID: this.#rpID,\n credential: {\n id: record.credential.id,\n publicKey: base64URLToBytes(record.credential.publicKey),\n counter: record.credential.counter,\n transports: record.credential.transports,\n },\n // UV optional for device compatibility; vault key remains password-gated.\n requireUserVerification: false,\n }).catch((error) => {\n log(\n 'Error verifying passkey authentication response',\n error instanceof Error ? error : new Error(String(error)),\n );\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.AuthenticationVerificationFailed,\n {\n code: PasskeyControllerErrorCode.AuthenticationVerificationFailed,\n cause: error instanceof Error ? error : new Error(String(error)),\n },\n );\n });\n if (!result.verified) {\n log('Passkey authentication verification returned unverified');\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.AuthenticationVerificationFailed,\n {\n code: PasskeyControllerErrorCode.AuthenticationVerificationFailed,\n },\n );\n }\n\n // persist passkey record with updated counter without clobbering concurrent updates\n this.update((state) => {\n if (!state.passkeyRecord) {\n throw new PasskeyControllerError(\n PasskeyControllerErrorMessage.NotEnrolled,\n { code: PasskeyControllerErrorCode.NotEnrolled },\n );\n }\n const latest = state.passkeyRecord;\n latest.credential.counter = Math.max(\n result.authenticationInfo.newCounter,\n latest.credential.counter,\n );\n });\n } finally {\n if (challenge) {\n this.#ceremonyManager.deleteAuthenticationCeremony(challenge);\n }\n }\n }\n}\n"]}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
4
+ 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");
5
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
+ };
7
+ var _CeremonyManager_instances, _CeremonyManager_registrationMap, _CeremonyManager_authenticationMap, _CeremonyManager_getMap, _CeremonyManager_pruneExpired, _CeremonyManager_enforceCapacity;
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.CeremonyManager = exports.MAX_CONCURRENT_PASSKEY_CEREMONIES = exports.CEREMONY_MAX_AGE_MS = exports.CEREMONY_TTL_SLACK_MS = exports.WEBAUTHN_TIMEOUT_MS = void 0;
10
+ /** WebAuthn `timeout` for credential creation and assertion (ms). */
11
+ exports.WEBAUTHN_TIMEOUT_MS = 60000;
12
+ /**
13
+ * Extra allowance beyond {@link WEBAUTHN_TIMEOUT_MS} before in-memory
14
+ * ceremony state is discarded (covers slow UX and clock skew).
15
+ */
16
+ exports.CEREMONY_TTL_SLACK_MS = 15000;
17
+ /**
18
+ * Maximum age for in-flight registration or authentication ceremony state
19
+ * (between options and verified response). This bounds the lifetime of a
20
+ * single WebAuthn ceremony only; it is not a user login session timeout.
21
+ */
22
+ exports.CEREMONY_MAX_AGE_MS = exports.WEBAUTHN_TIMEOUT_MS + exports.CEREMONY_TTL_SLACK_MS;
23
+ /**
24
+ * Upper bound on concurrent in-memory ceremonies per flow type (registration
25
+ * vs authentication), for abuse / leak protection.
26
+ */
27
+ exports.MAX_CONCURRENT_PASSKEY_CEREMONIES = 16;
28
+ /**
29
+ * In-memory store for in-flight WebAuthn ceremonies (registration vs authentication),
30
+ * keyed by base64url challenge. Enforces TTL and a per-flow size cap; not user session state.
31
+ */
32
+ class CeremonyManager {
33
+ constructor() {
34
+ _CeremonyManager_instances.add(this);
35
+ _CeremonyManager_registrationMap.set(this, new Map());
36
+ _CeremonyManager_authenticationMap.set(this, new Map());
37
+ }
38
+ /**
39
+ * Records registration ceremony state after pruning expired rows and evicting oldest if at cap.
40
+ *
41
+ * @param challenge - Same base64url challenge as in the creation options `challenge` field.
42
+ * @param ceremony - Payload to retrieve when the registration response returns.
43
+ */
44
+ saveRegistrationCeremony(challenge, ceremony) {
45
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_pruneExpired).call(this, 'registration');
46
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_enforceCapacity).call(this, 'registration');
47
+ __classPrivateFieldGet(this, _CeremonyManager_registrationMap, "f").set(challenge, ceremony);
48
+ }
49
+ /**
50
+ * Records authentication ceremony state after pruning expired rows and evicting oldest if at cap.
51
+ *
52
+ * @param challenge - Same base64url challenge as in the request options `challenge` field.
53
+ * @param ceremony - Payload to retrieve when the assertion response returns.
54
+ */
55
+ saveAuthenticationCeremony(challenge, ceremony) {
56
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_pruneExpired).call(this, 'authentication');
57
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_enforceCapacity).call(this, 'authentication');
58
+ __classPrivateFieldGet(this, _CeremonyManager_authenticationMap, "f").set(challenge, ceremony);
59
+ }
60
+ /**
61
+ * Returns registration ceremony for a challenge, pruning expired entries on this map first.
62
+ *
63
+ * @param challenge - Base64url challenge from decoded `clientDataJSON` (matches stored key).
64
+ * @returns Stored ceremony, or `undefined` if none or expired.
65
+ */
66
+ getRegistrationCeremony(challenge) {
67
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_pruneExpired).call(this, 'registration');
68
+ return __classPrivateFieldGet(this, _CeremonyManager_registrationMap, "f").get(challenge);
69
+ }
70
+ /**
71
+ * Returns authentication ceremony for a challenge, pruning expired entries on this map first.
72
+ *
73
+ * @param challenge - Base64url challenge from decoded `clientDataJSON` (matches stored key).
74
+ * @returns Stored ceremony, or `undefined` if none or expired.
75
+ */
76
+ getAuthenticationCeremony(challenge) {
77
+ __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_pruneExpired).call(this, 'authentication');
78
+ return __classPrivateFieldGet(this, _CeremonyManager_authenticationMap, "f").get(challenge);
79
+ }
80
+ /**
81
+ * Removes a registration ceremony by challenge.
82
+ *
83
+ * @param challenge - Map key for the ceremony to remove.
84
+ * @returns Whether an entry was deleted.
85
+ */
86
+ deleteRegistrationCeremony(challenge) {
87
+ return __classPrivateFieldGet(this, _CeremonyManager_registrationMap, "f").delete(challenge);
88
+ }
89
+ /**
90
+ * Removes an authentication ceremony by challenge.
91
+ *
92
+ * @param challenge - Map key for the ceremony to remove.
93
+ * @returns Whether an entry was deleted.
94
+ */
95
+ deleteAuthenticationCeremony(challenge) {
96
+ return __classPrivateFieldGet(this, _CeremonyManager_authenticationMap, "f").delete(challenge);
97
+ }
98
+ /** Drops all in-flight registration and authentication ceremonies. */
99
+ clear() {
100
+ __classPrivateFieldGet(this, _CeremonyManager_registrationMap, "f").clear();
101
+ __classPrivateFieldGet(this, _CeremonyManager_authenticationMap, "f").clear();
102
+ }
103
+ }
104
+ exports.CeremonyManager = CeremonyManager;
105
+ _CeremonyManager_registrationMap = new WeakMap(), _CeremonyManager_authenticationMap = new WeakMap(), _CeremonyManager_instances = new WeakSet(), _CeremonyManager_getMap = function _CeremonyManager_getMap(ceremonyType) {
106
+ return ceremonyType === 'registration'
107
+ ? __classPrivateFieldGet(this, _CeremonyManager_registrationMap, "f")
108
+ : __classPrivateFieldGet(this, _CeremonyManager_authenticationMap, "f");
109
+ }, _CeremonyManager_pruneExpired = function _CeremonyManager_pruneExpired(ceremonyType) {
110
+ const now = Date.now();
111
+ const map = __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_getMap).call(this, ceremonyType);
112
+ for (const [key, ceremony] of map) {
113
+ if (now - ceremony.createdAt > exports.CEREMONY_MAX_AGE_MS) {
114
+ map.delete(key);
115
+ }
116
+ }
117
+ }, _CeremonyManager_enforceCapacity = function _CeremonyManager_enforceCapacity(ceremonyType) {
118
+ const map = __classPrivateFieldGet(this, _CeremonyManager_instances, "m", _CeremonyManager_getMap).call(this, ceremonyType);
119
+ while (map.size >= exports.MAX_CONCURRENT_PASSKEY_CEREMONIES) {
120
+ let oldestKey;
121
+ let oldestTime = Infinity;
122
+ for (const [mapKey, ceremony] of map) {
123
+ if (ceremony.createdAt < oldestTime) {
124
+ oldestTime = ceremony.createdAt;
125
+ oldestKey = mapKey;
126
+ }
127
+ }
128
+ if (oldestKey === undefined) {
129
+ break;
130
+ }
131
+ map.delete(oldestKey);
132
+ }
133
+ };
134
+ //# sourceMappingURL=ceremony-manager.cjs.map