Skip to content

fix(passphrase): clear device session on passphrase toggle so addresses re-derive#234

Merged
BitHighlander merged 1 commit into
developfrom
fix/passphrase-toggle-stale-seed
Jun 9, 2026
Merged

fix(passphrase): clear device session on passphrase toggle so addresses re-derive#234
BitHighlander merged 1 commit into
developfrom
fix/passphrase-toggle-stale-seed

Conversation

@BitHighlander

Copy link
Copy Markdown
Collaborator

Problem

Users reported that after enabling a BIP-39 passphrase from Device Settings and entering it, the wallet kept showing the standard (no-passphrase) wallet's addresses — the entire portfolio looked stale/old. Critically, pressing "View on device" showed the wrong address on the device screen too, proving the device itself was deriving from the wrong seed (not a UI cache issue). Only physically reconnecting the KeepKey fixed it.

Root cause (firmware seed cache)

In keepkey-firmware storage.c:

  • Address derivation (GetPublicKey / GetAddress, incl. "view on device") goes through storage_getRootNode()storage_getSeed(), which caches the derived seed in session.seed (seedCached = true). During a standard session this caches the empty-passphrase seed.
  • ApplySettings(use_passphrase=true) calls storage_setPassphraseProtected(), which only flips the passphrase_protection flag — it does not clear session.seedCached.
  • So after enabling + entering a passphrase, passphrase_protect() fires and caches the new passphrase, but the next check if (!session.seedCached) is false, so the device reuses the stale empty-passphrase seed and never re-derives.

A USB unplug power-cycles the device, clearing seedCached — which is why reconnecting "fixed" it.

Fix

engine.applySettings() now sends ClearSession after toggling usePassphrase. ClearSession runs the firmware's session_clear(true), which zeroes seedCached, passphraseCached, and the seed buffer — a power-cycle equivalent over USB, no physical reconnect or app restart required.

The device then routes back through needs_pin → needs_passphrase, which:

  • re-prompts for PIN + passphrase and re-derives from the correct seed (on-device address is now correct),
  • resets the in-memory BTC/EVM account managers (existing needs_passphrase handling),
  • remounts the dashboard for a fresh balance fetch — so the stale UI clears too.

DeviceSettingsDrawer: since both enable and disable now clear the session and re-auth, neither path calls getFeatures inline anymore (it would race promptPin()'s getPublicKeys() on the transport lock and hang). The drawer re-fetches features the next time it opens.

Emulator is skipped (a ClearSession right after ApplySettings can leave a stale ButtonAck in the ring buffer; emulators reconnect for clean state).

Test plan

On a real KeepKey with a PIN set:

  1. Connect, standard wallet, note the ETH/BTC receive address (and "view on device").
  2. Settings → enable BIP-39 passphrase → device prompts for PIN, then passphrase → enter a non-empty passphrase.
  3. ✅ Dashboard now shows the hidden wallet's addresses; "View on device" shows the matching hidden address — without unplugging.
  4. Settings → disable passphrase → re-enter PIN → ✅ back to the standard wallet's addresses.
  5. Repeat enabling with an empty passphrase → ✅ standard wallet (unchanged), not blocked as hidden.

Notes

  • The underlying firmware behavior (cache not invalidated on setPassphraseProtected) is the true bug; this is the host-side fix that works on already-shipped firmware. A firmware-side session_clear in storage_setPassphraseProtected() would be a good follow-up.

…-derives

Enabling/disabling BIP-39 passphrase protection from Device Settings left
every wallet address — including 'view on device' — showing the previous
(standard) wallet until a physical reconnect.

Root cause is in firmware: storage_setPassphraseProtected() only flips the
passphrase_protection flag and does NOT invalidate session.seedCached. The
seed derived earlier in the session (empty passphrase) stays cached, so
storage_getRootNode() keeps returning it even after the user enters a new
passphrase. A USB unplug power-cycles the device and clears the cache, which
is why reconnecting 'fixed' it.

Fix without requiring a reconnect: engine.applySettings() now sends
ClearSession after toggling usePassphrase, dropping the cached seed +
passphrase + PIN so the device re-prompts and re-derives from the correct
seed. The device routes back through needs_pin -> needs_passphrase, which
also resets the in-memory account managers and remounts the dashboard, so
stale addresses/balances in the UI clear too. Skipped on the emulator.

DeviceSettingsDrawer: both enable and disable now re-auth, so neither path
calls getFeatures inline (would race promptPin's getPublicKeys); the drawer
re-fetches features when it next opens.
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