Skip to content

Feat/pwd change failure recovery - #10148

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

Feat/pwd change failure recovery#10148
lwin-kyaw wants to merge 9 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
Changes authentication/password sync, vault rewrite, and breaking public APIs; incorrect client migration could block wallet unlock or leave partial password-change state unresolved.

Overview
Adds a server-first password-change recovery model to @metamask/seedless-onboarding-controller so interrupted or cross-device Seedless password changes can be resumed safely instead of leaving the wallet ambiguous.

New persisted recovery signal: optional passwordChangePhase (SeedlessPasswordChangePhase) plus PasswordSyncStatus returned from unlock-time routing. changePassword now advances phases at irreversible steps, keeps the last phase on failure, and throws PasswordChangeInProgress on concurrent changes.

New public API: resolvePasswordSyncState({ skipCache }) (password-less unlock routing), reconcilePassword({ globalPassword }) (internal chain unlock + local vault rewrite + key re-wrap), and lifecycle helpers markPasswordChangeKeySyncPending / clearPasswordChangePhase. Messenger action types are updated accordingly.

Breaking removals: checkIsPasswordOutdated, submitGlobalPassword, and syncLatestGlobalPassword (and their actions)—clients should use the new resolve/reconcile flow. Keyring-side recovery stays in clients (Option A); ADR 0001, integration guide 0002, and future Option B plan 0003 document the contract.

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

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 d53243c. Configure here.

await this.#persistKeyringEncryptionKey(keyringEncryptionKey);
}

this.#resetPasswordOutdatedCache();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Key wrap breaks recovery retry

High Severity

#syncLatestGlobalPasswordInner rewrites the Seedless vault (and persisted authPubKey) before re-wrapping encryptedKeyringEncryptionKey. After that first persist, the wrapping key in the vault no longer matches the stored ciphertext. A crash or failure before the later persist leaves loadKeyringEncryptionKey unable to decrypt, and a later #runPasswordSyncFlow retry hits the same decrypt against the already-new vault, so the documented idempotent SEEDLESS_COMMITTED / LOCAL_KEYRING_PENDING recovery cannot restore the Keyring encryption key.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d53243c. Configure here.

this.#writePasswordChangePhase(
SeedlessPasswordChangePhase.LocalKeyringPending,
);
return PasswordSyncStatus.ReconcileKeyring;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reconcile drops another-device signal

High Severity

reconcilePassword with no phase rewrites the local Seedless vault and authPubKey before writing LOCAL_KEYRING_PENDING. If that phase write is lost, resolvePasswordSyncState sees matching local and remote authPubKey values, reports in-sync, and (for SEEDLESS_CHANGE_PENDING) even clears the phase. The client then skips Keyring recovery even though the local Keyring is still on the old password.

Additional Locations (1)
Fix in Cursor Fix in Web

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