-
-
Notifications
You must be signed in to change notification settings - Fork 278
Feat/TO-540: Add passkey controller #8422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tanguyenvn
wants to merge
42
commits into
main
Choose a base branch
from
feat/TO-540-passkey-controller
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 f56e452
refactor: passkey controller to return registration and authenticatio…
tanguyenvn 47b6bf9
feat: update configuration of passkeys
tanguyenvn e3c0db5
feat: allow update encryption key
tanguyenvn d5816f0
refactor: change method names and use crypto packages
tanguyenvn ffffcee
feat: port verification of authentication and registration responses …
tanguyenvn 9dfd697
feat: add passkey verification and refactor code
tanguyenvn e54d225
feat: accept prf availability from browser when generating registrati…
tanguyenvn f9fb799
feat: use rp ID and name from platform
tanguyenvn afe1777
chore: refactor docs
tanguyenvn 6f7b1c7
feat: allow clearing state in passkey controller
tanguyenvn 29cdfba
feat: allow multiple passkey ceremony at the same time
tanguyenvn ceeb6fc
refactor: use lowercase file names for webauthn module
tanguyenvn 31bdb6b
chore: refactor passkey controller
tanguyenvn 69a8dae
feat: allow verifying passkey authentication
tanguyenvn d1ff0ae
feat: add hints to passkey registration and authentication options
tanguyenvn e530b68
feat: skip verification of passkey authentication response when renew…
tanguyenvn d6f8ef2
refactor: restructure passkey record state
tanguyenvn d46d8eb
fix: address review comments
tanguyenvn 9503f03
Merge branch 'main' into feat/TO-540-passkey-controller
tanguyenvn 11609cb
chore: update deps
tanguyenvn 373de11
fix: address review comments
tanguyenvn 5246c45
fix: refactor to follow controller guidelines
tanguyenvn df275aa
fix: CI on README and deps
tanguyenvn c1086a0
fix: lint:msc
tanguyenvn c8db2c8
refactor: revert changes in seedless onboarding
tanguyenvn 75c5c1b
refactor: add comments to webauthn package
tanguyenvn 7375cba
refactor: use concatBytes from metamask/utils package
tanguyenvn 5390ca6
refactor: tests
tanguyenvn a0298b5
feat: update verify siganture to keep parity with simplewebauthn
tanguyenvn 85df40f
fix: address review comments
tanguyenvn 0591b27
Merge branch 'main' into feat/TO-540-passkey-controller
tanguyenvn b49dc0f
fix: address CI failure
tanguyenvn 5cc58c9
fix: address cursor review and constraints
tanguyenvn c5a5d0c
fix: remove barrel pattern for webauthn and utils
tanguyenvn 25805d7
fix: address review comments
tanguyenvn 8a3bf04
test: increase test coverage to 100%
tanguyenvn 210b8b9
refactor: add errors and logging
tanguyenvn 819f0c4
Update Release 935.0.0
tanguyenvn 11fb44f
fix: address review comments
tanguyenvn b97bc05
fix: lint
tanguyenvn 4c08463
Revert "Update Release 935.0.0"
tanguyenvn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }, | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| module.exports = CustomTestEnvironment; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.