Skip to content

Feat/pwd change failure recovery - #10148

Open
lwin-kyaw wants to merge 13 commits into
mainfrom
feat/pwd-change-failure-recovery
Open

Feat/pwd change failure recovery#10148
lwin-kyaw wants to merge 13 commits into
mainfrom
feat/pwd-change-failure-recovery

Conversation

@lwin-kyaw

@lwin-kyaw lwin-kyaw commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Explanation

Seedless password changes span multiple independently persisted states (remote Seedless/TOPRF, local Seedless vault, local KeyringController vault, the stored Keyring encryption key, and a lifecycle marker). These cannot be committed atomically, so a crash, lost response, or partial local update could previously leave the wallet in an ambiguous state where neither the old nor the new password reliably unlocks, and the client had no way to tell that recovery was needed.

This PR adds a server-first password-change recovery model to SeedlessOnboardingController:

  • A persisted, non-sensitive passwordChangePhase field (SeedlessPasswordChangePhase: SEEDLESS_CHANGE_PENDINGSEEDLESS_COMMITTEDLOCAL_KEYRING_PENDINGKEY_SYNC_PENDING, plus UNKNOWN) acts as a recovery signal. An unset/undefined phase means no change is in progress. The phase is not proof of remote or local state — recovery always re-verifies actual state before acting.
  • changePassword is now lifecycle-aware: it writes each phase at the irreversible boundaries, preserves the last known phase on error, and rejects a second concurrent change with PasswordChangeInProgress.
  • Two new public methods own the Seedless-side recovery sequencing (the controller owns the Seedless side; the client owns the Keyring side):
    • resolvePasswordSyncState({ skipCache }) — password-less, called at unlock (render + submit). Replaces the public checkIsPasswordOutdated read and returns a PasswordChangeRecoveryStatus that tells the client which recovery step to run next.
    • reconcilePassword({ globalPassword }) — password-consuming. Internally runs password-chain unlock and local vault rewrite, re-encrypts encryptedKeyringEncryptionKey under the new wrapping key so loadKeyringEncryptionKey keeps working, and advances to LOCAL_KEYRING_PENDING. Used both for an interrupted local password change and for a password change made on another device.
  • Lifecycle-advance methods: markPasswordChangeKeySyncPending after the Keyring encryption key is stored, and clearPasswordChangePhase once key synchronization and local persistence are verified. clearPasswordChangePhase is the only way back to "no change in progress".
  • A lost/ambiguous changeEncKey response is classified via fetchAuthPubKey comparison into old / new / unknown; ambiguous results stay UNKNOWN and keep the wallet locked.

Breaking changes vs main:

  • Remove checkIsPasswordOutdated and SeedlessOnboardingControllerCheckIsPasswordOutdatedAction. Call resolvePasswordSyncState({ skipCache }) instead (true maps to PasswordChangeRecoveryStatus.PasswordOutdated).
  • Remove submitGlobalPassword, syncLatestGlobalPassword, and their messenger actions. Call reconcilePassword({ globalPassword }) instead.
  • changePassword now writes lifecycle phases and rejects a concurrent change with PasswordChangeInProgress. Clients must not start a second password change while a lifecycle is unfinished, and must drive it to completion with clearPasswordChangePhase.

There is no awaitable durability hook on the controller for lifecycle writes — the phase is persisted as ordinary debounced controller state, so recovery re-verifies actual state (a stale/missing marker is recoverable via the outdated check + cryptographic Keyring verification). The controller does not call KeyringController (AllowedActions = never); clients own the Keyring-side steps and wallet locking.

Full design, recovery flow, and a step-by-step client integration guide are in docs/0002-password-change-recovery-flow.md. The ADR is in docs/0001-seedless-password-change-recovery.md.

Test plan

  • yarn workspace @metamask/seedless-onboarding-controller run test
  • Confirm changePassword writes SEEDLESS_CHANGE_PENDINGSEEDLESS_COMMITTEDLOCAL_KEYRING_PENDING and rejects a second concurrent change with PasswordChangeInProgress
  • Interrupted local change: resolvePasswordSyncStatereconcilePassword → Keyring old/new branch → storeKeyringEncryptionKey / markPasswordChangeKeySyncPendingclearPasswordChangePhase
  • Another-device outdated password: resolvePasswordSyncState returns password-outdated, reconcilePassword returns reconcile-keyring, and loadKeyringEncryptionKey still decrypts after vault rewrite
  • Ambiguous remote changeEncKey result stays UNKNOWN and does not infer success from a rejected Promise
  • Client unlock path no longer calls checkIsPasswordOutdated, submitGlobalPassword, or syncLatestGlobalPassword

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

High Risk
Breaking changes to password/unlock APIs and lifecycle-gated changePassword touch vault crypto and multi-device sync; incorrect client migration could lock users out or leave ambiguous wallet state.

