Files
AutistMask/TODO.md
clawbot 4e2ca87a06
All checks were successful
check / check (push) Successful in 27s
harden: verify all approval fields and make failed signing retryable (closes #174)
verifySignedTx compared only from, to, value and data, so a signed
transaction could differ from the approval in chain id, nonce, gas limit
or any fee field and still be broadcast. Worse, it named the fields it
checked and so admitted every field it did not name: a type 4 artifact
carrying an EIP-7702 authorization passed verification, paying the
approved amount to the approved recipient and, in the same transaction,
permanently installing another contract's code at the signer's own
account.

The check is now an allowlist in both directions. The transaction type
must be 0, 1 or 2 — the only types this wallet signs — so no later
EIP-2718 type can bring a field along; authorizationList, blobs, blob
commitments and blob gas fees are refused by name; and the access list
is compared with the approval. Every consequential field is compared and
any mismatch refuses outright: the chain id against the selected network
(and against the approval when the page fixed one), plus nonce, gas
limit, gasPrice, maxFeePerGas and maxPriorityFeePerGas wherever the
approval carries a value, together with the fee mechanism the approval
implies. Fields the approval does not carry are populated locally by the
popup and have no approved value to compare against, so they are held to
absolute ceilings instead. Verification then closes by rebuilding the
transaction from exactly those checked fields and comparing the unsigned
bytes, so an artifact carrying anything this module does not account for
is refused without having to be named first.

An approved value that is not a number now refuses like every other
quantity rather than escaping as a raw BigInt conversion error, which
was reported as retryable and left a live button that could never
succeed.

A failed signing attempt also left a button that could not succeed: the
background deleted the approval before it broadcast, so a retry found
nothing to sign. The approval is now retired once the request has an
outcome, and the background tells the popup which stage failed. A popup
that could not sign is retryable; a mismatch spends the approval; a
failed broadcast is terminal, because the node may have accepted the
transaction and still failed to answer and the popup's retry re-signs at
a freshly fetched nonce rather than re-broadcasting the same bytes,
which would send the approved transfer twice.

Keeping the approval alive for that retry cost it its single use: the
handler read it, then verified and broadcast asynchronously, so a second
AUTISTMASK_TX_RESPONSE carrying the same id started an independent
verify and broadcast instead of finding nothing. With the ordinary dApp
approval shape the page fixes no nonce, so two artifacts signed at
different nonces both verify and the approved transfer goes out twice; a
reloaded approval window during a slow broadcast is enough to send it,
since the only guard was popup-local button state. The approval is now
claimed synchronously, before the first await, and released only when an
attempt fails in a way the user may retry. Same interlock on
AUTISTMASK_SIGN_RESPONSE.

Surviving the whole verify-and-broadcast window put the approval within
reach of every other path that retires one, and those paths did not
consult the claim. Closing the approval popup, switching the active
address, or a reject arriving late each resolved the waiting promise
4001 while the attempt behind it ran to completion; the attempt's own
resolve then landed on a settled promise, so the transaction reached the
chain and the page was told the user rejected it. The user's natural
response is to redo the transfer from the site, which re-signs at a
fresh nonce and sends it twice — the outcome this change exists to
prevent, reached without an adversary, since the popup stays open across
the broadcast and a user closing an apparently-hung window is enough.

Every settlement now goes through one function. settleApproval() is the
only place an approval is resolved or removed, and it refuses a claimed
approval unless the caller holds the claim, so a path added later
inherits the interlock instead of having to remember it. The active-
address switch also leaves a claimed approval's window standing rather
than force-closing the window the attempt is reporting into. The
duplicate refusal on the sign path now carries a stage of its own, so
the popup stops telling the user to start again from the site while a
first attempt may still succeed.

Verification also compared only the decode against itself: both sides of
the closing byte comparison derive from one Transaction.from(), while
what is broadcast is the artifact string. An artifact re-encoded with a
leading zero byte on an RLP quantity therefore decoded to the approved
transaction, passed, and broadcast different bytes. The artifact is now
required to be the canonical encoding of its own decode, which is what
makes the claim that it *is* the approved transaction true.

The background's approval wiring had no tests, which is where these
defects lived. It has them now, driven through the real message listener
from eth_sendTransaction to broadcast, with windows.onRemoved captured
rather than stubbed away: each retirement path is asserted to leave a
mid-broadcast attempt alone and to still reject an approval no attempt
holds.
2026-08-12 08:45:01 +00:00

208 lines
12 KiB
Markdown

# Workflow
- `git pull` `next` and cut a branch from it — one branch per issue, named
`issue-<N>-<slug>`. Never branch from `main`.
- Do the work as one commit whose title ends with ` (closes #N)`, with the
`TODO.md` update in that same commit.
- Move Next Step to the top of Completed Steps; move the top item of Future
Steps into Next Step.
- Run `make fmt`, then `make check`. A feature branch may be red; `next` and
`main` may not.
- Rebase onto current `next` immediately before pushing — other branches land on
`next` continuously — and re-run `make check` after resolving, because a clean
textual merge can still break the build.
- Push the branch and open one PR per issue with base `next`. Never base `main`.
- An independent reviewer who did not write the change gates the merge. On a
passed review the PR is squash-merged into `next`.
- `next` is the branch for the next milestone. It is kept green and mergeable to
`main` at any moment, without notice.
- `main` receives exactly one PR per milestone, from `next`. Releases are tagged
from `main`.
# Status
pre-1.0, working towards the 1.0.0 milestone. Tagged v0.1.0 on 2026-02-27. The
milestone is in flight on `next`; its `next` -> `main` PR is
[#190](https://git.eeqj.de/sneak/AutistMask/pulls/190). `make check` verified
green on `next` at `e9fa8be` on 2026-08-10, and `make build` produces
`dist/chrome/` and `dist/firefox/` with every bundle verified to have `DEBUG`
compiled off.
The backlog lives on the
[Gitea tracker](https://git.eeqj.de/sneak/AutistMask/issues), which is
authoritative; this file does not duplicate it. Full policy file set present. A
real-browser end-to-end suite (`make test-e2e`) now sits alongside `make check`,
which cannot see a runtime `ReferenceError` in a popup view.
# Next Step
Land [#152](https://git.eeqj.de/sneak/AutistMask/issues/152): add ESLint to
`script/lint`. `make check` is `prettier --check` only today and cannot catch
undefined identifiers, which is how
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) shipped.
# Completed Steps
- 2026-08-12: Approval verification became an allowlist — transaction type
restricted to 0/1/2 so an EIP-7702 delegation can no longer ride along on an
approved transfer, every consequential field compared, the artifact
re-serialized from the checked fields alone and its exact bytes required to be
the canonical encoding of what was broadcast. One approval now yields at most
one broadcast, and every path that retires a pending approval — popup close,
active-address change, a late reject — goes through a single chokepoint that
refuses to settle an attempt already claimed for signing and broadcast
([#174](https://git.eeqj.de/sneak/AutistMask/issues/174)).
- 2026-08-12: An xprv wallet already in storage that was imported from a
non-master key is detected from the depth of its stored `xpub`, explained in
the wallet list, and blocked from signing, sending and private-key export
instead of throwing on the send screen
([#234](https://git.eeqj.de/sneak/AutistMask/issues/234)).
- 2026-08-12: An unreported `holders_count` is now parsed as `null` rather than
`0`, so the low-holder rule declines to judge an unknown count instead of
hiding a legitimate token as spam, in both the transaction history and the
Send token selector ([#230](https://git.eeqj.de/sneak/AutistMask/issues/230)).
- 2026-08-12: Bundled token list documentation no longer states a count. The
four "top 250" claims in `README.md` and the "roughly 500" claim in
`docs/README.md` are replaced with a description of how the list is actually
selected — a point-in-time CoinGecko snapshot of the highest-market-cap
Ethereum mainnet ERC-20s — with `TOKENS` in `src/shared/tokenList.js` named as
the authoritative set
([#239](https://git.eeqj.de/sneak/AutistMask/issues/239)).
- 2026-08-11: libsodium runs on WebAssembly in the shipped builds —
`'wasm-unsafe-eval'` added to both manifest CSPs after measuring the wasm2js
fallback at 20x the Argon2id cost, pinned in both directions by
`tests/manifest.test.js` and observed in the real popup by the e2e suite
([#182](https://git.eeqj.de/sneak/AutistMask/issues/182)).
- 2026-08-11: Known-symbol spoof verification became a Settings toggle
(`hideSpoofedSymbols`), on by default, governing the transaction-history
filter and the fraud-contract learning it feeds
([#176](https://git.eeqj.de/sneak/AutistMask/issues/176)).
- 2026-08-11: `script/verify-build` now walks `dist/` NUL-delimited and asserts
`dist/` is a real directory, so a path with a trailing space or a newline can
no longer carry a debug marker past the unlisted-bundle check
([#223](https://git.eeqj.de/sneak/AutistMask/issues/223)).
- 2026-08-11: UTC Timestamps checkbox moved from the Token Spam Protection well
into Display, next to the theme selector
([#212](https://git.eeqj.de/sneak/AutistMask/issues/212)).
- 2026-08-11: Network fee counted in the confirmation-screen balance check for
both ETH and ERC-20 sends, reserving what the node actually charges a type-2
transaction, with the arithmetic in a pure, unit-tested
`src/shared/txValidation.js`
([#154](https://git.eeqj.de/sneak/AutistMask/issues/154)).
- 2026-08-11: A dust threshold of `0` now means "hide nothing" instead of
falling back to the 100,000 gwei default, and every address comparison in
`src/shared/transactions.js` goes through one case-normalising helper so a
checksummed genuine contract is no longer read as a spoof
([#179](https://git.eeqj.de/sneak/AutistMask/issues/179)).
- 2026-08-11: Password-gated recovery phrase display for HD wallets, reached
from the wallet row in Settings, wiped on leaving the screen and excluded from
the views the popup can reopen onto
([#161](https://git.eeqj.de/sneak/AutistMask/issues/161)).
- 2026-08-11: Extended-key import hardened — the base58 checksum is now enforced
on every xprv and xpub, and a non-master key is refused with an explanation
instead of being derived beneath
([#210](https://git.eeqj.de/sneak/AutistMask/issues/210)).
- 2026-08-11: the balance refresh and the 24-hour phishing list refresh moved
from `setInterval` to the extension alarms API, with the phishing delta and
its fetch timestamps persisted to extension storage, so neither job dies with
the MV3 service worker. Each job's freshness guard was decoupled from its
alarm period at the same time — timed to the period, a guard vetoes its own
scheduled tick and halves the real refresh rate
([#158](https://git.eeqj.de/sneak/AutistMask/issues/158)).
- 2026-08-11: Policy compliance sweep — conditional verbose test rerun, local
Tailwind binary instead of `npx`, `--frozen-lockfile` on `make install`, and
the Makefile-only targets documented in the README
([#166](https://git.eeqj.de/sneak/AutistMask/issues/166)).
- 2026-08-11: `script/verify-build` diagnostics corrected: the both-markers
message now states what is and is not proven, an unreadable bundle is
diagnosed as an I/O fault rather than as changed output, the `*.js` assumption
lives only in `build.js`, and the unlisted-bundle scan hard-fails when it
cannot enumerate `dist/`
([#180](https://git.eeqj.de/sneak/AutistMask/issues/180)).
- 2026-08-11: Known-answer test coverage for the crypto core — BIP-39/BIP-32
derivation in `wallet.js` and the Argon2id vault in `vault.js`
([#159](https://git.eeqj.de/sneak/AutistMask/issues/159)).
- 2026-08-11: Three `README.md` claims corrected against the code — blocklist
attribution, token-display rule, navigation model
([#213](https://git.eeqj.de/sneak/AutistMask/issues/213)).
- 2026-08-11: README Screen Map rebuilt from the code — every screen, element
and transition re-verified against `src/popup/`
([#164](https://git.eeqj.de/sneak/AutistMask/issues/164)).
- 2026-08-11: `docs/README.md` rewritten against the code: no competitor names,
all five network destinations documented, password/Settings/Add Wallet
sections corrected ([#163](https://git.eeqj.de/sneak/AutistMask/issues/163)).
- 2026-08-11: `loadState()` now derives `hasWallet` from the wallet list instead
of trusting the persisted flag, so a profile already saved inconsistent no
longer stays broken on every load
([#195](https://git.eeqj.de/sneak/AutistMask/issues/195)).
- 2026-08-11: Wallet deletion repairs its own state — `hasWallet` follows the
remaining wallets, the selection only moves when it was deleted, and the
active-address change is broadcast to connected sites
([#156](https://git.eeqj.de/sneak/AutistMask/issues/156)).
- 2026-08-11: One row per on-chain value movement in transaction history: the
merge moved into the pure `mergeTransactions` and the zero-ETH native side of
a plain ERC-20 transfer absorbed into its token row
([#177](https://git.eeqj.de/sneak/AutistMask/issues/177)).
- 2026-08-11: `TODO.md` Workflow rewritten to the branch-and-PR-per-issue model
on `next`, with Status and Next Step refreshed
([#191](https://git.eeqj.de/sneak/AutistMask/issues/191)).
- 2026-08-09: `DEBUG` became a build-time constant defaulting to off, injected
as the `__BUILD_DEBUG__` esbuild define and turned on with
`AUTISTMASK_DEBUG=1`, so a plain `make build` no longer hands every newly
created wallet the publicly committed test recovery phrase
([#149](https://git.eeqj.de/sneak/AutistMask/issues/149)).
- 2026-08-09: dApp approval signing moved into the popup — the password no
longer crosses the extension messaging boundary; the background broadcasts and
resolves approvals only, and verifies the signed artifact against the approval
it holds (#157).
- 2026-08-09: Post-build assertion that every emitted bundle containing
`constants.js` has `DEBUG` compiled off, via `script/verify-build` on the
`make build` path (#170).
- 2026-08-09: Containerized Chrome end-to-end harness (`make test-e2e` /
`script/test-e2e`) driving the real popup with all network intercepted, plus
the two used-but-not-imported crashes it caught: AddToken unreachable (#150)
and TransactionDetail broken for every ERC-20 transfer (#151). Harness
demonstrated failing before the fixes and passing after (#181). Interception
covers the MV3 background service worker, not just the popup page, and a
launch-time canary aborts the suite if worker traffic starts escaping.
- 2026-08-09: Reviewed the repo end to end and filed the 1.0.0 backlog
(#149-#168).
- 2026-08-09: Test coverage for the address-poisoning defense in
`src/shared/transactions.js` (#160)
- 2026-07-26: About well in settings with build info, repo link and the version
click easter egg (#145); proper view navigation stack (#146).
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
shims, README Entrypoints section (#148)
- 2026-03-01: USD display suppressed on testnets (#142); estimated USD for ETH
in approve-tx view (#141).
- Sepolia testnet support (#137); etherscan links go to token-specific URLs
(#136).
- Transaction detail improvements: Type field and on-chain details (#130),
txid-first reordering (#133), swap display corrections (#128), expanded
confirm-tx warnings (#118).
- Dark mode theme setting (Light/Dark/System) with contrast fixes (#126);
timestamps include timezone offset (#120); layout shift audit, reserved space
for error messages (#124).
- Copy-flash visual feedback with timing tune (#113, #121); cross-wallet-type
duplicate detection (#115).
- 2026-02-27: v0.1.0 tagged.
- 2026-02-24: Initial scaffolding: popup UI, BIP-39 wallet creation via
ethers.js, wallet persistence, real ETH balances over RPC, ENS forward and
reverse resolution.
# Future Steps
Only work that has no issue of its own belongs here; everything else is on the
tracker.
- Pre-1.0 security review of the extension (key handling, DEBUG mode policy, RPC
input validation) before any 1.0rc tag. Individual filed issues are parts of
it, but the review is broader than any of them.
- Decide whether docker-in-docker makes `make test-e2e` runnable in the Gitea
workflow. Extending the suite itself is tracked as
[#183](https://git.eeqj.de/sneak/AutistMask/issues/183) and
[#184](https://git.eeqj.de/sneak/AutistMask/issues/184).
- Cut 1.0.0 once the milestone is empty, then continue tagging as milestones
land.