Skip to content

feat: mfa recovery controller - #10022

Draft
ieow wants to merge 2 commits into
mainfrom
cw/mfa-recovery-controller
Draft

feat: mfa recovery controller#10022
ieow wants to merge 2 commits into
mainfrom
cw/mfa-recovery-controller

Conversation

@ieow

@ieow ieow commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Explanation

Current state

MFA recovery needs to keep a recovery secret consistent across multiple escrow
replicas. A mutation can succeed at one escrow and fail at another, so the
controller needs durable progress tracking, request binding, and a safe way to
resume incomplete writes.

The original implementation also combined mutation orchestration, pending-state
validation, receipt handling, and read consistency logic in one large
controller file.

Solution

This PR introduces @metamask/mfa-recovery-controller, which coordinates
profile authentication, identifier authentication, recovery-secret reads, and
replication across build-configured escrow providers.

The mutation protocol now:

  • Creates one request-bound mutation containing the profile, operation,
    expected/new versions, payload hash, and escrow audiences.
  • Persists encrypted authorizing state before authorization.
  • Persists encrypted writing state with the auth token and acknowledged
    escrow receipts before starting writes.
  • Applies the same mutation to every configured escrow and validates each
    receipt against its expected escrow.
  • Persists successful receipts even when another escrow fails.
  • Allows resume() to retry only missing acknowledgements.
  • Requires escrow-side idempotency by mutation id so a crash after a write but
    before receipt persistence is safe to retry.
  • Clears pending state only after every configured escrow is acknowledged.

The controller now rejects malformed persisted operations before authorization
or escrow calls. Fully acknowledged pending mutations can be cleared without
checking availability of already-acknowledged escrows.

The large controller has been split into focused internal modules:

  • pending-operation-validation.ts handles runtime validation of decrypted
    pending state.
  • escrow-utils.ts handles receipt validation, settled-result narrowing, and
    highest-consistent-version selection.

The public exports remain unchanged by this refactor. The
RecoveryEscrowProvider.verifyReceipt contract intentionally requires the
expected escrow id, which is a breaking provider-interface change documented
in the changelog.

Security and trust model

Mutation authorization uses one request-bound AuthControllerToken for the
configured escrow audience set. Each escrow implementation must independently
verify the token signature, issuer, expiry, profile, exact mutation hash, and
that its own id is included in the mutation audiences.

Key-bound identifier authentication creates a separate challenge and proof for
each escrow. Escrow-challenge identifiers also complete a separate challenge
for each escrow.

The current design treats configured escrows as trusted equivalent replicas:
the recovery secret is replicated in full to each escrow. This is not an
M-of-N threshold-recovery design. A future independent-trust model would need
escrow-specific authorization scopes and escrow-specific or threshold-encrypted
secret material.

References

Related work is contained in this pull request.

Validation

  • yarn workspace @metamask/mfa-recovery-controller run test
    • 57 tests passing
    • 100% statements, branches, functions, and lines
  • Focused ESLint passes for the changed controller, utility, type, test, and
    stub files
  • yarn workspace @metamask/mfa-recovery-controller run build
  • yarn workspace @metamask/mfa-recovery-controller run changelog:validate
  • git diff --check

Checklist

  • Updated the test suite for the updated code
  • Updated documentation and JSDoc
  • Updated the package changelog
  • Prepared consumer follow-up pull requests for the breaking
    verifyReceipt signature change

ieow added 2 commits August 31, 2026 14:31
- Introduced the `@metamask/mfa-recovery-controller` package to manage MFA recovery flows across MetaMask clients.
- Added core functionalities including methods for registering recovery secrets, updating identifiers, and handling recovery states.
- Updated relevant documentation and code ownership files to include the new package.
- Ensured integration with existing MetaMask infrastructure by updating configuration files and dependencies.

This commit lays the groundwork for enhanced MFA recovery capabilities in MetaMask, improving user experience and security.
- Introduced a new `escrow-utils.ts` module for handling escrow-related operations, including receipt verification and selecting the highest consistent recovery secret version.
- Updated `MfaRecoveryController` to require `RecoveryEscrowProvider.verifyReceipt` to accept an expected escrow ID, enhancing receipt verification.
- Exposed state updates through the `MfaRecoveryController:stateChanged` event, allowing better tracking of state changes.
- Improved documentation in `CHANGELOG.md` and `README.md` to reflect changes in state management and mutation handling.
- Fixed issues related to clearing acknowledged mutations and validating persisted mutation states.

This commit enhances the robustness of the MFA recovery process and improves the overall user experience in MetaMask.
@ieow ieow changed the title Cw/mfa recovery controller feat: mfa recovery controller Aug 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0b4047c. Configure here.

...(await escrow.getSecret(authorizations[index], requestId)),
})),
);
return selectHighestConsistentVersion(results).recoverySecret;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Read auth fails closed

Medium Severity

getRecoverySecret tolerates individual getSecret failures, but #authorizeIdentifier uses Promise.all for challenges. If any escrow that reported available then fails challenge creation, the whole read fails even when another replica could return the secret.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0b4047c. Configure here.

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.

1 participant