summaryrefslogtreecommitdiffstats
path: root/docs/meshbay-draft-v5.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-14 17:51:48 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-14 17:51:48 +0200
commitf0984e86d9cb596a282ce6feb7cfc2f075b2794b (patch)
tree192198a5d596a06d15095bb1d3540dd4a8d397c8 /docs/meshbay-draft-v5.md
parent9fa2117de1caf4d713cc0b7a310b9549467738c3 (diff)
downloadmeshbay-f0984e86d9cb596a282ce6feb7cfc2f075b2794b.tar.gz
feat!: identity keys per node — C4's blast radius drops to one operator
One keypair was copied to every node its owner joined, so cracking the bundle on any single node yielded the identity used on all of them: their content on other operators' machines, and the ability to sign as them anywhere. That lateral reach was the part of C4 worth attacking. Each node now gets its own keypair, generated the first time its owner joins it and left with that node alone. An operator who cracks what sits on their own disk holds a key that is a stranger to every other node — and on their own node, one that unlocks nothing they did not already hold: they serve the content, the index and every byte of it by design. Nothing changes for the user. A first contact with a node already needed that operator's code, and the key is created in the same step; a second browser still recovers it from the node with the passphrase alone. Two operators can also no longer tell they host the same person by comparing keys. BREAKING, and deliberately without a compatibility path — the deployment is wiped for the next demo: - users.pk_ed25519 / pk_x25519 dropped (migration a7c31f9e40b2) - registration no longer sends or stores a key - PUT /v1/users/me/keys and regenerateKeys() gone; rotation is now `member unpin` plus a fresh code, decided on the machine that pinned it - /pubkeys returns an account id and a node's linking key. It was the directory H3 read, and nothing wraps for it any more - the pk_user JWT claim is gone That last one closed a live defect the inventory turned up: the node recorded pk_user as the uploader's identity and authorized deletion against it, so a hub issuing a token naming its own key could delete anyone's uploads on any node. Attribution now uses the key the node itself pinned. A simplification falls out. Registration generates nothing, so a scripted signup is a real account: `demo.py bootstrap` takes a wiped hub and node to a working demo with no browser, which was impossible while keys were born in one. Also fixes, found by running it on a wiped deployment: the key handed back on a join now belongs to the group the connection is for, not the group named in the invitation — an operator pairs node-wide but redeems the code while opening a group, and expects to read it. Tests: 343, including the two that state the property — a key pinned by one node is refused at another, and someone else's code does not admit it. Verified end to end against a wiped hub and node: bootstrap, pair, invite, join, download, stream, second browser, revoke. Design: docs/per-node-identity-v1.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs/meshbay-draft-v5.md')
-rw-r--r--docs/meshbay-draft-v5.md71
1 files changed, 44 insertions, 27 deletions
diff --git a/docs/meshbay-draft-v5.md b/docs/meshbay-draft-v5.md
index ffc30c2..2a0f3b1 100644
--- a/docs/meshbay-draft-v5.md
+++ b/docs/meshbay-draft-v5.md
@@ -65,7 +65,7 @@ This replaces the informal assurances scattered through v4 §4.2.x and §13.
| Client code integrity | ❌ **T3, accepted** | ❌ T3 | ✅ | ✅ | ✅ |
| Node content authority | ✅ | ✅ | ✅ sovereign | ✅ | ✅ |
| Hub cannot obtain the group key | ✅ | ✅ **since H3 closed** — except in `join_policy = "open"` groups, where it can join legitimately | — | — | ✅ |
-| Your identity keys stay yours | ✅ | ✅ | ⚠️ **offline attack on your keypair bundle** — see §7.1. Succeeds against a weak passphrase, and yields your content on *other* nodes plus the ability to sign as you | ✅ | ✅ |
+| Your identity keys stay yours | ✅ | ✅ | ⚠️ **offline attack on the bundle they hold** — see §7.1. Succeeds against a weak passphrase, and yields the identity used **on that node only**: nothing on anyone else's | ✅ | ✅ |
**The claim this project can make:** *the hub cannot read your content unless it ships you
malicious client code.* Since H3 closed (2026-08-14) that is the **only** remaining path,
@@ -92,11 +92,11 @@ even the hub."* Three reasons, all deliberate:
- **Members and the operator read everything.** Inherent: "end-to-end" here describes
*client ↔ node*, never *client ↔ client*.
-One boundary deserves naming, because the table above hid it until 2026-08-14: an operator
-hosts your content by design, but they should not be able to become *you*. They can try —
-your keypair bundle sits on their disk, and a weak passphrase gives it up (§7.1). That is
-the difference between a node operator reading what they host and a node operator reading
-what other operators host.
+One boundary deserves naming: an operator hosts your content by design, but they should not
+be able to become *you*. They can still try — a bundle sits on their disk and a weak
+passphrase gives it up — but since 2026-08-14 what it gives up is **the identity you use
+with them**, which unlocks nothing they did not already have. Reading what they host is by
+design; reading what *other* operators host is not, and no longer follows.
---
@@ -319,9 +319,15 @@ Hub minimization was considered and **deferred** (decision D4). The hub keeps se
the web UI and remains in the trusted path by choice. This section describes what it
*does*, not an aspiration.
-**Stores:** accounts (username, encrypted email, public keys, status, role), group
-registry and membership, IP logs (1 year, legal), node registrations, refresh tokens,
-notifications, moderation blocklist.
+**Stores:** accounts (username, encrypted email, status, role), group registry and
+membership, IP logs (1 year, legal), node registrations, refresh tokens, notifications,
+moderation blocklist.
+
+**No longer stores user identity keys** (2026-08-14). `users.pk_ed25519` and
+`users.pk_x25519` are dropped, `PUT /me/keys` is gone, and `/pubkeys` returns an account id
+and a node's linking key — nothing to wrap for. Tokens carry no `pk_user` claim either: the
+node used to record it as the uploader's identity, which let whoever issued tokens decide
+who could delete a file.
**Does not store:** file content, file names, private-group indexes, message content,
private keys, GEK bundles, keypair bundles, node IPs beyond ephemeral signaling.
@@ -371,19 +377,25 @@ separation, on-the-fly encryption.
### 7.1 The keypair bundle, and what it is worth to an attacker (C4)
-The bundle carries a user's identity keys, encrypted under their passphrase. It is stored
-on **every node whose group they join**, because that is what lets them open their account
-from a second browser — the ordinary expectation, and the only mechanism available to a
-browser that keeps nothing durable of its own.
+A bundle carries **one node's** identity keys, encrypted under the owner's passphrase, and
+is stored on that node. It is what lets a second browser open the same account there — the
+ordinary expectation, and the only mechanism available to a browser that keeps nothing
+durable of its own.
+
+The adversary is concrete: an operator holding their own node's disk, attacking offline at
+their leisure.
-So the adversary is concrete: an operator holding their own node's disk, attacking offline
-at their leisure.
+**What cracking one yields.** The identity that person uses **on that node** — where the
+operator already holds the content, the index and every byte they serve. It is not a key
+anywhere else: each node gets its own, generated the first time its owner joins it, and a
+key one node pinned is a stranger to the next (which asks for a code, like any first
+contact). Until 2026-08-14 a single identity was copied to every node joined, so one crack
+yielded content on *other* operators' nodes and the ability to sign as that user anywhere —
+that was the part worth attacking, and it is gone.
-**What cracking one yields.** The user's identity keys — and with them, content on
-**other** nodes and the ability to sign as that user. *Not* the content on the attacking
-operator's own node, which they host in the clear by design. This is the one place where a
-node operator can reach past the boundary the rest of the design gives them, and v5 did not
-say so before 2026-08-14.
+Two smaller consequences fall out. Two operators can no longer tell they host the same
+person by comparing keys. And the hub publishes no user keys at all now (§6.1), so there is
+no directory left to substitute from.
**Why Argon2id.** PBKDF2 is compute-only, which is exactly what a GPU is good at. Measured
on the development machine: PBKDF2-SHA512 600k costs 241 ms per guess on one core,
@@ -417,8 +429,10 @@ anything about one.
lives in IndexedDB for the session.
- The pre-proof window that serves bundles is still bounded (4 fetches) and audited.
-C4 closes properly when the native client stops storing bundles remotely (Phase 13.3):
-the material belongs on the user's own device, not on the hub *or* on other people's nodes.
+C4 is **reduced, not closed**: bundles still sit on disks their owner does not control, and
+a weak passphrase still gives up the key for that node. It closes when the native client
+stops storing them remotely (Phase 13.3) — the material belongs on the user's own device,
+not on the hub *or* on other people's nodes.
---
@@ -435,11 +449,14 @@ the material belongs on the user's own device, not on the hub *or* on other peop
| Transport | WebRTC | WebRTC **+ QUIC** |
| Positioning | **Convenience tier** — zero install | Recommended for sensitive use |
-**Several browsers, one identity.** A browser keeps nothing durable the user controls, so
-the account's keys are backed up — encrypted under the passphrase — to the nodes whose
-groups it joins. Any other browser then recovers them with the passphrase alone: same
-identity, same pin, no second pairing code. This is what makes the product behave the way
-people expect, and it is also finding C4 (§7.1). The native client removes the need for it
+**Several browsers, one identity per node.** A browser keeps nothing durable the user
+controls, so the identity it creates for a node is left with that node, encrypted under the
+passphrase. Any other browser recovers it there with the passphrase alone: same identity on
+that node, same pin, no second code. Joining a *different* node creates a different key and
+needs that operator's code, which is the first contact it has always needed.
+
+This is what makes the product behave the way people expect, and it is also finding C4
+(§7.1) — with a blast radius of one node. The native client removes the need for it
entirely, which is a large part of why it exists.
The SPA is not deprecated. It is the zero-install path and it stays. It must be labelled