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.
vault.js: Argon2id key derivation + XSalsa20-Poly1305 encryption
via libsodium-wrappers-sumo. No raw crypto primitives.
Wallet creation now requires a password. The mnemonic or private
key is encrypted before storage — only the ciphertext blob
(salt, nonce, ciphertext) is persisted. The plaintext secret
is never stored.
Sending requires the password to decrypt the secret, derive
the signing key, and construct the transaction. Wrong password
is caught and reported.