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