aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md31
1 files changed, 31 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index b2fb9b0..b8c2646 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -193,6 +193,37 @@ See `docs/invite-pairing-v1.md`. Read it before touching invites, admin authorit
- Revocation now works for key delivery (nothing stored survives it) — but **still rotate
the GEK**, the ex-member holds the current one
+## Keypair bundles and the browser KDF (2026-08-14)
+
+- The bundle key is **Argon2id 128 MB / t=3 / p=1**, WebAssembly vendored under
+ `static/vendor/` (CSP forbids external hosts; 12.2 must keep `wasm-unsafe-eval`).
+ **Do not change the parameters in one place**: `keyderive.js`, the QE harness and
+ `test_bundle_kdf_parity.py` are held byte-identical by that test, and a mismatch
+ presents as an account nobody can open
+- Bundles carry an `MBK2` marker; the PBKDF2 form is still readable and is
+ re-encrypted on the next backup. Both keys are derived at sign-in because the
+ passphrase is deliberately not retained
+- Cost is paid **once per sign-in** (650 ms bundle + 239 ms auth_key); reloading a
+ page derives nothing — the key lives in IndexedDB
+- The bundle is stored on **every node its owner joins**. That is what makes a
+ second browser work, and it is C4: cracking one yields identity keys, hence
+ content on *other* nodes and the ability to sign as that user. Draft-v5 §7.1 has
+ the measured numbers. **The passphrase is the wall; the KDF is a speed bump**
+- Floor: 12 characters and ~60 estimated bits, enforced client-side only — with the
+ password split (T1) the hub never sees a passphrase
+
+## Two lessons that cost four rounds of live testing
+
+- **`QE/deploy/e2e.py` cannot test `app.js`.** It is a second implementation of the
+ client, written in the right order by construction: it proves the protocol and
+ nothing about the SPA. Three ordering bugs passed it and failed in a browser.
+ `test_spa_ordering.py` exists for that class and is worth extending
+- **An unbounded `await` on the hub socket makes a node silently unreachable.**
+ Three instances found in `maintain_ws`: the offer handler awaited inside the read
+ loop, `ws.recv()` for auth with no timeout, and `return` on auth refusal ending
+ the task for good. Symptom is always the same — daemon running, logging nothing,
+ `connected_nodes: 0`, socket in CLOSE-WAIT. Look there first
+
**Corrections to remember:**
- `punch_nat()` is **not** a NAT traversal stack — one UDP probe, no STUN, no candidate
gathering, one ISP validated. **ICE/STUN (WebRTC) is the traversal path**, for native