Skip to content

feat(passkey): reveal SRP and private keys with passkey verification#8996

Open
tanguyenvn wants to merge 9 commits into
mainfrom
feat/TO-737-export-srp-passkey
Open

feat(passkey): reveal SRP and private keys with passkey verification#8996
tanguyenvn wants to merge 9 commits into
mainfrom
feat/TO-737-export-srp-passkey

Conversation

@tanguyenvn
Copy link
Copy Markdown
Contributor

@tanguyenvn tanguyenvn commented Jun 4, 2026

Description

Passkey-enrolled wallets currently require the wallet password to reveal sensitive key material (Secret Recovery Phrase and account private keys), even when the user has already unlocked MetaMask with a passkey. That creates friction and inconsistency with other passkey-gated flows (e.g. change password).

This PR adds password-less reveal paths for non–social-login wallets with an enrolled passkey:

  • SRP reveal: revealSeedWordsWithPasskey verifies the WebAuthn assertion, retrieves the passkey-wrapped vault key via retrieveVaultKeyWithPasskey, and calls keyringController.exportSeedPhrase({ encryptionKey: vaultKey }).
  • Private key reveal: exportAccountsWithPasskey uses the same vault-key retrieval, then calls keyringController.exportAccount({ encryptionKey: vaultKey }, address) for each address in a multichain account group. The multichain private key list UI uses passkey verification as the initial credential step, with password fallback.
  • Shared component: Extracts reusable PasskeyVerification (and runPasskeyVerificationCeremony) from change-password, reducing duplicated WebAuthn ceremony logic across settings and reveal flows.
  • Metrics: Adds SrpRevealWithPasskey MetaMetrics events (started / completed / failed).

In both flows, the passkey assertion is not only a step-up UI gate — the passkey-derived vault key must successfully decrypt the vault via the keyring controller export APIs, cryptographically binding the passkey to this vault.

Not supported:

  • Social-login (seedless) wallets — still password-only.
  • Side panel when the enrolled passkey is incompatible with WebAuthn in the side panel — falls back to password (no full-tab handoff).

Dependency: Requires the breaking @metamask/keyring-controller change that accepts VerificationCredentials ({ password } | { encryptionKey }) on exportSeedPhrase and exportAccount (mm-core PR for TO-737).

Changelog

CHANGELOG entry: Added the ability to reveal your Secret Recovery Phrase and account private keys using passkey verification instead of your wallet password.

Related issues

Fixes: TO-737

Manual testing steps

SRP reveal

  1. Set up a wallet with passkey enrolled (non–social-login).
  2. Go to Settings → Security & Privacy → Reveal Secret Recovery Phrase.
  3. Complete the SRP quiz (if shown).
  4. Confirm the passkey verification step appears instead of the password prompt.
  5. Complete the passkey ceremony and verify the SRP is revealed.
  6. Click Use password and confirm the password flow still works.
  7. Cancel or fail the passkey ceremony and confirm fallback to the password prompt.

Private key reveal (multichain account group)

  1. Open the multichain account group private key reveal flow.
  2. Confirm passkey verification is offered when passkey is active.
  3. Complete the passkey ceremony and verify private keys are shown for exportable addresses.
  4. Confirm password fallback works on failure or when choosing Use password.

General

  1. Repeat in the side panel with a passkey incompatible with side-panel WebAuthn — confirm password fallback.
  2. Confirm social-login wallets still use password-only reveal.
  3. Run unit tests: yarn test:unit ui/pages/keychains/reveal-seed.test.tsx ui/components/app/passkey-verification/passkey-verification.test.tsx ui/store/actions.test.js app/scripts/metamask-controller.actions.test.js

Screenshots/Recordings

Before

After

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

High Risk
Breaking API on sensitive export methods plus new encryption-key verification path; incorrect credential handling could affect SRP/private-key export security for all consumers.

Overview
BREAKING: exportSeedPhrase and exportAccount now accept a Credentials object ({ password } or { encryptionKey }) instead of a plain password string, with changelog and messenger action docs updated accordingly.

Re-authentication is centralized in #verifyCredentials, which validates either the wallet password or the serialized vault encryption key (via new #verifyEncryptionKey and decryptWithKey). Password-based export behavior is preserved; callers can pass a passkey-derived vault key so export is cryptographically tied to vault decryption, not only a UI gate.

