fix: give a wallet whose password is lost a way out, and say the password cannot be reset (closes #312)
All checks were successful
check / check (push) Successful in 31s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 23s

A user who forgot their password but held their recovery phrase was permanently
locked out: deletion was password-gated and re-importing the phrase was refused
as a duplicate. Their only escape was destroying extension storage through
browser internals, taking every other wallet with it.

DeleteWallet gains an "I have lost my password" route that destroys the stored
secret after the wallet's name is typed back. No password gate was added:
requiring one to discard a secret protects nothing, since an attacker who wants
destruction can uninstall the extension, and the only person it stops is the
legitimate user who lost it. The screen is excluded from RESTORABLE_VIEWS and
registers an onViewLeave cleanup.

Deletion was chosen over re-import because a key wallet is duplicate-checked by
address rather than xpub, so an xpub-only relaxation would leave that user
still wedged; because re-import makes the user retype their recovery phrase
into a live popup merely to change a password; and because it reaches no end
state that delete-then-import plus scanForAddresses() does not. The attacker
argument did not decide it — re-import clears the "no worse than the phrase
alone" bar.

All three AddWallet password hints now state the password cannot be recovered
or reset and name that mode's only backup, the xprv mode correctly claiming no
recovery phrase. deleteAddress.js no longer tells the user that deleting a
wallet asks for a password, which this change made false.

The typed confirmation collapses internal whitespace on both sides: a wallet
renamed with two spaces displays with one, so the string a user could see and
type could never match, making the confirmation untypable on the one screen
whose purpose is un-wedging a stuck user.

Measured, not reasoned, after review found the first reserve twice too large
and pushing the Import button below the fold: #btn-add-wallet-confirm bottom
628.13 -> 580.13 at 360x600, scrollHeight 636 -> 600, hint box 48px identical
across all three tabs and on re-entry. make check 40 suites / 828 tests,
test-e2e 55/55, test-e2e-firefox 8/8.
This commit was merged in pull request #334.
This commit is contained in:
2026-08-20 15:12:28 +02:00
parent aea999db85
commit 20e911059a
9 changed files with 853 additions and 52 deletions

View File

@@ -805,7 +805,9 @@ for the views listed in `RESTORABLE_VIEWS` (`src/popup/restorableViews.js`).
Every other screen falls back to Home. The screens that display a secret —
ExportPrivKey and ShowRecoveryPhrase — are deliberately absent from that list,
so the popup can never reopen onto one of them with no password prompt in front
of it.
of it. So are the two that destroy one, DeleteWallet and
DeleteWalletLostPassword: a popup reopened by accident must not land on a screen
whose button erases key material.
A reopened popup renders the wallet list and the one screen it restores onto,
and nothing else, so every screen on the stack behind that one is still the
@@ -828,7 +830,10 @@ exit from that screen rather than only on its "Back" button, so nothing secret
survives in a hidden view once the user has navigated away by any route. That
covers the revealed private key and recovery phrase, the recovery phrase,
private key or extended private key entered on AddWallet, and the password typed
on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign.
on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign. DeleteWalletLostPassword
registers one as well, for the neighbouring reason rather than that one: a
wallet name is not a secret, but a typed confirmation left standing in a hidden
view would leave a wallet one click from deletion.
#### Welcome (`welcome`)
@@ -889,7 +894,13 @@ on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign.
- **From xprv**: instruction text and a masked extended private key
input
- Password + confirm password inputs, with a hint line whose wording depends
on the selected tab
on the selected tab. Every wording says that the password cannot be
recovered or reset and names what the only backup of the wallet is — the
recovery phrase, the private key or the extended private key, according to
the tab. This is the only warning the user gets before the wallet exists;
without it, the lost-password route on DeleteWallet is the first they
would hear of it. The hint line reserves its height, so switching tabs
cannot move the password fields under the pointer.
- "Import" button
- **Transitions**:
- "Import" with a valid entry and a matching password of at least 12
@@ -1244,6 +1255,7 @@ on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign.
- Error line
- Password input
- "Confirm Delete" button
- An underlined "I have lost my password" control
- **Transitions**:
- "Confirm Delete" (correct password, other wallets remain) → deletes the
wallet and its site permissions, then → **Settings** with a "Wallet
@@ -1253,10 +1265,54 @@ on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign.
- Either way, the active address moves only if it belonged to the deleted
wallet, and `AUTISTMASK_ACTIVE_CHANGED` is broadcast when it does
(`src/shared/walletDelete.js`)
- "Confirm Delete" (wrong password) → "Wrong password." on the error line,
nothing deleted
- "Confirm Delete" (wrong password) → "That password is incorrect. Please
try again." on the error line, nothing deleted
- "I have lost my password" → **DeleteWalletLostPassword**
- "Back" → previous screen (Settings)
#### DeleteWalletLostPassword (`delete-wallet-lost-password`)
- **When**: User tapped "I have lost my password" on DeleteWallet.
- **Why it exists**: without it, a user who has forgotten the password but still
holds the recovery phrase has no route back into the product at all. Deletion
was password-gated, and importing the phrase again is refused as a duplicate
xpub by `findWalletByXpub()` while the wallet is still stored, so the only
escape was clearing extension storage through browser internals — which takes
every other wallet with it.
- **Elements**:
- "Back" button, "Delete Wallet Without a Password" heading
- A statement that the password cannot be recovered or reset, so the wallet
cannot be unlocked again, and that no password is needed to delete it
- What deletion does and does not do: it erases the copy of the key stored
on this device; nothing on chain changes and no money is moved
- The route back — adding the wallet again with the recovery phrase and a
new password — and, in bold, that without that phrase written down the
deletion loses everything the wallet holds, forever
- That the other wallets are not touched
- The wallet's name, and a text input asking for it to be typed back
- Error line
- "Delete This Wallet Forever" button
- **Transitions**:
- "Delete This Wallet Forever" (name typed correctly) → the same two
outcomes as "Confirm Delete" above, through the same `finishDelete()`, so
the selection repair, permission cleanup and `AUTISTMASK_ACTIVE_CHANGED`
broadcast are identical on both routes
- "Delete This Wallet Forever" (name does not match) → "That is not the name
of this wallet. Type <name> to confirm." on the error line, nothing
deleted
- "Back" → **DeleteWallet**, re-entered through its `show()` so the wallet
selection comes back with it. The two delete screens are siblings rather
than parent and child: nothing is pushed on the way here, so both have
Settings as their Back target.
- **Deliberately not password-gated.** A password in front of _discarding_ a
secret protects nobody: an attacker at the popup who wants the wallet gone can
uninstall the extension, so the only person such a gate stops is the owner who
forgot it. The typed name is a check that the user knows which wallet they are
on, not a secret, so it is matched with surrounding spaces and letter case
ignored.
- Not in `RESTORABLE_VIEWS`, alongside `delete-wallet-confirm`: a popup reopened
by accident must not land on a screen whose button erases key material.
#### DeleteAddress (`delete-address-confirm`)
- **When**: User tapped the `[x]` next to an address on Home. Offered only on HD
@@ -1273,13 +1329,13 @@ on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign.
refused: "+" derives the next unused index (`nextIndex` is a high-water
mark), and re-importing the wallet's key material is rejected as a
duplicate by `findWalletByXpub` while the wallet is still present. What
works is deleting the whole wallet in Settings — password-gated, and it
destroys the stored secret — then importing again, whereupon
`scanForAddresses()` rediscovers the address **only if it has on-chain
activity**. An address that was never used is not found by that scan. The
text is written by `recoveryPathText()` rather than sitting in
`index.html`, so it can name the wallet's own kind of key material: an
xprv wallet has no recovery phrase to re-import.
works is deleting the whole wallet in Settings — which destroys the stored
secret — then importing again, whereupon `scanForAddresses()` rediscovers
the address **only if it has on-chain activity**. An address that was
never used is not found by that scan. The text is written by
`recoveryPathText()` rather than sitting in `index.html`, so it can name
the wallet's own kind of key material: an xprv wallet has no recovery
phrase to re-import.
- A warning when the address holds anything, ETH or any tracked ERC-20,
followed by the holdings themselves via `balanceLinesForAddress()` and the
USD total via `formatAddressTotal()` (see