Repository navigation
fix(background): treat a wiped hardware wallet session as locked - #3090
Conversation
When a hardware wallet account is active, buildHasPrivateKeySelector returned true whenever isHardwareWalletLocked was false. The browser wipes storage.session on restart and on extension reload/update. The rehydrated session then has no public key, and isHardwareWalletLocked falls back to false, so the empty session read as unlocked. The popup saved hasPrivateKey: true, saw no public key, and routed to the unlock screen. The unlock screen only navigates on a false -> true change of hasPrivateKey, so a correct password did nothing and showed no error. Require a session public key before a hardware wallet session counts as unlocked. Add selector tests for the wiped and idle-locked cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8cp428MzVhQRRFYoqTVpA
There was a problem hiding this comment.
🔵 Needs a closer look
Because the change alters Ledger background unlock and session hydration, automated selector tests are insufficient while real-device Chrome and Firefox reload/restart validation remains outstanding.
No open issues 🎉
What changed in this PR
This PR fixes a hardware wallet unlock bug. After the browser wipes storage.session (on restart, reload, or update), the background rebuilds an empty session. The session selector then reports the wallet as unlocked, so the popup never leaves the unlock screen after a correct password. The fix requires a publicKey in the session before a hardware wallet counts as unlocked. Hot wallet (mnemonic) behavior is unchanged.
Changes:
buildHasPrivateKeySelectornow returnstruefor a hardware wallet only when the session is not idle-locked and has apublicKey.- Tests now set a
publicKeyon the unlocked case, and add cases for an idle-locked session and a wiped session with no public key.
| File | Description |
|---|---|
| extension/src/background/helpers/__tests__/session.test.ts | Adds a public key to the unlocked test and adds tests for idle-locked and wiped hardware-wallet sessions. |
| extension/src/background/ducks/session.ts | Adds a publicKey check to the hardware wallet unlock rule in buildHasPrivateKeySelector, with a comment that explains the wiped-session case. |
Review effort: Lite
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-58bfbe213c96efbf5ad8 |
There was a problem hiding this comment.
this lgtm, a couple of things we could add to harden these flows:
- a test that covers this flow: wiped session → loadAccount returns false → confirmPassword succeeds with the hardware wallet key
userActivitycould call thebuildHasPrivateKeySelectorselector the same waysaveSettingsdoes in order to avoid drift between the two.
Run the real loadAccount and confirmPassword handlers against a fresh session store with a hardware wallet active. Check that loadAccount reports the wiped session as locked, that the correct password unlocks it with the hardware wallet public key, and that a wrong password leaves it locked. Both cases fail without the selector fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8cp428MzVhQRRFYoqTVpA
userActivity had its own copy of the unlock check. Use the shared selector, as saveSettings does, so the idle alarm and loadAccount cannot drift apart. Behavior changes: - A wiped hardware wallet session (no public key) no longer rearms the idle alarm. - A hot wallet session with a hash key but an empty temporary store no longer rearms the idle alarm. loadAccount already reports it locked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R8cp428MzVhQRRFYoqTVpA
Brings in #3085 (Ledger option visible in light mode) and #3090 (a wiped hardware wallet session is treated as locked). Conflict: - en/pt translation.json: master added "{{walletType}} logo" next to the Earn keys. Merged key by key; both sides' keys are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
A hardware wallet session is now "unlocked" only when the session has a public key.
The change is in
buildHasPrivateKeySelector(extension/src/background/ducks/session.ts). The old rule was:isHardwareWalletLockedisfalse.The new rule adds one condition:
publicKey.The rule for hot wallet (mnemonic) sessions does not change.
userActivitynow usesbuildHasPrivateKeySelector, assaveSettingsdoes. Before, it had its own copy of the unlock check.Why
Users with an active Ledger account can be stuck on the unlock screen. They type the correct password and click Unlock. A loading animation shows. Then nothing happens. No error shows. A wrong password shows the error correctly.
The sequence is:
storage.session. This occurs on a browser restart and on an extension reload or update.publicKeyis"".isHardwareWalletLockedisfalse.hasPrivateKey: true.hasPrivateKey: truein Redux. It sees no public key, so it opens the unlock screen.hasPrivateKeychanges fromfalsetotrue. The value is alreadytruewhen the screen mounts.confirmPasswordsucceeds.hasPrivateKeystaystrue. The screen does not navigate.This occurs each time the popup opens. The user cannot get past the unlock screen.
With this fix, step 3 returns
false. The unlock screen mounts withhasPrivateKey: false. A successful unlock sets the public key (loginToAllAccountsdispatcheslogInwith the hardware wallet public key).hasPrivateKeychanges totrue, and the screen navigates.Other effects:
loadAccount,confirmPassword,importHardwareWallet,saveSettingsanduserActivityuse this selector.importHardwareWalletsets the public key before it reads the selector, so import is not affected.userActivityrearms the idle alarm only when the selector says the wallet is unlocked. This gives two behavior changes:loadAccountalready reports this session as locked.Tests
Selector tests (
extension/src/background/helpers/__tests__/session.test.ts):publicKey.Flow test (
extension/src/background/messageListener/__tests__/hardwareWalletWipedSessionUnlock.test.ts):This test uses the real session Redux store, the real
loadAccountandconfirmPasswordhandlers, and the realloginToAllAccounts. It mocks only storage, the key manager, store persistence and the session broadcast. The wallet has one hot account and one Ledger account. The Ledger account is active. The session store starts empty, the same as after a wipe.loadAccountreturnshasPrivateKey: falseand an emptypublicKey.confirmPasswordsucceeds. It returns the Ledger public key andhasPrivateKey: true.loadAccountreturns the Ledger public key andhasPrivateKey: true.confirmPasswordreturnsIncorrect password.loadAccountstill returnshasPrivateKey: false.Both flow cases fail without the fix.
loadAccountreturnshasPrivateKey: truefor the wiped session.userActivitytests (extension/src/background/messageListener/__tests__/userActivity.test.ts):publicKey.yarn test:cipasses, excepttestNodeCompat.js. That suite needs a built@stellar/varial-sdkpackage and is not related to this change.Not done yet: a manual test with a real Ledger. Steps to reproduce:
chrome://extensions.Before the fix: the unlock screen stays. After the fix: the account view opens.
Screenshots / video
N/A. There is no UI change. The fix changes when the unlock screen navigates.
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01R8cp428MzVhQRRFYoqTVpA