exportAccount now enforces the unlocked controller check (#assertIsUnlocked) like exportSeedPhrase. Unit tests cover encryption-key success/failure, missing vault, and locked-state errors for both export paths.

Reviewed by Cursor Bugbot for commit 834f9ac. Bugbot is set up for automated code reviews on this repo. Configure here.

@tanguyenvn tanguyenvn requested review from a team as code owners June 4, 2026 06:19
@tanguyenvn tanguyenvn self-assigned this Jun 4, 2026
@tanguyenvn
Copy link
Copy Markdown
Contributor Author

I'll need to support exporting private keys of imported accounts using passkeys

@ccharly
Copy link
Copy Markdown
Contributor

ccharly commented Jun 5, 2026

I'll need to support exporting private keys of imported accounts using passkeys

I was about to review, but IMO we should apply the new logic with the credentials in exportAccount too anyway.

The idea being, we now have several "credential kind" to unlock sensitive operations in this controller, passkeys knows the "vault key" (that they use with submitEncryptionKey), so they should be able to use this encrytion key (credential) anywhere we need "strong permission" (exportSeedPhrase and exportAccount).

I would also add this support in this PR!

Comment on lines +1092 to +1098
if (typeof credentials === 'string') {
await this.verifyPassword(credentials);
} else if (hasProperty(credentials, 'password')) {
await this.verifyPassword(credentials.password as string);
} else {
await this.verifyEncryptionKey(credentials.encryptionKey);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a new #verifyCredentials method for this, so we can use it in exportSeedPhrase and in exportAccount too.


if (typeof credentials === 'string') {
await this.verifyPassword(credentials);
} else if (hasProperty(credentials, 'password')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use in here, that's the pattern we use in this controller already:

Suggested change
} else if (hasProperty(credentials, 'password')) {
} else if ('password' in credentials) {

if (typeof credentials === 'string') {
await this.verifyPassword(credentials);
} else if (hasProperty(credentials, 'password')) {
await this.verifyPassword(credentials.password as string);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to type-cast here (at least, not when using if ('password' in credentials)

Suggested change
await this.verifyPassword(credentials.password as string);
await this.verifyPassword(credentials.password);

@tanguyenvn tanguyenvn requested a review from ccharly June 5, 2026 09:33
@tanguyenvn tanguyenvn changed the title feat(keyring-controller): allow exportSeedPhrase with encryption key credentials feat(keyring-controller): allow exportSeedPhrase and exportAccount with encryption key credentials Jun 5, 2026
@tanguyenvn tanguyenvn changed the title feat(keyring-controller): allow exportSeedPhrase and exportAccount with encryption key credentials feat(passkey): reveal SRP and private keys with passkey verification Jun 5, 2026
*
* @param encryptionKey - Serialized vault encryption key.
*/
async verifyEncryptionKey(encryptionKey: string): Promise<void> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't need to export it publicly for now, unlike verifyPassword that could be used to write "end-user logic" (e.g. to test the password), we should not really need to call verifyEncryptionKey explicitely 🤔

We could make this private (e.g. #verifyEncryptionKey).

WDYT @danroc?

* Credentials for re-authenticating the keyring during sensitive operations
* such as `exportSeedPhrase` and `exportAccount`.
*/
export type VerificationCredentials =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go for Credentials here!

});
});

describe('verifyEncryptionKey', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we want to make this private, we can still test those using any methods that uses Credentials as parameter, so that should still be fine, maybe we can rename that to:

Suggested change
describe('verifyEncryptionKey', () => {
describe('#verifyEncryptionKey', () => {

@tanguyenvn tanguyenvn requested a review from ccharly June 5, 2026 13:08
@tanguyenvn
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-previews

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.5.0-preview-834f9ac8f
@metamask-previews/accounts-controller@38.1.2-preview-834f9ac8f
@metamask-previews/address-book-controller@7.1.2-preview-834f9ac8f
@metamask-previews/ai-controllers@0.7.0-preview-834f9ac8f
@metamask-previews/analytics-controller@1.1.0-preview-834f9ac8f
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-834f9ac8f
@metamask-previews/announcement-controller@8.1.0-preview-834f9ac8f
@metamask-previews/app-metadata-controller@2.0.1-preview-834f9ac8f
@metamask-previews/approval-controller@9.0.1-preview-834f9ac8f
@metamask-previews/assets-controller@8.3.1-preview-834f9ac8f
@metamask-previews/assets-controllers@108.4.0-preview-834f9ac8f
@metamask-previews/authenticated-user-storage@2.0.0-preview-834f9ac8f
@metamask-previews/base-controller@9.1.0-preview-834f9ac8f
@metamask-previews/base-data-service@0.1.3-preview-834f9ac8f
@metamask-previews/bridge-controller@73.2.0-preview-834f9ac8f
@metamask-previews/bridge-status-controller@72.0.1-preview-834f9ac8f
@metamask-previews/build-utils@3.0.4-preview-834f9ac8f
@metamask-previews/chain-agnostic-permission@1.6.1-preview-834f9ac8f
@metamask-previews/chomp-api-service@3.1.0-preview-834f9ac8f
@metamask-previews/claims-controller@0.5.2-preview-834f9ac8f
@metamask-previews/client-controller@1.0.1-preview-834f9ac8f
@metamask-previews/compliance-controller@2.1.0-preview-834f9ac8f
@metamask-previews/composable-controller@12.0.1-preview-834f9ac8f
@metamask-previews/config-registry-controller@0.4.0-preview-834f9ac8f
@metamask-previews/connectivity-controller@0.2.0-preview-834f9ac8f
@metamask-previews/controller-utils@12.1.0-preview-834f9ac8f
@metamask-previews/core-backend@6.3.1-preview-834f9ac8f
@metamask-previews/delegation-controller@3.0.1-preview-834f9ac8f
@metamask-previews/earn-controller@12.2.0-preview-834f9ac8f
@metamask-previews/eip-5792-middleware@3.0.4-preview-834f9ac8f
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-834f9ac8f
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-834f9ac8f
@metamask-previews/ens-controller@19.1.3-preview-834f9ac8f
@metamask-previews/eth-block-tracker@15.0.1-preview-834f9ac8f
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-834f9ac8f
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-834f9ac8f
@metamask-previews/foundryup@1.0.1-preview-834f9ac8f
@metamask-previews/gas-fee-controller@26.2.2-preview-834f9ac8f
@metamask-previews/gator-permissions-controller@4.2.0-preview-834f9ac8f
@metamask-previews/geolocation-controller@0.1.3-preview-834f9ac8f
@metamask-previews/json-rpc-engine@10.5.0-preview-834f9ac8f
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-834f9ac8f
@metamask-previews/keyring-controller@26.0.0-preview-834f9ac8f
@metamask-previews/logging-controller@8.0.2-preview-834f9ac8f
@metamask-previews/message-manager@14.1.2-preview-834f9ac8f
@metamask-previews/messenger@1.2.0-preview-834f9ac8f
@metamask-previews/messenger-cli@0.2.0-preview-834f9ac8f
@metamask-previews/money-account-balance-service@1.0.2-preview-834f9ac8f
@metamask-previews/money-account-controller@0.3.1-preview-834f9ac8f
@metamask-previews/money-account-upgrade-controller@2.0.3-preview-834f9ac8f
@metamask-previews/multichain-account-service@10.0.1-preview-834f9ac8f
@metamask-previews/multichain-api-middleware@3.1.2-preview-834f9ac8f
@metamask-previews/multichain-network-controller@3.1.2-preview-834f9ac8f
@metamask-previews/multichain-transactions-controller@7.1.0-preview-834f9ac8f
@metamask-previews/name-controller@9.1.2-preview-834f9ac8f
@metamask-previews/network-controller@32.0.0-preview-834f9ac8f
@metamask-previews/network-enablement-controller@5.2.0-preview-834f9ac8f
@metamask-previews/notification-services-controller@24.1.2-preview-834f9ac8f
@metamask-previews/passkey-controller@2.0.1-preview-834f9ac8f
@metamask-previews/permission-controller@13.1.1-preview-834f9ac8f
@metamask-previews/permission-log-controller@5.1.0-preview-834f9ac8f
@metamask-previews/perps-controller@7.0.0-preview-834f9ac8f
@metamask-previews/phishing-controller@17.2.0-preview-834f9ac8f
@metamask-previews/polling-controller@16.0.6-preview-834f9ac8f
@metamask-previews/preferences-controller@23.1.0-preview-834f9ac8f
@metamask-previews/profile-metrics-controller@3.1.5-preview-834f9ac8f
@metamask-previews/profile-sync-controller@28.1.1-preview-834f9ac8f
@metamask-previews/ramps-controller@14.1.1-preview-834f9ac8f
@metamask-previews/rate-limit-controller@7.0.1-preview-834f9ac8f
@metamask-previews/react-data-query@0.2.1-preview-834f9ac8f
@metamask-previews/remote-feature-flag-controller@4.2.2-preview-834f9ac8f
@metamask-previews/sample-controllers@5.0.1-preview-834f9ac8f
@metamask-previews/seedless-onboarding-controller@10.0.0-preview-834f9ac8f
@metamask-previews/selected-network-controller@26.1.3-preview-834f9ac8f
@metamask-previews/shield-controller@5.1.2-preview-834f9ac8f
@metamask-previews/signature-controller@39.2.3-preview-834f9ac8f
@metamask-previews/snap-account-service@0.2.1-preview-834f9ac8f
@metamask-previews/social-controllers@2.2.1-preview-834f9ac8f
@metamask-previews/storage-service@1.0.1-preview-834f9ac8f
@metamask-previews/subscription-controller@6.1.3-preview-834f9ac8f
@metamask-previews/transaction-controller@66.0.0-preview-834f9ac8f
@metamask-previews/transaction-pay-controller@23.1.0-preview-834f9ac8f
@metamask-previews/user-operation-controller@41.2.3-preview-834f9ac8f
@metamask-previews/wallet@2.0.0-preview-834f9ac8f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants