fix: an already-imported non-master xprv wallet will throw at signing time with no explanation #234

Closed
opened 2026-08-11 15:18:05 +02:00 by clawbot · 2 comments
Collaborator

#210 makes extended-key import reject a non-master (depth > 0) key, because the serialized format does not carry the path and guessing the remaining components silently produces the wrong wallet. Correct for new imports.

It does not address wallets already in stored state. A type: "xprv" wallet holding a depth-3 key — only possible via the old broken path, so its addresses were never the user's to begin with — now throws at signing time instead of silently signing for the wrong tree. Throwing is the better of the two, but it happens at the worst possible moment, with no explanation, on the screen where the user is trying to move money.

There is no migration and no user-facing account of what happened.

Raised by the implementer of #232 and deliberately left out of its scope.

Implementation requirements

  • Detect an existing type: "xprv" wallet whose stored secret is not a master key, at unlock or wallet-list render time — not at signing time.
  • Surface it as a named, explained bad-wallet state in the wallet list: what is wrong, that the addresses shown were never derived from their key correctly, and what they can do about it.
  • No signing path may throw an unhandled error for such a wallet.
  • Be careful what the explanation claims. These wallets' addresses never corresponded to the user's key, so telling them "your funds are safe" would be wrong, and so would implying they have lost something — the addresses were derived from a tree the user never controlled. Say exactly what is true.
  • Do not silently delete or rewrite the wallet. It is the user's data and the decision is theirs.

Definition of done

  • A state fixture containing a depth-3 xprv wallet renders the explanation in the wallet list rather than appearing normal.
  • No signing, sending or address-derivation path throws an unhandled error for such a wallet.
  • A normal depth-0 xprv wallet is entirely unaffected.
  • The explanatory copy is accurate about what happened and is full sentences per the README Language & Labeling rules.
  • Tests cover both the affected and the unaffected wallet, demonstrated failing first.
  • TODO.md updated in the same commit.
  • make check passes.
https://git.eeqj.de/sneak/AutistMask/issues/210 makes extended-key import reject a non-master (depth > 0) key, because the serialized format does not carry the path and guessing the remaining components silently produces the wrong wallet. Correct for new imports. It does not address wallets **already in stored state**. A `type: "xprv"` wallet holding a depth-3 key — only possible via the old broken path, so its addresses were never the user's to begin with — now throws at signing time instead of silently signing for the wrong tree. Throwing is the better of the two, but it happens at the worst possible moment, with no explanation, on the screen where the user is trying to move money. There is no migration and no user-facing account of what happened. Raised by the implementer of https://git.eeqj.de/sneak/AutistMask/pulls/232 and deliberately left out of its scope. ## Implementation requirements - Detect an existing `type: "xprv"` wallet whose stored secret is not a master key, at unlock or wallet-list render time — not at signing time. - Surface it as a named, explained bad-wallet state in the wallet list: what is wrong, that the addresses shown were never derived from their key correctly, and what they can do about it. - No signing path may throw an unhandled error for such a wallet. - Be careful what the explanation claims. These wallets' addresses never corresponded to the user's key, so telling them "your funds are safe" would be wrong, and so would implying they have lost something — the addresses were derived from a tree the user never controlled. Say exactly what is true. - Do not silently delete or rewrite the wallet. It is the user's data and the decision is theirs. ## Definition of done - [ ] A state fixture containing a depth-3 xprv wallet renders the explanation in the wallet list rather than appearing normal. - [ ] No signing, sending or address-derivation path throws an unhandled error for such a wallet. - [ ] A normal depth-0 xprv wallet is entirely unaffected. - [ ] The explanatory copy is accurate about what happened and is full sentences per the README Language & Labeling rules. - [ ] Tests cover both the affected and the unaffected wallet, demonstrated failing first. - [ ] `TODO.md` updated in the same commit. - [ ] `make check` passes.
clawbot added this to the 1.0.0 milestone 2026-08-11 15:18:05 +02:00
Author
Collaborator

Plan.

Detection without the password. The stored xpub on an xprv wallet is the neutered node four levels below the imported key (both the current import path and the pre-fix one derive four levels: m/44'/60'/0'/0 absolute, 44'/60'/0'/0 relative). A master import therefore stores a depth-4 xpub; a depth-d import stores depth d + 4. So xpub.depth !== 4 is an exact read on "the imported key was not a master key", available at wallet-list render time with no decryption. New src/shared/walletDefects.js holds that check and the copy.

