fix: run libsodium on WebAssembly under the extension CSP (closes #182)
All checks were successful
check / check (push) Successful in 26s
All checks were successful
check / check (push) Successful in 26s
This commit was merged in pull request #206.
This commit is contained in:
@@ -1,14 +1,80 @@
|
||||
// Vault: password-based encryption of secrets using libsodium.
|
||||
// Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
|
||||
// All crypto operations are delegated to libsodium — no raw primitives.
|
||||
//
|
||||
// Backend: WebAssembly, deliberately (#182).
|
||||
//
|
||||
// libsodium ships one file containing both a WebAssembly build and a
|
||||
// wasm2js ("asm.js") translation of it. It tries WASM first and, if
|
||||
// instantiation throws, silently swaps in the translation. An extension
|
||||
// CSP of plain script-src 'self' refuses WASM, so every popup load used
|
||||
// to take that fallback — announced by nothing but an uncaught
|
||||
// CompileError in the console.
|
||||
//
|
||||
// Measured here, same Argon2id parameters (OPSLIMIT_INTERACTIVE,
|
||||
// MEMLIMIT_INTERACTIVE = 2 passes over 64MiB), node 22 on this machine:
|
||||
// WASM 141-198ms per derivation, wasm2js 3204-3660ms. The work factor is
|
||||
// identical either way — it is set by the ops/mem parameters, not by wall
|
||||
// time — so the fallback bought no security, it only made every password
|
||||
// operation take three and a half seconds, and the wallet asks for the
|
||||
// password on every signature.
|
||||
//
|
||||
// So both manifests declare 'wasm-unsafe-eval' for extension pages. That
|
||||
// keyword permits compiling WebAssembly and nothing else: not eval() of
|
||||
// strings, not inline script, not remote script. Reaching it requires
|
||||
// already executing script in the extension page, which is total
|
||||
// compromise on its own. 'unsafe-eval' would be a different matter and is
|
||||
// not granted. tests/manifest.test.js pins both policies to exactly
|
||||
// "'self' 'wasm-unsafe-eval'" so neither the grant nor the surrounding
|
||||
// strictness can drift unnoticed.
|
||||
//
|
||||
// The fallback still exists, and a wallet that refuses to decrypt is
|
||||
// worse than a slow one, so it is not disabled — it is made loud:
|
||||
// cryptoBackend() reports which backend this realm can run, ensureReady()
|
||||
// logs an error if it is not WASM, tests/vaultBackend.test.js asserts the
|
||||
// unit tests exercise the WASM backend, and the end-to-end suite asserts
|
||||
// it in the real popup under the real manifest.
|
||||
|
||||
const sodium = require("libsodium-wrappers-sumo");
|
||||
const { log } = require("./log");
|
||||
|
||||
// An empty WebAssembly module: the 8-byte magic number and version header,
|
||||
// no sections. Compiling it asks the cheapest possible form of the only
|
||||
// question that matters here — may this realm compile WebAssembly at all —
|
||||
// which is exactly what a CSP without 'wasm-unsafe-eval' refuses, and
|
||||
// exactly what decides which backend libsodium ends up on.
|
||||
const EMPTY_WASM_MODULE = new Uint8Array([
|
||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||
]);
|
||||
|
||||
// "wasm" or "asmjs": whether this realm may compile WebAssembly, which is
|
||||
// what decides libsodium's backend when the CSP is the reason it cannot —
|
||||
// the case this codebase guards. It probes the realm, not libsodium, so a
|
||||
// fallback taken for some other reason (allocation failure, corrupt module)
|
||||
// would not be caught here; tests/vaultBackend.test.js checks libsodium's
|
||||
// own marker directly.
|
||||
async function cryptoBackend() {
|
||||
try {
|
||||
await WebAssembly.compile(EMPTY_WASM_MODULE);
|
||||
return "wasm";
|
||||
} catch (_) {
|
||||
return "asmjs";
|
||||
}
|
||||
}
|
||||
|
||||
let ready = false;
|
||||
|
||||
async function ensureReady() {
|
||||
if (!ready) {
|
||||
await sodium.ready;
|
||||
if ((await cryptoBackend()) !== "wasm") {
|
||||
log.errorf(
|
||||
"libsodium is running on the wasm2js fallback: this realm " +
|
||||
"refuses to compile WebAssembly, so every password " +
|
||||
"derivation costs roughly 20x what it should. See the " +
|
||||
"backend note in src/shared/vault.js.",
|
||||
);
|
||||
}
|
||||
ready = true;
|
||||
}
|
||||
}
|
||||
@@ -59,4 +125,4 @@ async function decryptWithPassword(encrypted, password) {
|
||||
return sodium.to_string(plaintext);
|
||||
}
|
||||
|
||||
module.exports = { encryptWithPassword, decryptWithPassword };
|
||||
module.exports = { cryptoBackend, decryptWithPassword, encryptWithPassword };
|
||||
|
||||
Reference in New Issue
Block a user