Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
271ca41
feat: init passkey controller
tanguyenvn Apr 8, 2026
f56e452
refactor: passkey controller to return registration and authenticatio…
tanguyenvn Apr 10, 2026
47b6bf9
feat: update configuration of passkeys
tanguyenvn Apr 10, 2026
e3c0db5
feat: allow update encryption key
tanguyenvn Apr 14, 2026
d5816f0
refactor: change method names and use crypto packages
tanguyenvn Apr 15, 2026
ffffcee
feat: port verification of authentication and registration responses …
tanguyenvn Apr 15, 2026
9dfd697
feat: add passkey verification and refactor code
tanguyenvn Apr 16, 2026
e54d225
feat: accept prf availability from browser when generating registrati…
tanguyenvn Apr 16, 2026
f9fb799
feat: use rp ID and name from platform
tanguyenvn Apr 16, 2026
afe1777
chore: refactor docs
tanguyenvn Apr 16, 2026
6f7b1c7
feat: allow clearing state in passkey controller
tanguyenvn Apr 16, 2026
29cdfba
feat: allow multiple passkey ceremony at the same time
tanguyenvn Apr 18, 2026
ceeb6fc
refactor: use lowercase file names for webauthn module
tanguyenvn Apr 18, 2026
31bdb6b
chore: refactor passkey controller
tanguyenvn Apr 18, 2026
69a8dae
feat: allow verifying passkey authentication
tanguyenvn Apr 18, 2026
d1ff0ae
feat: add hints to passkey registration and authentication options
tanguyenvn Apr 20, 2026
e530b68
feat: skip verification of passkey authentication response when renew…
tanguyenvn Apr 20, 2026
d6f8ef2
refactor: restructure passkey record state
tanguyenvn Apr 21, 2026
d46d8eb
fix: address review comments
tanguyenvn Apr 21, 2026
9503f03
Merge branch 'main' into feat/TO-540-passkey-controller
tanguyenvn Apr 21, 2026
11609cb
chore: update deps
tanguyenvn Apr 21, 2026
373de11
fix: address review comments
tanguyenvn Apr 21, 2026
5246c45
fix: refactor to follow controller guidelines
tanguyenvn Apr 21, 2026
df275aa
fix: CI on README and deps
tanguyenvn Apr 21, 2026
c1086a0
fix: lint:msc
tanguyenvn Apr 21, 2026
c8db2c8
refactor: revert changes in seedless onboarding
tanguyenvn Apr 21, 2026
75c5c1b
refactor: add comments to webauthn package
tanguyenvn Apr 22, 2026
7375cba
refactor: use concatBytes from metamask/utils package
tanguyenvn Apr 22, 2026
5390ca6
refactor: tests
tanguyenvn Apr 22, 2026
a0298b5
feat: update verify siganture to keep parity with simplewebauthn
tanguyenvn Apr 22, 2026
85df40f
fix: address review comments
tanguyenvn Apr 22, 2026
0591b27
Merge branch 'main' into feat/TO-540-passkey-controller
tanguyenvn Apr 22, 2026
b49dc0f
fix: address CI failure
tanguyenvn Apr 22, 2026
5cc58c9
fix: address cursor review and constraints
tanguyenvn Apr 22, 2026
c5a5d0c
fix: remove barrel pattern for webauthn and utils
tanguyenvn Apr 23, 2026
25805d7
fix: address review comments
tanguyenvn Apr 23, 2026
8a3bf04
test: increase test coverage to 100%
tanguyenvn Apr 23, 2026
210b8b9
refactor: add errors and logging
tanguyenvn Apr 23, 2026
819f0c4
Update Release 935.0.0
tanguyenvn Apr 24, 2026
11fb44f
fix: address review comments
tanguyenvn Apr 24, 2026
b97bc05
fix: lint
tanguyenvn Apr 24, 2026
4c08463
Revert "Update Release 935.0.0"
tanguyenvn Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@

