diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 260 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 20 |
2 files changed, 209 insertions, 71 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 | diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index d631804..1790475 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -392,9 +392,13 @@ resume. ### Casting to a TV -**Desktop application only.** A film playing in the application can be sent to a -cast-capable TV or dongle on the same network: *Cast to device* in the player, -pick one from the list. Devices appear as they answer, usually within a couple +**The desktop and Android applications.** A film playing in the application can +be sent to a cast-capable TV or dongle on the same network: *Cast to device* in +the player, pick one from the list. On a phone, the film goes on playing silently +on the phone while the TV shows it, and the screen can be turned off. While a +TV plays the film, the player becomes its remote: the position shown is the +TV's, with play/pause, 30-second jumps and a slider to go anywhere in the film. +Devices appear as they answer, usually within a couple of seconds; the search carries on for a few more, for a TV that is still waking up. @@ -967,12 +971,16 @@ Better to know now than to go looking for it: is nothing to check a download against and no updates through your distribution. Until that ships, take them from the download page and from nowhere else. -- **There is no Android client.** A phone browser works. +- **The Android application is not released yet.** It works — groups, chat, + films, downloads, casting — but is built and installed by hand and signed + with a development key, so there is no store page and no update channel. The + back button and the lock screen do not drive it yet. A phone browser works + too. - **A node cannot be hosted on Android**, and is not planned to be. - **Hubs do not talk to each other yet.** Everyone in a group needs an account on the same hub. -- **Casting reaches Chromecast devices** from the desktop application. Support - for other TV protocols is designed but not built. +- **Casting reaches Chromecast devices** from the desktop and Android + applications. Support for other TV protocols is designed but not built. - **Some subtitle tracks cannot be shown** — the ones stored as images rather than text, roughly one embedded track in five. Displaying them would need text recognition. |