fix: a forgotten password permanently wedges the wallet — cannot delete it, cannot re-import the phrase #312
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?
Found by the pre-1.0 deployability audit (#303). Blocker: a user holding their recovery phrase can still be locked out of the product with no route back.
src/popup/views/deleteWallet.jsgates deletion on the password.src/popup/views/addWallet.js:137-143refuses a duplicate xpub: "This recovery phrase is already added".src/popup/index.html:158-161) says only "This password encrypts your recovery phrase on this device. You will need it to send funds." It never says the password cannot be recovered or reset.Consequence
A user who forgets the password but does hold their recovery phrase cannot use it. They cannot delete the wallet (password-gated) and cannot re-import the phrase (duplicate-refused). Their only escape is destroying the extension's storage through browser internals, which takes every other wallet with it. The funds are recoverable in other software, but AutistMask has locked itself, and the product never warned that this was possible.
Definition of done
make checkgreen.Plan. Taking the deletion route, not the re-import route.
Why: re-import would have to be built three times over (
hdby xpub,xprvby xpub,keyby address — a private-key wallet is duplicate-refused too, so an xpub-only fix leaves that user still wedged), it makes the user retype the recovery phrase into a live popup just to change a password, and it mutates a wallet in place. The deletion route reaches the same end state through code that already exists and is already tested: delete, then import again on AddWallet, which re-encrypts under the new password and re-runsscanForAddresses()to rediscover the used addresses. It also covers the case re-import does not — a user who has lost the password and simply wants the wallet gone.Shape:
delete-wallet-lost-password, reached from an underlined "I have lost my password" control ondelete-wallet-confirm. No password. Confirmation is typing the wallet's own name; the screen states plainly what is destroyed and that the recovery phrase is the only way back.RESTORABLE_VIEWS— a screen whose button destroys a wallet is not one a popup reopened by accident should land on.finishDelete(), so selection repair, site-permission cleanup and theAUTISTMASK_ACTIVE_CHANGEDbroadcast cannot drift apart.chrome.storage.localstub that structured-clones on bothsetandget, and asserts against what came back out of storage.Built on the deletion route in #334; the reasoning against re-import is in the PR body.
DeleteWallet now offers "I have lost my password", leading to a new
delete-wallet-lost-passwordscreen that destroys the wallet after the user types its name back. No password anywhere in that path, and none added anywhere else. The screen says what is erased (the copy of the key on this device), what is not (anything on chain, the money at the addresses, the other wallets), and states in bold that without the recovery phrase written down the deletion loses everything the wallet holds. Both routes now run onefinishDelete(), so the selection repair, site-permission cleanup andAUTISTMASK_ACTIVE_CHANGEDbroadcast are identical on each. The screen is excluded fromRESTORABLE_VIEWSand registers anonViewLeave()cleanup.AddWallet's password hint gained a sentence per import mode: the password cannot be recovered or reset, and the recovery phrase — or the private key, or the extended private key, according to the tab — is the only backup. The hint line reserves its height so the tabs cannot move the password fields.
Verification:
make checkgreen at exit 0, 827 tests over 40 suites, lint executed in the container rather than served from cache. The new suite drives the real view against achrome.storage.localstub that structured-clones on bothsetandgetand asserts against the storage read-back, with the stub's own non-aliasing pinned by the first test in the file. Two mutations run to show it is not vacuous: forcing the removal index to0fails 4 tests including "exactly the named wallet is destroyed", and deleting theawait saveState()fails 5 — the second is the one an in-memory-only assertion would have missed.Not run: the browser e2e suites, because
script/test-e2emoves theautistmask-e2e-chromeimage tag that is shared across sessions on this host.tests/popupElementIds.test.jsinmake checkcovers the new view's element ids statically.