## Web3Auth Team
/packages/seedless-onboarding-controller @MetaMask/web3auth
/packages/passkey-controller @MetaMask/web3auth
/packages/shield-controller @MetaMask/web3auth
/packages/subscription-controller @MetaMask/web3auth
/packages/claims-controller @MetaMask/web3auth
Expand Down Expand Up @@ -168,6 +169,8 @@
/packages/geolocation-controller/CHANGELOG.md @MetaMask/core-platform
/packages/keyring-controller/package.json @MetaMask/accounts-engineers @MetaMask/core-platform
/packages/keyring-controller/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform
/packages/passkey-controller/package.json @MetaMask/web3auth @MetaMask/core-platform
/packages/passkey-controller/CHANGELOG.md @MetaMask/web3auth @MetaMask/core-platform
/packages/logging-controller/package.json @MetaMask/confirmations @MetaMask/core-platform
/packages/logging-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform
/packages/message-manager/package.json @MetaMask/confirmations @MetaMask/core-platform
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Each package in this repository has its own README where you can find installati
- [`@metamask/network-controller`](packages/network-controller)
- [`@metamask/network-enablement-controller`](packages/network-enablement-controller)
- [`@metamask/notification-services-controller`](packages/notification-services-controller)
- [`@metamask/passkey-controller`](packages/passkey-controller)
- [`@metamask/permission-controller`](packages/permission-controller)
- [`@metamask/permission-log-controller`](packages/permission-log-controller)
- [`@metamask/perps-controller`](packages/perps-controller)
Expand Down Expand Up @@ -168,6 +169,7 @@ linkStyle default opacity:0.5
network_controller(["@metamask/network-controller"]);
network_enablement_controller(["@metamask/network-enablement-controller"]);
notification_services_controller(["@metamask/notification-services-controller"]);
passkey_controller(["@metamask/passkey-controller"]);
permission_controller(["@metamask/permission-controller"]);
permission_log_controller(["@metamask/permission-log-controller"]);
perps_controller(["@metamask/perps-controller"]);
Expand Down Expand Up @@ -423,6 +425,8 @@ linkStyle default opacity:0.5
notification_services_controller --> keyring_controller;
notification_services_controller --> messenger;
notification_services_controller --> profile_sync_controller;
passkey_controller --> base_controller;
passkey_controller --> messenger;
permission_controller --> approval_controller;
permission_controller --> base_controller;
permission_controller --> controller_utils;
Expand Down
24 changes: 24 additions & 0 deletions packages/passkey-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- 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.
- `PasskeyControllerError` with stable `code`, optional `cause` / `context`, `toJSON`, and `toString`; `PasskeyControllerErrorCode`, `PasskeyControllerErrorMessage`, and `controllerName`. Replaces `PasskeyAuthenticationRejectedError`—use `PasskeyControllerError` and `code` for auth failures.
- **BREAKING:** Operational error messages are prefixed with `PasskeyController - `; prefer `code` or `instanceof PasskeyControllerError` over matching raw strings.
- `renewVaultKeyProtection` uses the same `vault_key_decryption_failed` code as `retrieveVaultKeyWithPasskey` when AES-GCM decrypt fails.
- 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).
- Debug logging (via `@metamask/utils`) for registration/authentication verification failures, missing ceremony state, vault decrypt failures, and vault key mismatch during renewal.

### Fixed

- 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.
- 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).

[Unreleased]: https://github.com/MetaMask/core/
21 changes: 21 additions & 0 deletions packages/passkey-controller/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 MetaMask

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
155 changes: 155 additions & 0 deletions packages/passkey-controller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# `@metamask/passkey-controller`

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.

## Installation

`yarn add @metamask/passkey-controller`

or

`npm install @metamask/passkey-controller`

## Overview

The controller follows a two-phase ceremony pattern for both enrollment and authentication:

1. **Generate options** — call a synchronous method that returns options JSON and records **in-flight ceremony** state (challenge-keyed; not a user login session).
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).

### Key derivation strategies

The controller supports two key derivation methods, selected automatically during enrollment:

