fix: run libsodium on WebAssembly under the extension CSP (closes #182)
All checks were successful
check / check (push) Successful in 32s
All checks were successful
check / check (push) Successful in 32s
libsodium ships a WASM build and a wasm2js translation in one file, tries WASM first, and silently falls back if instantiation throws. Under a plain script-src 'self' the fallback was taken on every popup load, announced by nothing but an uncaught CompileError. Measured on the vault's own Argon2id parameters (OPSLIMIT_INTERACTIVE, MEMLIMIT_INTERACTIVE), node 22: WASM 141-198ms per derivation, wasm2js 3204-3660ms. The work factor is identical either way — it is set by the ops and memory parameters, not by wall time — so the fallback bought no security and cost about 3.5s on every operation that asks for the password, which is every signature. Both manifests now declare script-src 'self' 'wasm-unsafe-eval'; object-src 'self' for extension pages: an object under content_security_policy.extension_pages for Chrome MV3, a bare string for Firefox MV2. The keyword permits compiling WebAssembly and nothing else — not eval() of strings, not inline script, not remote script — and reaching it requires already executing script in an extension page. 'unsafe-eval' is not granted. The silence is what made this dangerous, so the fallback is now loud at three levels: tests/manifest.test.js pins both policies to exactly that token set, failing make check if the grant is dropped or if anything is added beside it; tests/vaultBackend.test.js asserts the unit tests exercise the WASM backend, with a self-validating check that libsodium never swapped its fallback in; and the e2e suite compiles a WebAssembly module inside the real popup under the real manifest, with the harness allowlist entry that used to excuse the CompileError now deleted. The runtime fallback itself is kept — a wallet that refuses to decrypt is worse than a slow one — but vault.js now reports the backend and logs an error when it is not WASM.
This commit is contained in:
51
README.md
51
README.md
@@ -123,16 +123,17 @@ unavailable). The suite lives in `tests/e2e/` and is driven by
|
||||
`playwright-core`, whose version must stay matched to the container's Playwright
|
||||
version — the browsers ship inside the image.
|
||||
|
||||
It covers popup load, wallet creation through the UI, the Add Token screen, the
|
||||
transaction detail screen for an ERC-20 transfer, and the recovery phrase screen
|
||||
— which wallet types are offered it, that it holds nothing before the password
|
||||
is accepted, that a wrong password reveals nothing, that leaving it by either
|
||||
route wipes it — including a leave taken while the decrypt is still running —
|
||||
and that reopening the popup does not land on it. All outbound network is
|
||||
intercepted at the browser level and served from fixtures in
|
||||
`tests/e2e/network.js`, so the run is deterministic and fully offline;
|
||||
unrecognised outbound requests are reported as failures rather than silently
|
||||
allowed.
|
||||
It covers popup load, WebAssembly compilation under the shipped CSP (see
|
||||
[Content Security Policy](#content-security-policy)), wallet creation through
|
||||
the UI, the Add Token screen, the transaction detail screen for an ERC-20
|
||||
transfer, and the recovery phrase screen — which wallet types are offered it,
|
||||
that it holds nothing before the password is accepted, that a wrong password
|
||||
reveals nothing, that leaving it by either route wipes it — including a leave
|
||||
taken while the decrypt is still running — and that reopening the popup does not
|
||||
land on it. All outbound network is intercepted at the browser level and served
|
||||
from fixtures in `tests/e2e/network.js`, so the run is deterministic and fully
|
||||
offline; unrecognised outbound requests are reported as failures rather than
|
||||
silently allowed.
|
||||
|
||||
That reporting has one bound worth knowing. Observation ends when the browser
|
||||
context is torn down, and nothing can watch traffic after that, so the run keeps
|
||||
@@ -1068,6 +1069,36 @@ battle-tested.
|
||||
Exceptions require explicit authorization in a code comment referencing this
|
||||
policy, but as of now there are none.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
Both manifests declare the same policy for extension pages —
|
||||
`script-src 'self' 'wasm-unsafe-eval'; object-src 'self'` — as an object under
|
||||
`content_security_policy.extension_pages` in `manifest/chrome.json` (MV3) and as
|
||||
a bare string in `manifest/firefox.json` (MV2).
|
||||
|
||||
`'wasm-unsafe-eval'` is there for one reason: libsodium. It ships a WebAssembly
|
||||
build and a `wasm2js` translation of it in one file, tries WASM first, and
|
||||
silently falls back to the translation if instantiation throws. Under a plain
|
||||
`script-src 'self'` the fallback was taken on every popup load, announced by
|
||||
nothing but an uncaught `CompileError`. Measured on the same Argon2id parameters
|
||||
the vault uses (`OPSLIMIT_INTERACTIVE`, `MEMLIMIT_INTERACTIVE`), WASM derives a
|
||||
key in 141-198ms and `wasm2js` in 3204-3660ms. The work factor is identical — it
|
||||
is set by the ops and memory parameters, not by wall time — so the fallback
|
||||
bought nothing and cost about three and a half seconds on every operation that
|
||||
asks for the password, which is every signature.
|
||||
|
||||
The keyword permits compiling WebAssembly and nothing else: not `eval()` of
|
||||
strings, not inline script, not remote script. Using it requires already
|
||||
executing script in an extension page, which is complete compromise on its own.
|
||||
`'unsafe-eval'` is a different proposition and is not granted.
|
||||
|
||||
The grant is pinned in both directions. `tests/manifest.test.js` asserts the
|
||||
exact token set in both manifests, so dropping `'wasm-unsafe-eval'` (a silent
|
||||
20x regression on the key derivation) and adding anything beyond it both fail
|
||||
`make check`. `tests/vaultBackend.test.js` asserts the unit tests run the WASM
|
||||
backend, and `make test-e2e` compiles a WebAssembly module inside the real popup
|
||||
under the real manifest.
|
||||
|
||||
### DEBUG Mode Policy
|
||||
|
||||
The `DEBUG` constant in the popup JS enables a red "DEBUG / INSECURE" banner and
|
||||
|
||||
Reference in New Issue
Block a user