summaryrefslogtreecommitdiffstats
path: root/docs/meshbay-draft-v6.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/meshbay-draft-v6.md')
-rw-r--r--docs/meshbay-draft-v6.md270
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.