aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/MESHBAY_DESIGN.md82
1 files changed, 73 insertions, 9 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 371163e..4dcd81c 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -420,14 +420,54 @@ being bob on his first connection:
| Safety-number comparison | Nothing — but it needs two humans reading digits at the worst moment | correct, unusable as a default |
| **One-time pairing code** | **Nothing: the code never reaches the hub** | **adopted** |
-**An invitation link is the one code that names no account**, because it goes to
-someone who may not have one yet. At the node it is a bearer code — a member of one
-group, once, never an operator, never for someone already in the group, at most
-twenty outstanding per group, cancellable (`MESHBAY_NODE_PROTOCOL.md` §8.6). Who may
-bring it is decided before the node is reached: the hub admits only the account whose
-verified address the inviter named, and the client sends the code only to the node
-the link names, once that node has signed its challenge (§5.2). The node half is
-built; the hub ticket and the client are not yet (§15.2, `docs/invite-links.md`).
+**Invitation links.** A link lets somebody who has no account yet register and land
+in the group without typing a code. The code above cannot do that: it names an
+account when it is issued, and someone who has not registered has none. So a link
+carries **two secrets with two jobs**:
+
+| Half | Secret | Checked by | Grants |
+|---|---|---|---|
+| Hub ticket | 128 random bits, stored as `sha256` | the hub, against the redeeming account's **verified address** — the blind index of the address the inviter typed | membership, i.e. the right to *reach* the node (§7.1). Nothing about keys |
+| Node code | the ordinary 40-bit code, with `kind = "link"` | the issuing node only | pinning and the group key, as for any code |
+
+```
+https://<hub>/#/invite?v=1&g=<group_id>&t=<ticket>&n=<node_pk, base64url>&c=<CODE>
+```
+
+Everything is after `#`, so no part of it is sent to the hub in a request; the page
+reads it and rewrites the address to `#/invite` at once. `g` lets the client check
+that the group the hub answers is the one the inviter meant; `n` names **the node that
+issued the code**, which is the only node holding it.
+
+At the node the code is therefore a **bearer code**, and everything else about it is
+fixed: a member of one named group, once, never an operator, not spendable by
+someone already an active member of that group, at most twenty outstanding per
+group, cancellable by a handle unrelated to the code (`MESHBAY_NODE_PROTOCOL.md`
+§8.6). `kind` is a column rather than an empty account id, so a row bound to nobody
+can never be read as bound to anybody (**AV1**).
+
+The rules the client follows, each of which a test holds:
+
+- it sends the code **only to the node the link names, and only once that node has
+ signed its challenge with that key** (§5.2). Without the signature the code went to
+ whichever peer answered signaling, and in a group hosted twice, to whichever host
+ answered first;
+- it never sends the code to the hub (unless the inviter asked the hub to mail it —
+ below), and it keeps the pending invitation in the tab's `sessionStorage` only,
+ cleared on success, refusal, expiry and sign-out;
+- joining is **one explicit click** after the invitation is shown — group, inviter —
+ never automatic, or a link would add anyone to any group without asking;
+- a link whose origin is not this hub is refused (the desktop client accepts one
+ pasted in), so a ticket is never sent to another hub.
+
+What it holds against, per the convention at the top. **A third party holding the
+link** — a messaging service that previews it, a forwarded mail — gets nothing
+without the invitee's mailbox. **An active hub** is not stopped: it verifies the
+addresses it compares, and could already be anybody. **A hijacked signaling path**
+no longer receives the code on a native client; a browser is still **T3**, exactly as
+for a typed code. What it costs: the hub learns that an owner invited an address
+(as a blind index and a masked form, until the link is used or expires). Addresses
+are compared case-insensitively and nothing more, so `+tags` and dots must match.
**Mailing the code is the inviter's choice, and it gives up the row above.**
The Members tab can ask the hub to mail the code to the invitee's address on file
@@ -439,6 +479,15 @@ 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.
+The same box sits under the link form, sharing the same preference. Ticked, the hub
+mails the link to the address the ticket is bound to — no second address, and
+**the hub writes the link itself** from its own origin, the ticket it has just
+minted, and the node key and code it is handed, each checked against its exact
+shape, so a hub-signed mail cannot be made to carry any other URL. Because that mail
+reaches an address the hub may have no relationship with, at the request of anyone
+who owns a group, it is also counted per sending account (**AV29**). A refused mail
+does not cancel the link, which is shown either way.
+
**Delegation is designed and deferred.** `invite_create` is authorised as a *role*
check against the roster rather than an equality test against the operator, and
the `delegate` role value is reserved, so a group admin who does not run the node
@@ -1752,6 +1801,20 @@ staff exempt. Public groups are the ones that cost other people something: they
appear in the directory and are brokered to strangers. The check is at creation
only, which is correct because the update endpoint refuses to change visibility.
+**An invitation link's hub half is a ticket, and it grants membership to one
+account.** `group_invite_links` holds `sha256(ticket)`, the invitee's address as
+the same blind index `users` has, and a masked form for the owner's list — never
+the address. Redeeming requires a signed-in account whose verified address has
+that index; anyone else holding the ticket is told only that it is not for this
+account, and an unknown, used, expired or cancelled ticket, or a group no longer
+active, is one uniform refusal. Creating a link says nothing about whether the
+address has an account (**M1**). At most twenty outstanding per group, as on the
+node; a lifetime clamped to thirty days; a node token may create one for its own
+operator's group (the CLI) and may not ask for mail. What the binding holds
+against, per the convention: **third parties** — a messaging service that
+previews the link, a forwarded mail — and not this hub, which verifies the
+addresses it compares and could already be anybody.
+
**Which nodes host a group is answered to its members.** For a public group that
is everyone, which is what public means; for a private one it is the membership
row and nothing else. Answering any authenticated account — as it did while only
@@ -3048,6 +3111,7 @@ had already been asked.
| **AV25** | **Which nodes host a group is answered to its members** (§7.3). Only the public case checked, so a private group told any authenticated account that knew its id which machines hosted it — and an ex-member knows that id for ever |
| **AV26** | **A sign-in lockout refuses passphrase sign-in and nothing else** (§7.7). It is keyed by username, usernames are public, and so anyone can spend somebody else's attempts. Open sessions, renewal and device sign-in are untouched and a reset code ends it, which bounds what a stranger buys to one forced sign-in. The lockout is a DoS primitive by construction; this is the ceiling on it |
| **AV27** | **A free-text third-party search is bounded per member and per node** (§6.5). `tmdb_search_req` spends the *operator's* credential, which TMDB rates and the whole group's automatic matching depends on, so one member holding a search box degrades the library for everyone. Per member and not per connection — three tabs is one person — and kept in the group context so a reconnect does not reset it. The refusal is an error, because an empty result list is what "no such film" looks like |
+| **AV29** | **An invitation link is bounded on both halves and its mail on the sender** (§3.4, §7.3). Twenty outstanding per group on the node (bearer codes) and on the hub (tickets); and because a link mail reaches an address the hub has no relationship with, at the request of anyone who owns a group, it is counted **per sending account per day** (`mail.invite_link_daily_cap`, 10), under the recipient and instance bounds and outside the recovery reserve (`invite_link` is not a recovery purpose) |
| **AV28** | **How many node keys one account may announce is bounded** (§7.2). Each is a row plus an IP-log row under a one-year retention, so an account in a loop writes a year of storage on the operator's disk having paid only for signatures. Proof of possession (**M8**) settles whose key it is and not how many. Counted only where a row is added: re-announcing a key already held keeps working at the ceiling, or a node that reached it could never refresh its address again |
### 13.6 Chat design findings
@@ -3200,7 +3264,7 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows.
| — | DLNA/UPnP casting (§11.4) |
| — | **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) |
-| — | **Invitation links** (§3.4). The node half is built — the unbound code, its signed ops and the join path; the hub ticket bound to an address and the client that reads the link are not (`docs/invite-links.md`) |
+| — | **Invitation links** (§3.4, §7.3). The node half (the unbound code, its signed ops, the join path) and the hub half (the ticket bound to a verified address, its mail) are built; the client that creates and reads a link, and the CLI's `--link`, are not |
| — | Tier 3 roster attestation (§3.3) |
| — | Android client |
| — | **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 |