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