fix: leaving the private-key export screen leaves the private key in the DOM for the life of the popup #221
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
export-privkeyregisters no view-leave cleanup. Leaving it by any route other than its own Back button — the settings gear, for instance — leaves the decrypted private key sitting in#export-privkey-valueinside the hidden view for the rest of the popup's life.Shipped behaviour, not introduced by anything in flight. Found by the independent review of #215, which hit the same bug class on the new recovery-phrase screen.
A private key left in a hidden DOM node is recoverable by anything with script access to the popup document, and it outlives the moment the user thought they had dismissed it. The screen's implied contract is that leaving disposes of the secret.
Why it is now cheap
#161 adds an
onViewLeave()hook tosrc/popup/views/helpers.js, run byshowView()on ANY exit rather than only on Back. Once that lands this is a two-line fix: register aclear()forexport-privkeythe same way.Depends on #215 landing first, since it introduces the hook.
Implementation requirements
export-privkeythat wipes the key from the DOM and from any closure holding it.Definition of done
export-privkeyby Back, by the settings gear, and by any other navigation clears the key from the DOM.export-privkeyremains absent fromRESTORABLE_VIEWS.TODO.mdupdated in the same commit.make checkpasses.Plan:
src/popup/views/addressDetail.jsinto its ownsrc/popup/views/exportPrivkey.js, modelled onsrc/popup/views/showPhrase.js:onViewLeave("export-privkey", clear), aclear()that wipes the value node, the password input and the closure state, and arevealGenerationliveness guard. Extraction is what makes the load-bearing test possible —addressDetail.jscannot be required outside a browser,showPhrase.js-shaped module can.decryptWithPassword, then writessigner.privateKeyinto#export-privkey-valuewith no check that the screen is still live. Guard goes in, key derivation moves behind the guard so nothing is even derived once the screen is gone.add-wallet(a generated recovery phrase, an imported private key or xprv, and the password, all cleared on entry only),confirm-tx,delete-wallet-confirm,approve-tx,approve-sign(password inputs, cleared on entry only, left populated after the screen navigates on). Each gets anonViewLeaveclear in the same change; none of them has a post-await write, so none needs a generation guard.tests/exportPrivkey.test.jsdriving the module against a small DOM stub — leave during the decrypt asserts the key never lands in#export-privkey-value, plus leave-by-gear and leave-by-Back after a completed reveal, plus an assertion thatexport-privkeyis absent fromRESTORABLE_VIEWS.README.mdview-navigation section andTODO.mdupdated in the same commit.Built in #248 (branch
fix/issue-221-privkey-dom-wipe, basenext).The post-await hole was present. The screen moved into
src/popup/views/exportPrivkey.jswith anonViewLeave()cleanup and therevealGenerationliveness guard, the guard placed in front of the key derivation so an abandoned decrypt does not even derive the key. The audit covered five more screens holding secret material in the DOM with entry-only clearing —add-wallet(generated or pasted recovery phrase, private key, extended private key, password),confirm-tx,delete-wallet-confirm,approve-tx,approve-sign— all now registering the same cleanup; none of them writes a secret after an await, so none needed a guard.Verified:
make checkgreen on the rebased branch (15 suites, 374 tests, prettier clean) andscript/cibuildexit 0 with themake checkandmake buildlayers executing rather than cached. The load-bearing test was confirmed load-bearing by removing the guard and re-runningmake check: 2 failures, both intests/exportPrivkey.test.js— the leave-during-decrypt case and the harder re-entry variant that only the generation counter catches.