diff options
Diffstat (limited to 'docs/meshbay-draft-v6.md')
| -rw-r--r-- | docs/meshbay-draft-v6.md | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/docs/meshbay-draft-v6.md b/docs/meshbay-draft-v6.md new file mode 100644 index 0000000..6a161cd --- /dev/null +++ b/docs/meshbay-draft-v6.md @@ -0,0 +1,270 @@ +# MeshBay — Architecture Draft v6 + +> Status: **current specification.** Supersedes `meshbay-draft-v5.md`. +> **Sections not restated here are unchanged from v5**, which remains the reference for +> everything v6 does not touch — the handshake (§4), node authority (§5), the hub's role +> (§6), cryptography (§7) and the testing posture (§10) are all still v5's. +> +> v6 exists because a design discussion on 2026-08-17 settled the desktop client and, in +> doing so, changed four things v5 states: what a group's content *is*, how a person's +> devices are admitted, how authorship is established, and which shell the native client +> uses. It also records one rule v5 assumed without writing down. +> +> The v5 convention is carried forward and is not negotiable: **a claim in this document +> must name the adversary it holds against.** A property that holds against a passive hub +> and not an active one is written that way. + +--- + +## 0. Reading order + +| Read | For | +|---|---| +| **this document** | what is true now, and what changed from v5 | +| `meshbay-draft-v5.md` | everything v6 does not restate — still authoritative there | +| `second-review.md` | the findings (C1–C6, H1–H7, M*, L*) referenced throughout the code | +| `docs/invite-pairing-v1.md` | invitations, pairing codes, the node roster — **as built** | +| `docs/per-node-identity-v1.md` | identity keys are per node; the hub stores none | +| `docs/desktop-client-v1.md` | the desktop client in full — shell, device linking, roots, packaging, execution order | +| `devel-phases-next.md` | the roadmap | + +--- + +## 1. Changes from v5 + +| # | Category | Change | Source | +|---|---|---|---| +| 1 | Content model | A group's content is **several named roots**, not one directory. Names are unique, derived from the directory's basename, and form a union virtual root | E7 / decision 11, 13 | +| 2 | Identity | **Device linking**: one person may hold several devices on a node, admitted by a key the node already pinned, bound by a one-time code the new device generates | E2 / decision 2 | +| 3 | Client | The native client is **Electron**, not pywebview. Structural decision 18 reversed | E1 / decision 1 | +| 4 | Node authority | `gek_rotate` may be a signed MNP op — the C5b rule forbids *key material arriving from outside*, not an operator-signed instruction where the node generates the key | §2.3 below | +| 5 | Server state | **Group-related server state lives on the node, always.** Written down because v5 assumed it | E9 / decision 17 | +| 6 | Portability | exFAT/NTFS and Windows are the **common** case. Case folding and Unicode normalization become correctness requirements, not compatibility notes | E8 / decision 12 | +| 7 | Accounts | Native registration is **hybrid**: passphrase-derived `auth_key` (the recovery path) plus a device Ed25519 key for day-to-day authentication | E3 / decision 4 | +| 8 | Authorship | Chat senders are **cryptographically authenticated to each other**; an upload has a **provable owner** who may delete it, as the operator may. v5's node-asserted attribution is replaced | operator decision, §2.4b | + +--- + +## 2. What v6 changes in v5's text + +### 2.1 §5.2 Uploads — now per root + +v5 confines uploads to `shared_root/uploads/` with a filename allowlist, no overwrite, +chunk ordering and a size cap. All four protections stand. Two amendments: + +- There is no single `shared_root`. **The operator designates one root as the upload + destination**; the quarantine lives inside it. If that root is unavailable the upload + fails with a stated reason and never falls back to another; if none is designated, + uploads are refused rather than guessed. +- **The no-overwrite rule is unchanged and still holds on exFAT/NTFS.** An earlier + draft claimed a string comparison let `README.TXT` land on `readme.txt` there. It does + not: the check is `Path.exists()`, and `stat()` is itself case-insensitive on those + filesystems, so the upload already gets a free name. C5a is not reachable through the + filesystem. Case folding is needed for index identity, collision reporting and root + names — comparisons the code makes itself. + +v5's rule that nothing derived is written beside the originals is **unchanged**, and it +decides the video-thumbnail question: a frame grab is produced on demand and cached on the +device that asked, so the node keeps no thumbnail store. + +### 2.2 §5.5 Admission — devices, not one key per person + +v5 and `invite-pairing-v1.md` bind **one** key pair to an account per node: `identities` +has `user_id` as its primary key and `pin_identity` does `INSERT OR REPLACE`. A person +with a browser and a native client needs two keys on the same node, so: + +- `identities` becomes keyed by `(user_id, pk_ed25519)`, with `label`, `added_at`, + `added_by_pk` and `revoked_at`. **`INSERT OR REPLACE` must go** — today it silently + overwrites, which becomes a hole the moment a second key is legitimate. +- A new device is admitted when **a key the node already pinned countersigns it**, bound + by a one-time code the new device generates and displays, hashed together with the new + keys so the node cannot substitute them. +- The operator's one-time code remains available and is unchanged. Device linking is an + addition to admission, not a replacement. + +**Against an active hub this holds**, and for the same reason §5.5 holds: the hub has +stored no user keys since 2026-08-14, so it cannot produce the countersignature. Against a +malicious node operator it is not a new exposure — a node can only add a device to itself, +where it already reads everything it serves. + +**Where it does not hold:** approval performed *in a browser* inherits T3, because the hub +serves that browser its code and can read the typed code. The first browser-to-native link +is therefore the moment of highest exposure for an account, and it happens once. + +Full design: `docs/desktop-client-v1.md` §4. + +### 2.3 §5.1 GEK activation — the rule, stated precisely + +v5 says *"nothing arriving over MNP can activate a GEK"*. Read precisely: the rule targets +**key material arriving from outside** (C5b), not the instruction. An operator-signed +`gek_rotate` where **the node generates the key with its own CSPRNG** satisfies the +property v5 §5.5 actually establishes — the node produces every copy of the key — and is +allowed. + +**The initial `gek-init` stays local.** With no GEK, `join_result` answers `no_gek` and no +MNP session completes, so there is no authenticated session to carry a signed op. Placing +it in the pre-proof window is possible and is deliberately deferred; that window is where +C4 and C5b were born. + +### 2.4 §8.2 Native client — Electron + +pywebview is replaced by Electron plus an optional Python sidecar for hub-less `group://` +over QUIC. The non-negotiable is unchanged and is the entire point: **UI assets ship +inside the package and load from disk.** A shell pointing at the hub's `/app/` is a browser +with a different icon. + +What changes is the engine, not the claim. Native still does **not** remove trust in the +hub operator; it converts an undetectable, per-request attack into an artifact that can be +hashed and compared, and that value is realised by reproducible builds (18.7), not by the +packaging format. + +Two corrections to v5's client table: + +- **Key storage.** Identity keys are generated and kept locally, never bundled. C4 closes + for a native device unconditionally — and **stays open for any account that also uses a + browser**, which has no durable storage of its own and still needs a bundle on each + node. An account is only as strong as its weakest client. +- **Crypto.** The client keeps WebCrypto *and* gains local Argon2id and ChaCha20 in the + main process. v5 implied WebCrypto is lost with the browser engine; under Electron it is + not. + +### 2.4b §5.1 Authorship — authenticated, not asserted + +v5 §5.1 authorizes `file_delete` by "the node operator, or the user who uploaded the file +(verified by the key recorded at upload)". Two changes: + +**Authorization moves from the key to the account.** With several devices per person, +`_admin_exec_file_delete` — which verifies against `entry.uploader_pk`, the exact uploading +key — would refuse Alice's desktop the right to delete what her phone uploaded. It becomes +**any non-revoked device of `uploader_id` in the roster**, with `uploader_pk` kept as the +audit record of which device acted. This remains **roster-rooted, not token-rooted**: a hub +minting a token that claims to be Alice holds no key the node pinned for Alice, so the +signature fails — the property `per-node-identity-v1.md` established is preserved. + +**Ownership becomes provable.** The uploader signs `meshbay:upload:v1` over node, group, +root, path, content hash, account and timestamp; the node stores it with the index entry. +Ownership is then verifiable by any member rather than asserted by the node, and the C5a +path — overwriting a file to become its recorded uploader — is closed a second time. + +**Chat senders must be cryptographically authenticated to each other.** v5 relied on NS6, +where the node enforces `sender_id` from the authenticated session; that is the node's +word. Messages are signed with the sender's **device** key, clients pin `account → device +keys` on first sight using the device-add countersignatures as evidence, and the operator +may sign a roster attestation to close first contact. + +**Against whom this holds.** Against another member: fully — no member can forge another's +signature. Against someone holding the node's disk: fully — a stolen chat store cannot be +*extended* with messages that verify. Against the node operator: **partially, and the +partial part is worth having** — once a member's client has pinned Alice's device key, an +operator who turns malicious later cannot forge Alice to that member; forgery is limited to +accounts the victim has never seen. Full protection at first contact requires an +attestation rooted outside the node, which is what the operator-signed roster and safety +numbers provide. + +Design: `docs/desktop-client-v1.md` §4.8. + +### 2.5 §6.1 Hub role — one addition, one rule + +The hub gains exactly one endpoint from all of this: **`POST /v1/users/auth`**, device +Ed25519 authentication on the pattern of `POST /v1/nodes/auth`. Nothing else in the +desktop-client design adds a row or a column to the hub. + +And the rule v5 assumed without writing: + +> **Group-related server state lives on the node.** Files, indexes, members' devices, +> pending device requests, invitations, chat, per-root availability, and anything a future +> feature wants to keep about a group — all on the node. The hub holds accounts, the group +> registry and membership, signaling, notifications and the moderation surface, and +> nothing else about content. + +Verified for the multi-root change: `SwarmSource` carries `content_hash`, `node_id` and +`endpoint` — **no paths, no filenames** — and private groups register nothing (H7). The +content model changes end to end without the hub moving. + +### 2.6 §7 Cryptography — unchanged, one consumer added + +No parameter changes. `keyderive.py` now has a third consumer: the desktop client derives +`auth_key` exactly as the browser does at registration. `test_bundle_kdf_parity.py` covers +it, and the standing warning is unchanged — **never change those parameters in one +place**; a mismatch does not look like an error, it looks like an account nobody can open. + +--- + +## 3. Filesystem portability as a security property + +New in v6, because it was treated as an edge case and is not one. Most users are expected +to share from an external exFAT or NTFS volume, on Windows. + +| Property | Consequence | +|---|---| +| Case-insensitive, case-preserving | `Film.mkv` and `film.mkv` cannot coexist. The index needs a canonical identity and a **case-folding collision check** at scan time. The no-overwrite rule must be case-folded — **this one is a security fix** (§2.1) | +| Unicode normalization | `Café.mkv` written on macOS (NFD) and Windows (NFC) are different byte strings. Normalize to **NFC for identity**, preserve the original bytes for display and opening | +| Windows reserved names, `MAX_PATH` | A group indexed on Linux can hold names Windows cannot create. The client sanitizes on save **and says so**; use `\\?\` paths | +| FAT/exFAT timestamps (2 s, local time) | mtime alone is not a change detector. Size + mtime with tolerance, rehash when in doubt | +| Watcher reliability | `ReadDirectoryChangesW` drops events under load; inotify on a FUSE mount is unreliable. **Periodic reconciliation is mandatory on both platforms** | +| No symlinks, no POSIX permissions | Simplifications: nothing to defend against, and the node runs as the user anyway | + +A volume that disappears must **freeze** the affected root's subtree, never empty it. +Emptying propagates deletions for a whole library as though the owner had erased it. + +--- + +## 4. Security claims — deltas only + +v5 §2's table stands. Three rows change, and they are the honest version: + +| Claim | Passive hub | Active hub | Malicious node operator | +|---|---|---|---| +| Client code integrity | ✅ ships in the package (native) | ⚠️ **detectable, not prevented** — realised by 18.7, not by packaging | ✅ | +| Keypair bundles (**C4**) | closed for native devices | closed for native devices | ⚠️ **open for any account that also uses a browser** | +| Devices | ✅ | ✅ the hub cannot countersign a device — it holds no user keys | ⚠️ a node adds devices only to itself, where it already reads everything | + +**The claim v6 supports:** *the hub cannot read your content, and against a native client +its only remaining lever is the artifact it ships — which can be hashed and compared.* + +**The claim it must not make:** that a native client makes the hub untrusted, or that C4 +is closed for an account that still signs in from a browser. + +--- + +## 5. Still open + +v5 §9's list stands, with these movements: + +| # | Item | Status | +|---|---|---| +| C4 | Remote keypair bundles | **Partially closed.** Gone for native devices; open for browser-using accounts until the signed `device_policy {allow_bundle: false}` opt-out ships | +| T3 | Hub serves the SPA | **Accepted permanently** for browser users. Removed for native clients, whose value depends on 18.7 | +| — | Chat encryption (Sender Keys) | Phase 15, unchanged. Pairwise to identity keys, never GEK-derived — and **now to devices**, which multiplies the recipients per person | +| — | Delegation | Designed, deferred, unchanged | +| — | Hub identity pinning | New. `GET /v1/hub/pubkey` exists and nothing pins it; bounded, because a substituted hub can neither read content nor ship the code to a native client | + +**Phase 15 has been re-read against device linking (2026-08-17) and was wrong as written.** +The correction is recorded in `devel-phases-next.md` §15.0b; the load-bearing part: + +- **A sender key is per device, never per person.** A shared per-person chain advanced by + two devices produces key and nonce reuse — which is exactly why `first-review.md` C1 + rejected a shared Double Ratchet for groups. The same mistake, one level down. +- `senderkeys.py` already fails this silently: `GroupSenderKeyStore.add_sender` does + `self._states[dist.sender_id] = ...`, so a second device under the same `sender_id` + **overwrites the first and drops its chain**. `sender_id` must become a device + identifier. +- **Revoking a device must rotate**, like revoking a member. +- **A newly linked device cannot read history** until every sender redistributes, unless + the linking device hands over its own state sealed to the new device's key. +- **Sender attribution stays node-trusted.** A sender key proves a *device*; the mapping + from device to account comes from the node's roster. Encrypted chat does not make + senders cryptographically authenticated to each other, and the docs must not imply it. + +Ordering consequence: **device linking (Stage C) lands before Phase 15**, or Phase 15 is +built against an identity model that is about to change underneath it. + +--- + +## 6. Where the detail lives + +This document states what changed and what holds. It does not restate the desktop client's +design, which is long and belongs in one place: +**`docs/desktop-client-v1.md`** — shell requirements, device-linking protocol and schema, +account creation, node management over signed MNP ops, several roots per group, filesystem +portability, packaging and first run, the web tier, and the execution order for all of it. |