From 1c96aceb54d66cae1b48aa0eb8887f68e53f9e24 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 14 Aug 2026 14:25:44 +0200 Subject: perf(client): bundle KDF to 128 MB, and derive it once per sign-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Argon2id memory 64 → 128 MB. Memory is the lever, not time: it caps how many guesses a card can hold at once, so the ceiling on one high-end GPU moves from roughly 4k to roughly 2k guesses/s and its 24 GB fits ~187 lanes instead of ~375. Measured through the vendored build: 640 ms, against 322 ms at 64 MB. While measuring the real cost of a sign-in, found the SPA deriving the bundle key twice — once for the key pair kept for the session, then again inside decryptBundle() for the local bundle. At these parameters that is 0.6 s of pure waste. Measured now, end to end: auth_key (PBKDF2 600k) 239 ms bundle v1 (PBKDF2 600k) 240 ms legacy, until every bundle is upgraded bundle v2 (Argon2id 128MB) 650 ms ----------------------------------- sign-in 1 129 ms (889 ms once no v1 bundles remain) Once per sign-in, and only then: reopening a group, downloading, streaming and reloading the page all reuse the key, which lives in IndexedDB from login. Also bounds two waits in the node's hub WebSocket, found because the node went silent again mid-deploy. It had reconnected after the hub restart, sent its auth frame, and waited for a reply that never came — `ws.recv()` had no timeout, so a hub that accepts a socket and then says nothing for a few seconds while starting up parks the task forever: node running, logging nothing, invisible to everyone. The auth exchange now times out at 15 s, connect at 15 s, and a refused auth retries with a fresh token instead of ending the task for good. QE harness signs in once per account and reuses the token — several clients there stand for several browsers of one person, and what tells them apart is which keys they hold, not which token, while the hub quite rightly rate-limits repeated logins from one address. Tests: 341, plus the live workflow. Co-Authored-By: Claude Opus 5 --- packages/meshbay-hub/src/meshbay_hub/static/keyderive.js | 15 +++++++++------ .../src/meshbay_hub/static/vendor/PROVENANCE.md | 2 +- packages/meshbay-hub/tests/test_bundle_kdf_parity.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) (limited to 'packages/meshbay-hub') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js index afa5d27..af119c7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js @@ -75,11 +75,12 @@ async function generateKeypairs() { // is exactly what a GPU is good at, so 600k iterations bought far less than the // wall-clock time suggested. // -// 64 MB / t=3 / p=1 measured at ~320 ms through this WASM build on a desktop, so -// roughly a second on a modest phone — the most that belongs in a login. Memory -// is what matters here: at 64 MB per guess, a 24 GB card holds a few hundred in -// parallel instead of the effectively unbounded number PBKDF2 allows. -const ARGON2_MEM_KIB = 65536; // 64 MB +// 128 MB / t=3 / p=1 measured at ~640 ms through this WASM build on a desktop. +// Memory is the lever, not time: each guess must hold 128 MB, so a 24 GB card +// fits ~187 in parallel and its bandwidth caps it near 2k guesses/s, against no +// ceiling at all for PBKDF2. 256 MB would double that again at ~1.3 s, which is +// too much to ask of a phone for something paid at every sign-in. +const ARGON2_MEM_KIB = 131072; // 128 MB const ARGON2_TIME = 3; const ARGON2_LANES = 1; @@ -273,7 +274,9 @@ async function loginAndRecover(username, password) { && localStorage.getItem(`meshbay_kp_${username}`)) || null; if (bundleEnc) { - const keys = await decryptBundle(bundleEnc, password, username); + // Reuse the keys just derived — decryptBundle() would run the KDF again, + // and at these parameters that is another 0.6 s for nothing. + const keys = await decryptBundleWithKey(bundleEnc, result.bundleKey); result.skEdB64 = keys.skEd; result.skXB64 = keys.skX; result.keypairBundleEnc = bundleEnc; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/vendor/PROVENANCE.md b/packages/meshbay-hub/src/meshbay_hub/static/vendor/PROVENANCE.md index 35d742b..6935e91 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/vendor/PROVENANCE.md +++ b/packages/meshbay-hub/src/meshbay_hub/static/vendor/PROVENANCE.md @@ -22,7 +22,7 @@ request and nothing to locate at runtime. keypair bundle is protected by the passphrase alone and rests on every node whose group its owner joins (finding C4), so PBKDF2 — compute-only, and therefore cheap on a GPU — was the wrong tool for it. Measured through this build on the dev -machine: Argon2id 64 MB / t=3 / p=1 takes ~320 ms, against ~240 ms for +machine: Argon2id 128 MB / t=3 / p=1 takes ~640 ms, against ~240 ms for PBKDF2-SHA512 at 600k, for a memory cost a GPU cannot ignore. ### argon2.wasm diff --git a/packages/meshbay-hub/tests/test_bundle_kdf_parity.py b/packages/meshbay-hub/tests/test_bundle_kdf_parity.py index c3c8ff9..27e10d4 100644 --- a/packages/meshbay-hub/tests/test_bundle_kdf_parity.py +++ b/packages/meshbay-hub/tests/test_bundle_kdf_parity.py @@ -41,7 +41,7 @@ pytestmark = pytest.mark.skipif( # Parameters must match keyderive.js. If someone tunes them there and not here, # this test fails — which is the point: changing them silently orphans every # bundle already written. -MEM_KIB, TIME_COST, LANES = 65536, 3, 1 +MEM_KIB, TIME_COST, LANES = 131072, 3, 1 CASES = ["alice", "grenet", "utilisateur-é", ""] PASSWORDS = ["correct horse battery staple", "p", "üñïçø∂é ✓ 🔐"] -- cgit v1.2.3