Overview
Introduces a server-first password-change recovery model for Seedless: a persisted passwordChangePhase lifecycle (non-secret recovery signal) and PasswordSyncStatus so clients can route unlock/recovery without guessing wallet state after partial failures or another-device password changes.

Breaking API: removes public checkIsPasswordOutdated, submitGlobalPassword, and syncLatestGlobalPassword (and their messenger actions). Clients should call resolvePasswordSyncState({ skipCache }) at unlock (replaces the outdated boolean; password-outdated ≈ old true) and reconcilePassword({ globalPassword }) to run chain-unlock + local vault rewrite internally, re-wrap encryptedKeyringEncryptionKey, and advance to LOCAL_KEYRING_PENDING. Adds clearPasswordChangePhase, markPasswordChangeKeySyncPending, and PasswordChangeInProgress when changePassword runs while a lifecycle is unfinished.

changePassword now writes phases at irreversible boundaries, preserves the last phase on error, and documents that Keyring reconciliation, locking, and final clearPasswordChangePhase remain client-owned (Option A). Adds ADR/integration docs (0001–0003) and changelog entries for consumers.

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

@lwin-kyaw
lwin-kyaw requested review from a team as code owners September 9, 2026 12:38
@lwin-kyaw
lwin-kyaw deployed to default-branch September 9, 2026 12:38 — with GitHub Actions Active

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

Stale Bugbot comment from a previous run.

