fix: run libsodium on WebAssembly under the extension CSP (closes #182)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
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:
52
tests/vaultBackend.test.js
Normal file
52
tests/vaultBackend.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// The unit tests must exercise the libsodium backend that actually ships
|
||||
// (#182). Before this, they could not: node compiles WebAssembly happily,
|
||||
// the extension CSP refused it, and so the browser silently ran the
|
||||
// wasm2js translation while every test ran the WASM build.
|
||||
//
|
||||
// With 'wasm-unsafe-eval' in both manifests the two agree, and these tests
|
||||
// hold that agreement in place from the node side. tests/manifest.test.js
|
||||
// holds up the CSP end of it, and the end-to-end suite observes the real
|
||||
// popup.
|
||||
|
||||
const { cryptoBackend } = require("../src/shared/vault");
|
||||
|
||||
// The module libsodium-wrappers-sumo itself requires and drives. Not a new
|
||||
// dependency: it is inspected here, never used to perform crypto, because
|
||||
// it is the only thing that can say which backend is loaded.
|
||||
const SODIUM_CORE = "libsodium-sumo";
|
||||
|
||||
describe("libsodium backend", () => {
|
||||
test("this realm compiles WebAssembly, so the tests run the WASM build", async () => {
|
||||
await expect(cryptoBackend()).resolves.toBe("wasm");
|
||||
});
|
||||
|
||||
test("libsodium did not swap in the wasm2js fallback", async () => {
|
||||
const core = require(SODIUM_CORE);
|
||||
await require("libsodium-wrappers-sumo").ready;
|
||||
// useBackupModule is the entry point to the fallback; taking it
|
||||
// replaces the module's exports with the translation's, and the
|
||||
// entry point goes with them. Still present after ready means the
|
||||
// WASM module is the one in place. The test below is what keeps
|
||||
// that inference honest.
|
||||
expect(typeof core.useBackupModule).toBe("function");
|
||||
});
|
||||
|
||||
// Deliberately last, and deliberately destructive: it takes the
|
||||
// fallback, which replaces the loaded module for the rest of this
|
||||
// file. Jest gives each test file its own module registry, so nothing
|
||||
// outside sees it.
|
||||
//
|
||||
// Without this, the check above would be a claim about libsodium's
|
||||
// internals with nothing holding it to account: if a future version
|
||||
// kept useBackupModule on the fallback module too, the marker would
|
||||
// quietly become true in both backends and the test would pass while
|
||||
// measuring nothing. Forcing the fallback and watching the marker
|
||||
// disappear is what makes its presence mean something.
|
||||
test("the fallback marker distinguishes the two backends", async () => {
|
||||
const core = require(SODIUM_CORE);
|
||||
await require("libsodium-wrappers-sumo").ready;
|
||||
expect(typeof core.useBackupModule).toBe("function");
|
||||
await core.useBackupModule();
|
||||
expect(typeof core.useBackupModule).toBe("undefined");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user