| Strategy | When used | Input key material |
| -------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| **PRF** | Authenticator supports the [WebAuthn PRF extension](https://w3c.github.io/webauthn/#prf-extension) | PRF evaluation output |
| **userHandle** | PRF is unavailable | Random `userHandle` generated during registration |

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.

## Usage

### Setting up the controller

```typescript
import { PasskeyController } from '@metamask/passkey-controller';
import type { PasskeyControllerMessenger } from '@metamask/passkey-controller';

const messenger: PasskeyControllerMessenger = /* create via root messenger */;

const controller = new PasskeyController({
messenger,
rpID: 'example.com',
rpName: 'My Wallet',
expectedOrigin: 'chrome-extension://abcdef1234567890',
// Optional — both default to `rpName` when omitted.
userName: 'My Wallet',
userDisplayName: 'My Wallet',
});
```

### Passkey enrollment (registration)

```typescript
// 1. Generate registration options (synchronous)
const options = controller.generateRegistrationOptions();

// 2. Pass options to the browser WebAuthn API
const response = await navigator.credentials.create({ publicKey: options });

// 3. Verify and protect the vault key
await controller.protectVaultKeyWithPasskey({
registrationResponse: response,
vaultKey: myVaultEncryptionKey,
});
```

### Passkey unlock (authentication)

```typescript
// 1. Generate authentication options (synchronous)
const options = controller.generateAuthenticationOptions();

// 2. Pass options to the browser WebAuthn API
const response = await navigator.credentials.get({ publicKey: options });

// 3. Verify and retrieve the vault key
const vaultKey = await controller.retrieveVaultKeyWithPasskey(response);
```

### Password change (vault key renewal)

```typescript
const options = controller.generateAuthenticationOptions();
const response = await navigator.credentials.get({ publicKey: options });

await controller.renewVaultKeyProtection({
authenticationResponse: response,
oldVaultKey: currentVaultKey,
newVaultKey: newVaultKey,
});
```

### Checking enrollment and removing a passkey

```typescript
controller.isPasskeyEnrolled(); // boolean

controller.removePasskey(); // user-facing unenroll; clears persisted passkey and in-flight ceremonies

controller.clearState(); // same persisted reset + clears in-flight ceremony state; use for app lifecycle (e.g. wallet reset)
```

### Selectors

For Redux selectors and other code paths without access to the controller
instance, use the exported selector(s):

```typescript
import { passkeyControllerSelectors } from '@metamask/passkey-controller';

passkeyControllerSelectors.selectIsPasskeyEnrolled(state); // boolean
```

### Errors

`PasskeyControllerError` is thrown for controller failures. Expected operational
cases use a stable `code` from `PasskeyControllerErrorCode` (for example:
`not_enrolled`, `no_registration_ceremony`, `authentication_verification_failed`,
`missing_key_material`, `vault_key_decryption_failed`). Human-readable strings
live on `PasskeyControllerErrorMessage`. Use `instanceof PasskeyControllerError`
and a defined `error.code` to tell these apart from malformed WebAuthn payloads
and other `Error` values. Thrown errors from the internal WebAuthn verify helpers
are also surfaced as `PasskeyControllerError` with the same `registration_verification_failed`
or `authentication_verification_failed` code and the original error as `cause`.
`verifyPasskeyAuthentication` returns `false` only for
those controller errors (with `code`) and rethrows everything else.

## API

### State

| Property | Type | Description |
| --------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
| `passkeyRecord` | `PasskeyRecord \| null` | Enrolled passkey credential data and encrypted vault key. `null` when no passkey is enrolled. |

### Messenger actions

| Action | Handler |
| ---------------------------- | ------------------------------------ |
| `PasskeyController:getState` | Returns the current controller state |

For derived enrollment status outside of components that hold a controller
reference, use `passkeyControllerSelectors.selectIsPasskeyEnrolled` (see
[Selectors](#selectors)).

### Messenger events

| Event | Payload |
| -------------------------------- | ------------------------------------------------------------ |
| `PasskeyController:stateChanged` | Emitted when state changes (standard `BaseController` event) |

## Contributing

This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
14 changes: 14 additions & 0 deletions packages/passkey-controller/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const merge = require('deepmerge');
const path = require('path');

const baseConfig = require('../../jest.config.packages');

const displayName = path.basename(__dirname);

module.exports = merge(baseConfig, {
displayName,
testEnvironment: '<rootDir>/jest.environment.js',
coverageThreshold: {
global: { branches: 100, functions: 100, lines: 100, statements: 100 },
},
});
17 changes: 17 additions & 0 deletions packages/passkey-controller/jest.environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { TestEnvironment } = require('jest-environment-node');

/**
* Passkey orchestration uses the Web Crypto API (`crypto.getRandomValues`) in Node tests.
*/
class CustomTestEnvironment extends TestEnvironment {
async setup() {
await super.setup();
if (typeof this.global.crypto === 'undefined') {
// Only used for testing.
// eslint-disable-next-line n/no-unsupported-features/node-builtins
this.global.crypto = require('crypto').webcrypto;
Comment thread
mcmire marked this conversation as resolved.
}
}
}

module.exports = CustomTestEnvironment;
78 changes: 78 additions & 0 deletions packages/passkey-controller/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "@metamask/passkey-controller",
"version": "0.0.0",
"description": "Controller and utilities for passkey-based wallet unlock",
"keywords": [
"Ethereum",
"MetaMask"
],
"homepage": "https://github.com/MetaMask/core/tree/main/packages/passkey-controller#readme",
"bugs": {
"url": "https://github.com/MetaMask/core/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/core.git"
},
"files": [
"dist/"
],
"sideEffects": false,
"main": "./dist/index.cjs",
"types": "./dist/index.d.cts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references",
"build:all": "ts-bridge --project tsconfig.build.json --verbose --clean",
"build:docs": "typedoc",
"changelog:update": "../../scripts/update-changelog.sh @metamask/passkey-controller",
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/passkey-controller",
"publish:preview": "yarn npm publish --tag preview",
"since-latest-release": "../../scripts/since-latest-release.sh",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",
"test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache",
"test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@levischuck/tiny-cbor": "^0.3.3",
"@metamask/base-controller": "^9.1.0",
"@metamask/messenger": "^1.1.1",
"@metamask/utils": "^11.9.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.2",
"@noble/hashes": "^1.8.0"
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.0",
"@ts-bridge/cli": "^0.6.4",
"@types/jest": "^29.5.14",
"deepmerge": "^4.2.2",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"ts-jest": "^29.2.5",
"typedoc": "^0.25.13",
"typedoc-plugin-missing-exports": "^2.0.0",
"typescript": "~5.3.3"
},
"engines": {
"node": "^18.18 || >=20"
}
}
Loading
Loading