Comment thread packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts Outdated
@lwin-kyaw

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-09-09T16:51:26.722442Z abca9bc Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref abca9bceacbc27108e1b5714e53b3fcbeb4e2cf1 does not exist
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@lwin-kyaw

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

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@8.1.0-preview-abca9bcea
@metamask-previews/accounts-controller@39.1.1-preview-abca9bcea
@metamask-previews/address-book-controller@7.1.2-preview-abca9bcea
@metamask-previews/ai-controllers@1.0.0-preview-abca9bcea
@metamask-previews/analytics-controller@2.1.0-preview-abca9bcea
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-abca9bcea
@metamask-previews/announcement-controller@8.1.0-preview-abca9bcea
@metamask-previews/app-metadata-controller@2.0.1-preview-abca9bcea
@metamask-previews/approval-controller@9.0.2-preview-abca9bcea
@metamask-previews/assets-controller@15.0.0-preview-abca9bcea
@metamask-previews/assets-controllers@111.1.3-preview-abca9bcea
@metamask-previews/authenticated-user-storage@3.0.2-preview-abca9bcea
@metamask-previews/base-controller@9.1.0-preview-abca9bcea
@metamask-previews/base-data-service@1.0.0-preview-abca9bcea
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-abca9bcea
@metamask-previews/bridge-controller@80.2.0-preview-abca9bcea
@metamask-previews/bridge-status-controller@75.5.0-preview-abca9bcea
@metamask-previews/build-utils@3.0.4-preview-abca9bcea
@metamask-previews/chain-agnostic-permission@1.7.0-preview-abca9bcea
@metamask-previews/chomp-api-service@4.0.2-preview-abca9bcea
@metamask-previews/claims-controller@0.6.1-preview-abca9bcea
@metamask-previews/client-controller@1.0.1-preview-abca9bcea
@metamask-previews/client-utils@2.1.1-preview-abca9bcea
@metamask-previews/compliance-controller@2.1.0-preview-abca9bcea
@metamask-previews/composable-controller@12.0.1-preview-abca9bcea
@metamask-previews/config-registry-controller@3.1.0-preview-abca9bcea
@metamask-previews/connectivity-controller@0.3.0-preview-abca9bcea
@metamask-previews/controller-utils@12.3.0-preview-abca9bcea
@metamask-previews/core-backend@9.0.0-preview-abca9bcea
@metamask-previews/cryptography@0.0.0-preview-abca9bcea
@metamask-previews/delegation-controller@3.0.2-preview-abca9bcea
@metamask-previews/earn-controller@12.2.6-preview-abca9bcea
@metamask-previews/eip-5792-middleware@3.0.5-preview-abca9bcea
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-abca9bcea
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-abca9bcea
@metamask-previews/eth-block-tracker@15.0.1-preview-abca9bcea
@metamask-previews/eth-json-rpc-middleware@24.0.2-preview-abca9bcea
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-abca9bcea
@metamask-previews/foundryup@1.0.1-preview-abca9bcea
@metamask-previews/gas-fee-controller@26.3.2-preview-abca9bcea
@metamask-previews/gator-permissions-controller@5.0.2-preview-abca9bcea
@metamask-previews/geolocation-controller@1.0.0-preview-abca9bcea
@metamask-previews/java-tron-up@1.0.0-preview-abca9bcea
@metamask-previews/json-rpc-engine@10.5.0-preview-abca9bcea
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-abca9bcea
@metamask-previews/keyring-controller@27.1.1-preview-abca9bcea
@metamask-previews/kyc-controller@0.0.0-preview-abca9bcea
@metamask-previews/local-node-utils@1.0.0-preview-abca9bcea
@metamask-previews/logging-controller@9.0.0-preview-abca9bcea
@metamask-previews/message-manager@14.1.2-preview-abca9bcea
@metamask-previews/messenger@2.0.0-preview-abca9bcea
@metamask-previews/messenger-cli@0.2.0-preview-abca9bcea
@metamask-previews/money-account-api-data-service@0.4.1-preview-abca9bcea
@metamask-previews/money-account-balance-service@2.4.3-preview-abca9bcea
@metamask-previews/money-account-controller@1.0.0-preview-abca9bcea
@metamask-previews/money-account-upgrade-controller@4.0.0-preview-abca9bcea
@metamask-previews/money-account-utils@1.2.0-preview-abca9bcea
@metamask-previews/multichain-account-service@13.0.2-preview-abca9bcea
@metamask-previews/multichain-api-middleware@4.0.3-preview-abca9bcea
@metamask-previews/multichain-network-controller@3.2.4-preview-abca9bcea
@metamask-previews/multichain-transactions-controller@7.1.2-preview-abca9bcea
@metamask-previews/name-controller@9.1.2-preview-abca9bcea
@metamask-previews/network-connection-banner-controller@0.2.1-preview-abca9bcea
@metamask-previews/network-controller@36.0.0-preview-abca9bcea
@metamask-previews/network-enablement-controller@6.0.5-preview-abca9bcea
@metamask-previews/notification-services-controller@27.0.1-preview-abca9bcea
@metamask-previews/passkey-controller@3.1.0-preview-abca9bcea
@metamask-previews/permission-controller@13.1.1-preview-abca9bcea
@metamask-previews/permission-log-controller@5.1.0-preview-abca9bcea
@metamask-previews/perps-controller@16.1.0-preview-abca9bcea
@metamask-previews/phishing-controller@17.4.1-preview-abca9bcea
@metamask-previews/platform-api-docs@0.1.0-preview-abca9bcea
@metamask-previews/polling-controller@16.0.9-preview-abca9bcea
@metamask-previews/preferences-controller@23.1.0-preview-abca9bcea
@metamask-previews/profile-metrics-controller@4.0.3-preview-abca9bcea
@metamask-previews/profile-sync-controller@29.0.0-preview-abca9bcea
@metamask-previews/ramps-controller@20.2.0-preview-abca9bcea
@metamask-previews/rate-limit-controller@7.0.1-preview-abca9bcea
@metamask-previews/react-data-query@1.0.0-preview-abca9bcea
@metamask-previews/remote-feature-flag-controller@6.1.1-preview-abca9bcea
@metamask-previews/sample-controllers@5.0.6-preview-abca9bcea
@metamask-previews/seedless-onboarding-controller@10.1.1-preview-abca9bcea
@metamask-previews/selected-network-controller@26.1.7-preview-abca9bcea
@metamask-previews/sentinel-api-service@1.0.1-preview-abca9bcea
@metamask-previews/shield-controller@6.0.1-preview-abca9bcea
@metamask-previews/signature-controller@39.2.10-preview-abca9bcea
@metamask-previews/smart-transactions-controller@26.0.0-preview-abca9bcea
@metamask-previews/snap-account-service@2.1.2-preview-abca9bcea
@metamask-previews/social-controllers@2.8.0-preview-abca9bcea
@metamask-previews/solana-test-validator-up@1.0.0-preview-abca9bcea
@metamask-previews/stellar-quickstart-up@0.0.0-preview-abca9bcea
@metamask-previews/storage-service@1.0.2-preview-abca9bcea
@metamask-previews/subscription-controller@8.0.1-preview-abca9bcea
@metamask-previews/transaction-controller@69.8.1-preview-abca9bcea
@metamask-previews/transaction-pay-controller@27.1.1-preview-abca9bcea
@metamask-previews/user-operation-controller@41.2.9-preview-abca9bcea
@metamask-previews/wallet@12.0.2-preview-abca9bcea
@metamask-previews/wallet-cli@0.0.0-preview-abca9bcea

@lwin-kyaw

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref abca9bceacbc27108e1b5714e53b3fcbeb4e2cf1 does not exist
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Stale Bugbot comment from a previous run.

Comment thread packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts Outdated

@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 0adf182. Configure here.

// keep the wallet locked.
return PasswordSyncStatus.Unknown;
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale phase snapshot races recovery

High Severity

resolvePasswordSyncState reads passwordChangePhase before taking the controller lock. A concurrent changePassword can finish and advance local Seedless state first, so this method can still follow the SEEDLESS_CHANGE_PENDING branch, clear a later recovery phase, and return in-sync while the Keyring still needs reconciliation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0adf182. 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