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.md74
1 files changed, 48 insertions, 26 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 78376a3..170081f 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -99,7 +99,7 @@ opens them.
│ └─────────┘
MHP 0.1 │ signalling (SDP/ICE, <1 KB), presence, revocation push MHP
│
- ┌────┴────┐ MNP 3.0 ┌──────────┐
+ ┌────┴────┐ MNP 4.0 ┌──────────┐
│ node │◄──────── WebRTC DataChannel / QUIC ──────────►│ client │
└─────────┘ index, file chunks, streams, chat, admin └──────────┘
holds the files browser SPA or desktop
@@ -293,7 +293,7 @@ comes from the hash binding:
|---|---|
| Request TTL | 1 h, `[node] device_request_ttl_minutes` |
| Devices per account per node | 5 |
-| Attempts per connection | 5, then a node-wide lockout |
+| Attempts per connection | 5, audited on exhaustion |
| Filing a request | requires the account to have at least one pinned identity already |
`member unpin <user>` removes **every** device of an account, and is the only
@@ -489,9 +489,10 @@ The Members tab can ask the hub to mail the code to the invitee's address on fil
can join in the invitee's place. It is offered because a code that arrives on its
own is worth more to most groups than the property, and it is stated rather than
hidden: the box reads *"Send the invitation by e-mail (may land in spam)"*, is
-checked by default, and is **remembered per account** (the `invite_email`
-preference), so an operator who unticks it once is not asked to again. Unticked,
-the hub is never called and the table above holds exactly. The CLI mails nothing.
+**unticked by default** — giving the hub the code is something the inviter opts into,
+never something done unasked — and is **remembered per account** (the `invite_email`
+preference), so an operator who ticks it once is not asked to again. Unticked, the
+hub is never called and the table above holds exactly. The CLI mails nothing.
The same box sits under the link form, sharing the same preference. Ticked, and
with an address typed, the hub mails the link to that address — and
@@ -549,8 +550,10 @@ rendered as a grouped mnemonic. `recovery_key = HKDF-SHA256(R, info =
"meshbay:recovery:v1:" + username)` — HKDF and not Argon2, because `R` has 256 bits
and there is nothing to brute-force. Every time an identity bundle is written to a
node, a **second copy** is written beside it wrapped under `recovery_key`
-(`bundle_enc_recovery`, additive on the wire). `R` is a pass-through: offered in
-the registration email by default, never written to any database, never logged.
+(`bundle_enc_recovery`, additive on the wire). `R` is a pass-through: shown once
+on screen at registration and mailed with the verification code only if the person
+ticks the box for it (unticked by default), never written to any database, never
+logged.
The reset endpoints are built to leak nothing. `POST /v1/users/password/reset-request`
requires **the username and the email on file as a pair**, checked against a blind
@@ -929,7 +932,7 @@ implementations of one security check is **C6** waiting to happen.
```
client → node handshake {token, group_id, nonce_c, v, v_min}
-node authorize_token() JWT · scope · denylist · group_id · membership · hosting
+node authorize_token() JWT · aud · scope · node · denylist · group_id · membership · hosting
node → client handshake_challenge {nonce_s, node_pk, sig} sig: Ed25519 over the challenge (3.4)
── pre-proof window: bundle fetch, join ──
client → node handshake_response {proof}
@@ -956,7 +959,9 @@ buys, per the convention at the top: a client that knows which node it means to
reach can refuse to send a code anywhere else — against a hijacked signaling path
and against a second host of the same group. It proves *a* key, not the *right*
one: it helps only a client that already knows which key to expect. A wrong
-signature is refused; an absent one is an older node, discovered from its answer.
+signature is refused; an absent one leaves the key unproved until the ack, and a
+client holding a link code then does not send it. With the floor at 4.0 every
+reachable node signs, so that branch is one only a lowered floor could reach again.
**Channel binding is mandatory and an absent one is refused** — never degraded to
nonce-only, which would silently drop MitM detection:
@@ -1000,7 +1005,9 @@ for `hubFetch` and signaling, and a short-lived **MNP token** (`aud = MNP_AUD`,
from `POST /v1/nodes/mnp-token`) that carries the member's `sub`, `groups` and
`jti` and is the only thing presented in the handshake. The node binds `MNP_AUD`
when it decodes, so a session token is refused here; the hub API binds its own
-audience, so an MNP token captured by an operator is refused there. It is
+audience and requires `exp`, `sub` and a known `scope`, so an MNP token captured
+by an operator is refused there, and so is any other hub-signed token that is not a
+session (a revocation broadcast, an MHP token). It is
checked once, before the proof, so its short life never interrupts a transfer or
a film already playing — a reconnect fetches a fresh one. `meshbay_common/tokens.py`
holds the two audience strings, shared by the hub that issues and the node that
@@ -1031,10 +1038,11 @@ requester and it therefore grants nothing across accounts.
`gek_required: false` bypass (**NS8**).
**Refusals carry a code**, not only a sentence, because a client can act on a code.
-`not_a_member` in particular is usually a token issued before the person was added
-to the group — `groups` is baked in at sign-in and the hub pushes no updates — so
-the client refreshes once and retries rather than telling someone who was invited a
-minute ago that they are not a member.
+`not_a_member` means the hub did not count the account a member when it minted the
+token; since the MNP token is minted per connection from the membership the hub holds
+then, a stale `groups` claim is no longer the usual cause. The client still refreshes
+once and retries on that code before telling someone who was invited a minute ago
+that they are not a member.
### 5.3 Correlation and liveness
@@ -1279,11 +1287,13 @@ it**.
> which point it silently takes the other. **A field kept "just in case" is how
> the branches come back.**
-**The floor is not the current version, and MINOR additions are why.** It is
-`MNP_MIN_SUPPORTED` in `handshake.py`, it equals the last MAJOR, and 3.1, 3.2,
-3.3 and 3.4 have all been added above it without moving it. So a peer can be reachable and
-still not do something the current version can, and the client has to cope with
-that — **by reading the peer's own answer, never by comparing version numbers**.
+**The floor is not necessarily the current version, and MINOR additions are why.**
+It is `MNP_MIN_SUPPORTED` in `handshake.py` and it equals the last MAJOR. Today the
+two coincide at 4.0, but 3.1, 3.2, 3.3 and 3.4 were each added above the 3.0 floor
+without moving it, and the next MINOR will be added above 4.0 the same way. So a
+peer can be reachable and still not do something the current version can, and the
+client has to cope with that — **by reading the peer's own answer, never by comparing
+version numbers**.
3.2's audio tracks are the worked example: the node lists them in `stream_init`,
the client draws its selector from that list, and a node that sends no list gets no
selector. 3.3's subtitles repeat it exactly, and add the case where the list is
@@ -1813,8 +1823,10 @@ the next node on a `not_hosted` refusal (`MESHBAY_NODE_PROTOCOL.md` §6.3).
The node authenticates to the hub with an Ed25519 signature over a domain-separated
timestamped message — **no password and no auth key on a node** — and receives a
-`scope: "node"` token that is refused for group management. The operator manages
-groups from a client (**NS7**).
+`scope: "node"` token that is refused for group management and on every admin and
+moderator route, even when the account behind it holds a hub role: what a node may do
+is its operator's roster pin, and a hub role is a person's, exercised from a client.
+The operator manages groups from a client (**NS7**).
Signaling is rate-limited, SDP-size bounded, capped per user, and **the caller must
share an active group with the target node**. Otherwise any authenticated user
@@ -1950,6 +1962,16 @@ The client shows the real state, not a blanket one. **Revocation is honoured by
nodes** and the denylist survives a restart (**H4**); signaling refuses a group that
is not active.
+**Revocation has one door, and it broadcasts.** Only an administrator revokes, and
+only through `POST /v1/admin/revoke`, which signs the revocation and pushes it to every
+connected node — the same signed broadcast an administrator's account deletion sends
+(§7.7). The user and group PATCH handlers refuse `revoked` outright, because a
+status written there reached no node and behaved as a suspension while claiming to be
+a revocation. Moving a group or an account *out* of `revoked` is an administrator's
+call too, and it changes the hub row only: the nodes keep enforcing the revocation
+they received, so the hub and the nodes then disagree until each operator clears it
+(`denylist clear`). That is why the table says "no".
+
**Moderator is not administrator.** The user-patch handler is split by field: a
moderator may act on the fields moderation needs and may not write `role`.
@@ -2067,8 +2089,8 @@ The rules that make this safe:
attempts than the limit. A request that checked no passphrase gives its attempt
back.
- **Every path that checks the passphrase counts on the same row**: sign-in,
- passphrase change and account deletion. A right passphrase clears it; failures
- older than the window age out.
+ passphrase change, changing the e-mail address on file and account deletion. A
+ right passphrase clears it; failures older than the window age out.
- **A lockout refuses passphrase sign-in and nothing else.** Open sessions, token
renewal and device sign-in continue, and a reset code sent to the address on
file clears it — so a stranger who locks a public username costs its owner at
@@ -2952,7 +2974,7 @@ The bulk of the codebase is portable because the portability rules in §10 were
treated as correctness from the start. What the port needed is registered as
**W1–W9** (§13.7) and is done; packaging is built and awaits a clean-machine run.
-Two Windows-specific design points worth stating here:
+Three Windows-specific design points worth stating here:
- **The node runs in one of three modes, chosen at install and switchable
afterwards** from the Node page: only while the application is open (it starts
@@ -3090,7 +3112,7 @@ be understood, not so the incident can be retold.
| **NS4** | **Operator authority comes from the node's roster and from nowhere else.** No auto-pin from the keystore, no resolution through the hub, no config key — a config naming one is warned about and never obeyed (§3.4, §6.1) |
| **NS5** | The proof is **bound to the transport channel** (DTLS fingerprints / certificate hash), so a signaling relay that substitutes its own cannot produce it (§5.2) |
| **NS6** | **`sender_id` is enforced from the authenticated session, never the wire.** It is what the store keys on; it is not what authenticates a message — the device signature is (§4.5) |
-| **NS7** | The node authenticates to the hub with **Ed25519 and no password**, and its token's scope is refused for group management (§7.2) |
+| **NS7** | The node authenticates to the hub with **Ed25519 and no password**, and its token's scope is refused for group management and on the admin and moderator API (§7.2) |
| **NS8** | **The node refuses connections when it holds no group key.** There is no bypass switch |
### 13.3 Second review (code review) — the default numbering
@@ -3274,7 +3296,7 @@ had already been asked.
|---|---|
| **W1** | Platform directories: no hardcoded XDG paths |
| **W2** | Signal handling is platform-guarded |
-| **W3** | Daemon lifecycle: a per-user startup launcher by default, a scheduled-task service mode offered, switchable after install (§11.2) |
+| **W3** | Daemon lifecycle: three modes — only while the application is open, at sign-in, or as a boot-time scheduled task — chosen at install and switchable after; one start/stop implementation, the CLI's (§11.2) |
| **W4** | Packaging: one per-user installer carrying client and node, with media tools bundled |
| **W5** | File permission calls are skipped where they have no meaning |
| **W6** | Media-tool discovery fails at startup with a stated reason rather than at first use |