From 6d167392f6f8ede37e2794a68a3738f8ba03131d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 16:58:31 +0200 Subject: feat(client): the desktop application keeps M and every node identity in its main process keyring.js derives, opens, mints, seals, signs and agrees there; the page gets public keys and a handle. Argon2 comes from the page's own WebAssembly build (Electron's crypto has none). Without OS key storage the page keeps its keys as a browser does. A node's bundle is settled after connecting, re-sealed when the key changed. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-client/src/argon2-wasm.js | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/meshbay-client/src/argon2-wasm.js (limited to 'packages/meshbay-client/src/argon2-wasm.js') diff --git a/packages/meshbay-client/src/argon2-wasm.js b/packages/meshbay-client/src/argon2-wasm.js new file mode 100644 index 0000000..c68e994 --- /dev/null +++ b/packages/meshbay-client/src/argon2-wasm.js @@ -0,0 +1,66 @@ +/** + * Argon2id for the main process, from the page's own WebAssembly build. + * + * Electron's Node is built on BoringSSL, which has no Argon2: `crypto.argon2` + * exists there and refuses (`ERR_CRYPTO_ARGON2_NOT_SUPPORTED`) — found by + * signing in to the real application, since a plain Node has it. The vendored + * build the page already runs is the one implementation both sides can share, + * which also makes their agreement a matter of construction. + */ + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +let ready = null; + +/** + * The emscripten loader reads its options from a global `Module` (through + * `self`) when it is required, and again while the WebAssembly instantiates, + * which is asynchronous. Both globals are set for that time only — until the + * first derivation has run — so nothing else in this process sees them after. + */ +function load(vendorDir) { + if (ready) return ready; + const saved = {}; + for (const name of ['self', 'Module']) { + saved[name] = Object.prototype.hasOwnProperty.call(globalThis, name) + ? { value: globalThis[name] } : null; + } + const restore = () => { + for (const [name, was] of Object.entries(saved)) { + if (was) globalThis[name] = was.value; else delete globalThis[name]; + } + }; + globalThis.Module = { wasmBinary: fs.readFileSync(path.join(vendorDir, 'argon2.wasm')) }; + globalThis.self = globalThis; + ready = (async () => { + try { + const lib = require(path.join(vendorDir, 'argon2.min.js')); + // One derivation at the lowest cost: the instance exists once it returns. + await lib.hash({ pass: 'x', salt: new Uint8Array(8), time: 1, mem: 8, + hashLen: 16, type: lib.ArgonType.Argon2id }); + return lib; + } finally { + restore(); + } + })(); + ready.catch(() => { ready = null; }); + return ready; +} + +/** `(password, salt, params) => Promise` over the vendored build. */ +function wasmArgon2(vendorDir) { + return async (password, salt, { memory, passes, parallelism, tagLength }) => { + const a = await load(vendorDir); + const out = await a.hash({ + pass: String(password), salt: new Uint8Array(salt), + time: passes, mem: memory, parallelism, hashLen: tagLength, + type: a.ArgonType.Argon2id, + }); + return Buffer.from(out.hash); + }; +} + +module.exports = { wasmArgon2 }; -- cgit v1.2.3