summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/meshbay-draft-v5.md15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/vendor/PROVENANCE.md2
-rw-r--r--packages/meshbay-hub/tests/test_bundle_kdf_parity.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/hub_client.py17
5 files changed, 37 insertions, 14 deletions
diff --git a/docs/meshbay-draft-v5.md b/docs/meshbay-draft-v5.md
index 6fdbbc6..e8cc3a4 100644
--- a/docs/meshbay-draft-v5.md
+++ b/docs/meshbay-draft-v5.md
@@ -353,11 +353,20 @@ whose group the user joins, and GEK and keypair bundle fetches are served in the
window because the client needs its bundle to compute the proof. The window is bounded
(4 fetches) and audited.
-The bundle's own protection moved from PBKDF2-SHA512 to **Argon2id, 64 MB, t=3, p=1**
+The bundle's own protection moved from PBKDF2-SHA512 to **Argon2id, 128 MB, t=3, p=1**
(`static/vendor/argon2.min.js`, WebAssembly, no external host). PBKDF2 is compute-only,
so 600k iterations cost an attacker with a GPU far less than the wall clock suggested:
-measured on the dev machine, both take ~0.3 s honestly, but only one of them makes a
-graphics card allocate 64 MB per guess. The two implementations — the browser's WASM and
+measured on the dev machine, PBKDF2 costs 241 ms and Argon2id 88 ms natively, but only
+one of them makes a graphics card find 128 MB per guess. The honest size of that gain:
+on a single card the ceiling moves from roughly 8k guesses/s to roughly 2k — a factor of
+four, not a thousand. What it really buys is the cost of scale, since 128 MB per lane caps
+a 24 GB card at about 187 concurrent guesses and makes custom hardware unattractive.
+
+**The passphrase, not the KDF, is what decides this.** At ~2k guesses/s a dictionary-plus-
+rules run of 10⁹ candidates — which covers a large share of human-chosen passwords —
+takes about six days on one card. Four random words (~52 bits) takes longer than the age
+of the universe. No parameter choice saves a weak passphrase; it only moves it from hours
+to days. The two implementations — the browser's WASM and
`argon2-cffi` in QE — are held byte-identical by a parity test, because a disagreement
would present as an account nobody can open.
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", "üñïçø∂é ✓ 🔐"]
diff --git a/packages/meshbay-node/src/meshbay_node/hub_client.py b/packages/meshbay-node/src/meshbay_node/hub_client.py
index 691ac7c..334107d 100644
--- a/packages/meshbay-node/src/meshbay_node/hub_client.py
+++ b/packages/meshbay-node/src/meshbay_node/hub_client.py
@@ -241,6 +241,7 @@ class HubClient:
# working one until someone notices the node has vanished.
async with websockets.connect(
ws_url, ping_interval=20, ping_timeout=20, close_timeout=5,
+ open_timeout=15,
) as ws:
auth_msg = {
"type": "auth",
@@ -250,10 +251,20 @@ class HubClient:
if group_ids:
auth_msg["group_ids"] = group_ids
await ws.send(json.dumps(auth_msg))
- auth_resp = json.loads(await ws.recv())
+ # Bounded: a hub that accepts the socket and then says nothing
+ # — which is what it does for a few seconds while restarting —
+ # would otherwise park this task here forever, with the node
+ # running, silent, and invisible to everyone.
+ auth_resp = json.loads(
+ await asyncio.wait_for(ws.recv(), timeout=15))
if auth_resp.get("type") != "auth_ok":
- log.error("WS auth failed: %s", auth_resp)
- return
+ # Not fatal: the token may simply have expired while we
+ # were disconnected. Refresh on the next pass rather than
+ # ending the task, which used to strand the node for good.
+ log.warning("WS auth refused: %s — retrying in 5s", auth_resp)
+ await asyncio.sleep(5)
+ await self.ensure_fresh_token()
+ continue
self._ws = ws
log.info("Hub WS connected")