Surfacing. The wallet list renders a named notice block under the wallet's name, and the "+" add-address button is suppressed for such a wallet. Send (both the main-view button and the address-detail one), the dapp approve-transaction and approve-sign screens, and Export Private Key are gated before any decryption, so the user sees the explanation rather than a decrypt-then-throw. getSignerForAddress keeps throwing as the backstop.

Copy. The addresses do descend from the key the user imported, at a non-standard path, so I will not say the funds are gone and I will not say they are safe. The notice states that the derivation path was applied beneath a non-master key, that these are not the addresses that key produces under the standard path, that anything held at them is still reachable by software repeating the same non-standard derivation, and that the wallet is not deleted.

Nothing is deleted or rewritten. Tests: a state fixture with a depth-3 xprv wallet and a normal depth-0 one, asserted against the rendered wallet-list markup and against the signing paths.

Plan. **Detection without the password.** The stored `xpub` on an `xprv` wallet is the neutered node four levels below the imported key (both the current import path and the pre-fix one derive four levels: `m/44'/60'/0'/0` absolute, `44'/60'/0'/0` relative). A master import therefore stores a depth-4 xpub; a depth-`d` import stores depth `d + 4`. So `xpub.depth !== 4` is an exact read on "the imported key was not a master key", available at wallet-list render time with no decryption. New `src/shared/walletDefects.js` holds that check and the copy. **Surfacing.** The wallet list renders a named notice block under the wallet's name, and the "+" add-address button is suppressed for such a wallet. Send (both the main-view button and the address-detail one), the dapp approve-transaction and approve-sign screens, and Export Private Key are gated before any decryption, so the user sees the explanation rather than a decrypt-then-throw. `getSignerForAddress` keeps throwing as the backstop. **Copy.** The addresses do descend from the key the user imported, at a non-standard path, so I will not say the funds are gone and I will not say they are safe. The notice states that the derivation path was applied beneath a non-master key, that these are not the addresses that key produces under the standard path, that anything held at them is still reachable by software repeating the same non-standard derivation, and that the wallet is not deleted. Nothing is deleted or rewritten. Tests: a state fixture with a depth-3 xprv wallet and a normal depth-0 one, asserted against the rendered wallet-list markup and against the signing paths.
Author
Collaborator

Built in #247.

New src/shared/walletDefects.js detects the wallet from the depth of its stored xpub — a master import stores depth 4, a depth-d import stores d + 4 — so no password is needed and the check runs at wallet-list render time. The list renders the explanation under the wallet's name and withholds the "+" button; send from all three screens, dapp transaction approval, dapp message signing and Export Private Key all refuse before asking for a password; getSignerForAddress stays as the backstop with a message written for signing rather than for import. The wallet record is not modified, and a test asserts that.

Copy is in the PR body. It says the addresses do descend from the imported key and that this build cannot sign for them, and it neither claims the funds are safe nor implies a loss.

Verified: 22 new tests in tests/walletDefects.test.js, demonstrated failing first — with the module stubbed to return null and walletListHtml absent, make test gave 13 failed / 370 passed. After the fix make check is green on the rebased branch (15 suites, 383 tests, prettier --check clean twice), and make build emits 4 bundles verified autistmask-build-debug=off.

Built in [#247](https://git.eeqj.de/sneak/AutistMask/pulls/247). New `src/shared/walletDefects.js` detects the wallet from the depth of its stored `xpub` — a master import stores depth 4, a depth-`d` import stores `d + 4` — so no password is needed and the check runs at wallet-list render time. The list renders the explanation under the wallet's name and withholds the "+" button; send from all three screens, dapp transaction approval, dapp message signing and Export Private Key all refuse before asking for a password; `getSignerForAddress` stays as the backstop with a message written for signing rather than for import. The wallet record is not modified, and a test asserts that. Copy is in the PR body. It says the addresses do descend from the imported key and that this build cannot sign for them, and it neither claims the funds are safe nor implies a loss. Verified: 22 new tests in `tests/walletDefects.test.js`, demonstrated failing first — with the module stubbed to return `null` and `walletListHtml` absent, `make test` gave 13 failed / 370 passed. After the fix `make check` is green on the rebased branch (15 suites, 383 tests, `prettier --check` clean twice), and `make build` emits 4 bundles verified `autistmask-build-debug=off`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#234