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