// The shipped Content Security Policy, pinned in both directions. // // This is the anti-regression check for #182. libsodium decides its // backend by trying to compile WebAssembly and catching the failure, so a // CSP that refuses WASM demotes the vault to the wasm2js translation — // roughly 20x slower per Argon2id derivation — and says so only in a // console message nobody reads. Dropping 'wasm-unsafe-eval' from either // manifest therefore has to fail a check, not a log line. // // It is equally a check against loosening. 'wasm-unsafe-eval' is granted // deliberately and narrowly (see the backend note in src/shared/vault.js); // 'unsafe-eval', 'unsafe-inline' and any remote script source are not, and // an exact match on the token set is what keeps the next edit from // smuggling one in alongside. // // build.js copies these files to dist//manifest.json verbatim, so // what is asserted here is what ships. const fs = require("fs"); const path = require("path"); const MANIFEST_DIR = path.join(__dirname, "..", "manifest"); const EXPECTED_SCRIPT_SRC = ["'self'", "'wasm-unsafe-eval'"]; const EXPECTED_OBJECT_SRC = ["'self'"]; const FORBIDDEN_SOURCES = [ "'unsafe-eval'", "'unsafe-inline'", "http:", "https:", "data:", "blob:", "*", ]; function readManifest(name) { return JSON.parse( fs.readFileSync(path.join(MANIFEST_DIR, name + ".json"), "utf8"), ); } // "script-src 'self'; object-src 'self'" -> { "script-src": ["'self'"], ... } function parseCsp(policy) { const directives = {}; for (const part of policy.split(";")) { const tokens = part.trim().split(/\s+/).filter(Boolean); if (tokens.length === 0) continue; directives[tokens[0]] = tokens.slice(1); } return directives; } function assertPolicy(policy) { const directives = parseCsp(policy); expect(Object.keys(directives).sort()).toEqual([ "object-src", "script-src", ]); expect(directives["script-src"].slice().sort()).toEqual( EXPECTED_SCRIPT_SRC, ); expect(directives["object-src"].slice().sort()).toEqual( EXPECTED_OBJECT_SRC, ); for (const source of FORBIDDEN_SOURCES) { expect(directives["script-src"]).not.toContain(source); expect(directives["object-src"]).not.toContain(source); } } describe("shipped Content Security Policy", () => { // MV3 takes an object and applies extension_pages to the popup and the // background service worker, which is where libsodium runs. test("chrome MV3 allows WASM and nothing else beyond 'self'", () => { const csp = readManifest("chrome").content_security_policy; expect(typeof csp).toBe("object"); expect(Object.keys(csp)).toEqual(["extension_pages"]); assertPolicy(csp.extension_pages); }); // MV2 takes the policy as a bare string, and Firefox 102 and later // require 'wasm-unsafe-eval' for extension pages exactly as Chrome // does. Same policy, different manifest shape. test("firefox MV2 allows WASM and nothing else beyond 'self'", () => { const csp = readManifest("firefox").content_security_policy; expect(typeof csp).toBe("string"); assertPolicy(csp); }); // The two targets share one codebase and one crypto path; a policy // that drifts apart between them means one of the two builds is // running a backend nothing tests. test("both targets ship the same policy", () => { const chrome = readManifest("chrome").content_security_policy.extension_pages; const firefox = readManifest("firefox").content_security_policy; expect(firefox).toBe(chrome); }); });