aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_DESIGN.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
-rw-r--r--docs/MESHBAY_DESIGN.md260
1 files changed, 195 insertions, 65 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 189dbc5..525c5c3 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -17,7 +17,7 @@
> it. §13 is the register of those labels.
>
> Wire versions at the time of writing: **MNP 6.0** (oldest peer accepted 4.0),
-> **MHP 0.1**, packages **0.17.0**. The normative source for the wire format is
+> **MHP 0.1**, packages **0.18.0**. The normative source for the wire format is
> `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol
> serves, not its byte layout.
@@ -134,76 +134,67 @@ paths, no filenames**.
---
-## 2. Trust model
+## 2. Who you trust
-### 2.1 Adversaries
+MeshBay lets you share things with the people you choose, and keeps everyone else
+out. In plain terms, here is who can see your group — and the one caveat worth
+knowing.
-Every claim below is written against one of these, and they are the only ones the
-document uses:
+### 2.1 Inside your group
-| Adversary | What they can do |
-|---|---|
-| **Passive hub** | Read everything the hub legitimately stores and relays |
-| **Active hub** | Also lie: forge tokens, invent accounts, substitute values it publishes, ship modified client code to a browser |
-| **Malicious node operator** | Read and alter everything on their own machine, including the plaintext files they host |
-| **Malicious group member** | Everything a member may do, plus anything the protocol fails to refuse |
-| **Network attacker** | Observe and tamper with traffic between any two parties |
-| **Local attacker** | Reach loopback services and files on a client or node machine |
-| **Registered hub user with no membership** | Reach every hub endpoint that does not check membership |
-| **Federated peer hub** | Push directory rows and revocations over MHP |
+A group lives on a node, run by its operator. Everyone in the group — the operator
+and the members — shares its files, its index and its chat. That is what a group
+*is*: the people you decided to let in, the same as a shared folder or a team
+workspace. They are the people you trust, and they are the only ones who can read
+what you put there.
-### 2.2 Security claims
+Two things follow from that by design — they are the shape of a shared space, not
+gaps in it:
-| Claim | Passive hub | Active hub | Malicious node operator | Malicious member | Network attacker |
-|---|---|---|---|---|---|
-| Data never transits the hub | ✅ | ✅ | — | — | ✅ |
-| File content is unreadable | ✅ | ❌ **T3** (browser) · ✅ native | ❌ by design — the operator hosts the files | ❌ members share the group key | ✅ |
-| The file index is unreadable | ✅ | ❌ T3 · ✅ native | ❌ | ❌ | ✅ |
-| Chat content is unreadable | ✅ | ❌ T3 · ✅ native | ❌ — the operator is a member | ❌ | ✅ |
-| Chat is unreadable **from a copy of the node's storage that lacks its unlock key** — not from a whole disk by default (§4.5) | ✅ | ✅ | — the operator holds the unlock key | ✅ | ✅ |
-| Content cannot be modified | ✅ | ✅ | ❌ by design | ✅ | ✅ |
-| The node cannot be impersonated | ✅ | ✅ | — | ✅ | ✅ |
-| Client code integrity | ❌ **T3, accepted** (browser) · ✅ ships in the package (native) | ❌ T3 · ⚠️ native: **detectable, not prevented** | ✅ | ✅ | ✅ |
-| The hub cannot obtain the group key | ✅ | ✅ **except** in an open-join group, where it can join legitimately (§7.3), and for an invitation the inviter asked the hub to mail, whose code it then holds (§3.4) | — | — | ✅ |
-| Node content authority | ✅ | ✅ | ✅ sovereign | ✅ | ✅ |
-| Devices cannot be added by the hub | ✅ | ✅ — the hub holds no user key and cannot countersign | ⚠️ a node adds a device only to itself, where it already reads everything | ✅ | ✅ |
-| Chat senders are authenticated to each other | ✅ | ✅ | ⚠️ only for accounts the reader has already seen (§3.3) | ✅ | ✅ |
-| Keypair bundles (**C4**) | ✅ it holds the pepper and no bundle | ⚠️ it can fetch a bundle with a token it mints and holds the pepper: an offline passphrase search, as T3 already concedes for browsers · none to fetch for an account without browser access | ✅ no offline search: the bundle does not open without the hub's pepper — only sign-in attempts, bounded and audited · none on disk for an account without browser access | — | — |
-| Deleting your account erases you | ✅ hub-side | ✅ hub-side | ❌ files, pinned identity and bundle stay on the node (§7.7) | — | — |
-| Your identity keys stay yours | ✅ | ⚠️ as the row above | ✅ the bundle they hold does not open without the pepper, and a leaked bundle key opens **that node's** bundle only | ✅ | ✅ |
+- Your operator hosts your files, so they can change what they host. A node's
+ content is the operator's to keep and to serve (§7).
+- Leaving a group removes you on the hub, but copies already on a node stay with
+ its operator (§7.7) — as with anything you have handed to someone in person.
-### 2.3 What the project must not claim
+### 2.2 Everyone else sees nothing
-Three sentences are forbidden, each for a deliberate reason:
+Outside your group nobody can read what you share — not the hub that connects you,
+not anyone watching the network, not the operator of any *other* group:
-- **"Everything is encrypted and unreadable by other parties, even the hub."**
- A hub that ships the code can lift keys from the page regardless of protocol
- design (**T3**). That is an artifact-level attack, not a silent directory lie,
- and it is removed for native clients — not for browsers.
-- **"A native client makes the hub untrusted."** It converts an undetectable,
- per-request, per-user attack into a persistent artifact that can be hashed and
- compared. That value is realised by reproducible builds and published hashes,
- not by the packaging format. A build signed with a key the hub operator holds
- *relocates* trust; it does not remove it.
-- **"C4 is closed."** It is closed for an account whose identities the desktop
- application keeps (browser access off, §3.7): nothing of them is on any node.
- For an account with browser access the bundle is still on each node, sealed
- under the passphrase **and** a pepper the hub holds — no operator can search it
- offline, but an active hub can, and that is T3's adversary already. **An account
- is only as strong as its weakest client.**
+- Your content never passes through the hub in the clear. The hub introduces nodes
+ to one another and relays sealed traffic; it does not hold what you share (§5).
+- The hub holds no key of yours. It cannot read your group, add a device to your
+ account, or act as you — and it cannot hand your group's key to anyone, save
+ where you ask it to (an open-join group, or an invitation you asked it to mail;
+ §7.3, §3.4).
+- Each node carries its own identity keys, so a key that somehow leaked would open
+ that one node and no other (§3), and a copy of a node's storage without its
+ unlock key reveals none of its chat (§4.5).
-"End-to-end" here describes **client ↔ node**, never client ↔ client. Members and
-the operator read everything in their group; that is what a group is.
+### 2.3 Your operator hosts you, but cannot become you
-### 2.4 One boundary worth naming
+Keeping your content is the deal you made with your operator. Turning that into
+*being* you is the line they cannot cross. The identity bundle stored on their
+node opens only with your passphrase and a secret the hub releases to no one but a
+session that has already proved the passphrase or a device key; the only attempts
+left to an operator are ordinary sign-ins, which the hub counts and locks out. And
+reading what *they* host never reaches what *other* operators host (§3.2).
-An operator hosts your content by design. They should not be able to become
-*you*. The bundle on their disk does not let them try: it opens only with the
-passphrase and a pepper the hub hands to nobody but a session that proved the
-passphrase or a device key, so the only guesses left to them are sign-ins, which
-the hub counts and locks out. And were a bundle key to leak all the same, it
-opens **the bundle on that node** and no other. Reading what they host is by
-design; reading what *other* operators host is not, and does not follow (§3.2).
+### 2.4 The one honest caveat: the browser
+
+Used in a web browser, MeshBay is a page the hub sends you on each visit — so a hub
+that had been taken over could send an altered page and lift your keys from it.
+This is true of **every** web application; most simply never say so, and no
+protocol can prevent it, because the attack is in the code itself rather than in
+the messages. So we never claim your content is unreadable *even by the hub* when
+you use a browser.
+
+The desktop and mobile apps close this. Their code is installed once, from a
+package anyone can rebuild from source and check against a published hash, so a
+tampered build is caught instead of silently trusted. For the strongest assurance,
+use the app. This is the project's single standing limit for browser use — tracked
+as **T3** — and the full, adversary-by-adversary analysis behind every statement
+in this section is in §13.9 for readers who want it.
---
@@ -1964,7 +1955,7 @@ an operator-signed op.
Hub minimisation was considered and **deferred, and may be dropped** (decision D4).
The hub keeps serving the web UI and remains in the trusted path by choice. That is
a legitimate product call; what follows from it is carried deliberately rather than
-by accident (§2.3).
+by accident (§2.4).
**Stores:** accounts (username, encrypted email, status, role), the group registry
and membership, IP logs (one year, legal retention), node registrations, refresh
@@ -3352,6 +3343,45 @@ A client, not a host. The platform is hostile to *hosting* a node — background
execution, storage, battery — and fine as a *client*, which is one of the reasons
enrichment is node-side (§6.5).
+**The application is the desktop client's design in a system WebView**
+(`packages/meshbay-android/`). What §8.2 depends on is not Electron but an
+engine with `RTCPeerConnection`, WebCrypto X25519/Ed25519 and MSE, the
+interface loaded from the package, and a privileged side reached through a
+narrow bridge — and Android has all three:
+
+- **The interface is the hub's `static/`, copied at build time** (§8.3) and
+ served from the APK by an asset loader under
+ `https://appassets.androidplatform.net` — a secure context, so `crypto.subtle`
+ exists. Nothing the hub serves is ever loaded into the WebView; the policy is
+ the desktop's, sent as a header.
+- **The bridge offers the desktop preload's `window.meshbay`** wherever it
+ offers anything. What a phone does not have — the node, shared folders, the
+ tray — is **absent, not a function that refuses**: `platform.js` decides what
+ to show from whether an object exists. The bridge answers the packaged
+ origin's top-level document only (`addWebMessageListener`, `isMainFrame`); a
+ same-origin child frame does get the port, and is refused there.
+- **Keys are held natively** (§3.7, §14.1 #20): device key, bundle key and every
+ node identity, under an Android Keystore key, never handed to the page. The
+ Kotlin keyring is a third implementation of the bundle format and the
+ transcripts, so **`tests/vectors/keyring.json` — generated from the desktop
+ keyring and checked against the specification — is what every implementation
+ must reproduce**; a list the vectors cannot hold (the admin operations that
+ may be signed) is compared by source.
+- **Hub calls leave from native code**, to the signed-in hub only, as on the
+ desktop. **Downloads go to disk** through the Storage Access Framework or the
+ Downloads collection, as pending files until complete; uploads come through
+ the system picker.
+- **A hidden page is frozen by Chromium sixty seconds after it is hidden** —
+ measured, and whatever the process's importance: a foreground service, wake
+ locks and the renderer's priority policy do not prevent it. A cast's pipeline
+ lives in the page (WebRTC → decrypt → relay), so while one runs the shell
+ keeps the WebView reported visible and holds a media-playback foreground
+ service; nowhere else, because a page never hidden is never throttled.
+
+Release builds are signed with the development key until the release key exists
+(Stage D12); §2.3's sentence about who holds a signing key applies to whichever
+store distributes them.
+
### 11.4 Casting
An HTTP relay in the desktop client serves a standard fragmented-MP4 stream that
@@ -3366,6 +3396,32 @@ within two. The main process holds what the scan has found and the page polls it
waiting on one call for the whole scan; a poll uses the same checked `handle()`
door as every other call, where a pushed event would be a second one.
+**The Android relay is the desktop's, with three things the phone found.**
+(1) **The header is everything before the first moof**, however many chunks it
+arrives in: the node's first chunk can be the 28-byte ftyp alone, and served as
+the header it left the receiver without a moov. (2) **What a receiver has not
+read waits in a spool file, not in memory.** A fragment is a segment — 5 to
+10 MB at a film's bitrate — and the page runs ahead of the television by its
+whole read-ahead; dropped past the desktop's 8 MB bound, each lost fragment
+froze the picture for its length. (3) **A seek's first segments are held while
+it lands** (`video-player.js`): they are the new stream, its header first, and
+they arrived while `reinitAt` waited on the SourceBuffer and were dropped as the
+old film's — the local player never noticed, a relay restarted there did. The
+local element keeps playing while a cast runs, because its playhead paces the
+stream, and is muted.
+
+**While a receiver plays the film, the player is its remote.** Phone and
+television start apart and drift, so a scrubber on the local playhead lied about
+where the film was and a seek from it landed off by the gap. The page polls
+`cast:status`, whose `chromecast` carries the receiver's `playerState` and
+`position` (stream seconds, zero at the relay's start; the page adds the
+stream's start), and shows that over the local picture with play/pause
+(`cast:chromecast:pause`/`play`), ±30 s and a scrubber. A seek is the ordinary
+seek: it restarts the relay and reloads the receiver there, and until that
+lands the target is shown, not the old stream's position. Pausing the receiver
+pauses the local element too, which otherwise goes on fetching for nobody. The
+copy-URL cast has no receiver to read and keeps the ordinary player.
+
**Subtitles are rebased onto the relay's clock before they are sent.** The node
extracts a track whole, so its cues carry the film's timeline, and the player
can use them unchanged because its SourceBuffer is given `timestampOffset =
@@ -3553,7 +3609,7 @@ Two structural recommendations from that review stand as rules:
|---|---|
| **T1** | **The password split.** The hub never sees a passphrase; it holds a verifier for a client-derived `auth_key`. The passphrase floor can therefore only be enforced client-side (§3.1) |
| **T2** | The hub was the key directory. **Closed** by admission redesign, not by safety numbers: the invite path reads no directory at all (§3.4). Reclassified as **H3** |
-| **T3** | **The hub serves the SPA. Accepted permanently for browser users.** It is the only remaining way an active hub reads content, it is an artifact-level attack rather than a silent lie, and it does not exist for a native client — whose value is realised by reproducible builds, not by packaging (§2.3, §8.2) |
+| **T3** | **The hub serves the SPA. Accepted permanently for browser users.** It is the only remaining way an active hub reads content, it is an artifact-level attack rather than a silent lie, and it does not exist for a native client — whose value is realised by reproducible builds, not by packaging (§13.9, §8.2) |
### 13.5b Availability between members (`AV`)
@@ -3686,6 +3742,72 @@ had already been asked.
| **O13** | **Hub identity pinning.** The client points at a hub by URL and nothing pins that hub's identity. Bounded, because a substituted hub can neither read content nor ship the code to a native client — worth doing all the same |
| **V1–V13**, **P1–P5** | Per-application open items: wording of a disabled-service state, whether artwork reuses the chunk path, cache TTL, multi-track surfacing, HEIC/RAW support, a fuller EXIF panel, lightbox preloading, album-boundary behaviour, cover selection |
+### 13.9 Formal adversary model
+
+§2 states the trust model for a human reader. This restates the same ground
+formally, for auditors and implementers: the adversaries every claim is written
+against, the claim-by-adversary matrix, and the over-claims the project refuses to
+make. Nothing here is new — it is §2 with the proofs shown.
+
+"End-to-end" throughout describes **client ↔ node**, never client ↔ client. The
+operator and the members read everything in their group; that is what a group is.
+
+#### Adversaries
+
+Every claim is written against one of these, and they are the only ones the
+document uses:
+
+| Adversary | What they can do |
+|---|---|
+| **Passive hub** | Read everything the hub legitimately stores and relays |
+| **Active hub** | Also lie: forge tokens, invent accounts, substitute values it publishes, ship modified client code to a browser |
+| **Malicious node operator** | Read and alter everything on their own machine, including the plaintext files they host |
+| **Malicious group member** | Everything a member may do, plus anything the protocol fails to refuse |
+| **Network attacker** | Observe and tamper with traffic between any two parties |
+| **Local attacker** | Reach loopback services and files on a client or node machine |
+| **Registered hub user with no membership** | Reach every hub endpoint that does not check membership |
+| **Federated peer hub** | Push directory rows and revocations over MHP |
+
+#### Claim matrix
+
+| Claim | Passive hub | Active hub | Malicious node operator | Malicious member | Network attacker |
+|---|---|---|---|---|---|
+| Data never transits the hub | ✅ | ✅ | — | — | ✅ |
+| File content is unreadable | ✅ | ❌ **T3** (browser) · ✅ native | ❌ by design — the operator hosts the files | ❌ members share the group key | ✅ |
+| The file index is unreadable | ✅ | ❌ T3 · ✅ native | ❌ | ❌ | ✅ |
+| Chat content is unreadable | ✅ | ❌ T3 · ✅ native | ❌ — the operator is a member | ❌ | ✅ |
+| Chat is unreadable **from a copy of the node's storage that lacks its unlock key** — not from a whole disk by default (§4.5) | ✅ | ✅ | — the operator holds the unlock key | ✅ | ✅ |
+| Content cannot be modified | ✅ | ✅ | ❌ by design | ✅ | ✅ |
+| The node cannot be impersonated | ✅ | ✅ | — | ✅ | ✅ |
+| Client code integrity | ❌ **T3, accepted** (browser) · ✅ ships in the package (native) | ❌ T3 · ⚠️ native: **publicly verifiable, not prevented** | ✅ | ✅ | ✅ |
+| The hub cannot obtain the group key | ✅ | ✅ **except** in an open-join group, where it can join legitimately (§7.3), and for an invitation the inviter asked the hub to mail, whose code it then holds (§3.4) | — | — | ✅ |
+| Node content authority | ✅ | ✅ | ✅ sovereign | ✅ | ✅ |
+| Devices cannot be added by the hub | ✅ | ✅ — the hub holds no user key and cannot countersign | ⚠️ a node adds a device only to itself, where it already reads everything | ✅ | ✅ |
+| Chat senders are authenticated to each other | ✅ | ✅ | ⚠️ only for accounts the reader has already seen (§3.3) | ✅ | ✅ |
+| Keypair bundles (**C4**) | ✅ it holds the pepper and no bundle | ⚠️ it can fetch a bundle with a token it mints and holds the pepper: an offline passphrase search, as T3 already concedes for browsers · none to fetch for an account without browser access | ✅ no offline search: the bundle does not open without the hub's pepper — only sign-in attempts, bounded and audited · none on disk for an account without browser access | — | — |
+| Deleting your account erases you | ✅ hub-side | ✅ hub-side | ❌ files, pinned identity and bundle stay on the node (§7.7) | — | — |
+| Your identity keys stay yours | ✅ | ⚠️ as the row above | ✅ the bundle they hold does not open without the pepper, and a leaked bundle key opens **that node's** bundle only | ✅ | ✅ |
+
+#### Over-claims the project refuses to make
+
+Three sentences are forbidden, each for a deliberate reason:
+
+- **"Everything is encrypted and unreadable by other parties, even the hub."**
+ A hub that ships the code can lift keys from the page regardless of protocol
+ design (**T3**). That is an artifact-level attack, not a silent directory lie,
+ and it is removed for native clients — not for browsers.
+- **"A native client makes the hub untrusted."** It converts an undetectable,
+ per-request, per-user attack into a persistent artifact that can be hashed and
+ compared. That value is realised by reproducible builds and published hashes,
+ not by the packaging format. A build signed with a key the hub operator holds
+ *relocates* trust; it does not remove it.
+- **"C4 is closed."** It is closed for an account whose identities the desktop
+ application keeps (browser access off, §3.7): nothing of them is on any node.
+ For an account with browser access the bundle is still on each node, sealed
+ under the passphrase **and** a pepper the hub holds — no operator can search it
+ offline, but an active hub can, and that is T3's adversary already. **An account
+ is only as strong as its weakest client.**
+
---
## 14. Decisions that are not revisited
@@ -3757,7 +3879,9 @@ pause and resume, the group-application framework with Chat, Files, Videos,
Music and Photos, cross-group search with source merging, per-account playlists,
casting to a Chromecast with subtitles rebased onto the relay's clock, the
operator CLI and loopback control API, the desktop client through its identity
-and download stages, account recovery, and the Windows port through packaging.
+and download stages, account recovery, the Windows port through packaging, and
+the Android client — the shell, native keys, downloads and uploads, and casting
+(§11.3).
The packages install: a machine has been taken from the built artefacts to a
running hub and node on **Ubuntu 26.04 (`.deb`), Fedora 44 (`.rpm`) and
@@ -3785,13 +3909,19 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| — | **Bitmap subtitles** (PGS, VOBSUB — about a fifth of the embedded streams). No WebVTT without OCR; they are not listed rather than listed and blank. Burn-in covers them and costs `-c:v copy`, which is what the eight-slot sizing assumes never happens |
| — | Delegation (§3.4) |
| — | Tier 3 roster attestation (§3.3) |
-| — | Android client |
+| — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and a release key (§11.3) |
| — | **Federation between two hubs.** The protocol is written and switched off in the code (§7.6); what is not built is one run between two machines |
### 15.3 Open, and why each is where it is
| Item | Status |
|---|---|
+| **The desktop cast relay drops whole segments** | `cast-relay.js` drops a fragment once 8 MB wait for a receiver, and a fragment is a segment, 5 to 10 MB at a film's bitrate: the picture freezes for its length. Its backlog is bounded in fragments only. The Android relay spools a receiver's lead to disk and bounds its backlog in bytes (§11.4); the desktop has neither |
+| **The desktop cast relay takes the first chunk for the whole header** | The node's first chunk can be the ftyp alone, the moov in the next; the receiver then gives up. Fixed in the Android relay (§11.4), not in `cast-relay.js` |
+| **A cast restart may pull far ahead** | Seen once on an emulator with a synthetic film: after the restart's reinit the node reported `duration=None`, and the page pulled most of the film at network speed. Not reproduced on a real film; the suspicion is a read-ahead budget computed without a duration |
+| **"Copy stream URL" after picking a receiver casts to that receiver** | The player keeps the last device chosen, so the copy-only path reconnects it instead of only starting the relay |
+| **The home page says "No groups yet" when the hub cannot be reached** | An unreachable hub reads as an account with no groups, rather than as an error |
+| **`meshbay-node init` says "Settings → Link Node"** | The control is on the Profile page, as QUICKSTART says |
| **C4** for accounts with browser access | Closed against operators by the pepper; **open against an active hub**, which holds the pepper and can fetch a bundle with a token it mints — the adversary T3 already concedes for browsers (§3.7) |
| **A desktop that never held an identity cannot open its recovery copy** | The application opens a node's passphrase copy, not the recovery copy: after a reset, an identity it never held is recovered from a browser (§3.6). And an identity the application mints gets a recovery copy only when the recovery key is entered on its Profile page with browser access on |
| **T3** for browser users | **Accepted permanently.** Removed for native clients, and that removal's value depends on reproducible builds |
@@ -3830,7 +3960,7 @@ superseded document kept under `docs/`, a note somebody holds elsewhere.
| Cited as | Read |
|---|---|
-| `draft-v5 §2`, `draft-v6 §4` — security claims | §2.2 |
+| `draft-v5 §2`, `draft-v6 §4` — security claims | §13.9 |
| `draft-v5 §3` — transport, NAT traversal | §5.1 |
| `draft-v5 §4`, §4.1–4.4 — handshake, transcript, channel binding, mutual auth | §5.2 |
| `draft-v5 §5.1`, `draft-v6 §2.3`, `§2.4b` — privileged operations, key activation, authorship | §5.4 |