eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– The home server
2
3A tiny, anonymous, content-blind, passkey-gated blob store β€” the optional
4backup half of the onboarding design
5(`docs/onboarding-and-home-server.md` in the design repo). It lets a member
6keep an **encrypted** list of their chat servers (and, later, credentials)
7restorable on a fresh device with one passkey ceremony.
8
9It is **not** a Matrix-style homeserver: it holds no messages, no roster, no
10identity any instance recognises. It disappearing inconveniences you exactly
11once β€” you lose the backup, not the chats.
12
13## The additive-only invariant (non-negotiable)
14
15**A client with no enrolment record makes no requests to any home server,
16ever** β€” no passive pings, no "check for a backup" lookups. Enrolment is
17created only by an explicit user action on this page. A web-only user who
18declines never appears in any home-server log. Instances are not involved at
19all; the only relationship is client ↔ home.
20
21## Who sees what
22
23| Party | Sees |
24|---|---|
25| An instance | your per-instance public key + ciphertext (unchanged β€” instances don't know this server exists) |
26| The home server (if enrolled) | one WebAuthn credential public key per passkey, one opaque **padded** blob, device link tokens (hashed), IPs/timing |
27| A stolen device link token | ciphertext reads + overwrites (the server keeps the last 5 versions; destructive ops need a fresh ceremony) |
28| A compromised enrolled device | the blob β€” but it already holds live sessions to every instance in it, so nothing new |
29| Nobody | the relationship between your identities on different instances, except your own devices |
30
31## What it stores (per user)
32
33- **users** β€” a random id. No email, no username, no display name; the
34 credential is the identity.
35- **credentials** β€” WebAuthn credential public keys (+ sign count/flags).
36- **one blob** β€” opaque client-encrypted bytes. The plaintext is padded to a
37 fixed 16 KiB *before* encryption so length leaks nothing; the last 5
38 versions are retained as overwrite protection.
39- **device link tokens** β€” bearer tokens minted after a verified ceremony,
40 stored **hashed** (like the messenger's device tokens).
41- **ceremony proofs** β€” 5-minute hashed proofs minted by a verified ceremony;
42 the only credential destructive ops accept.
43
44## Crypto (all client-side, `web/static/crypto.js`)
45
46Passkey PRF output β†’ HKDF β†’ per-credential **KEK**. A random long-lived
47**DEK** encrypts the blob (AES-GCM); the DEK is wrapped once per enrolled
48passkey's KEK (envelope encryption β€” a second passkey is recovery redundancy).
49The PRF output never leaves the client; the serializer strips PRF results
50before anything is sent. No PRF support = no enrolment on that device;
51add-device links remain the universal fallback.
52
53## API
54
55```
56POST /api/register/begin WebAuthn attestation options (rate-limited).
57POST /api/register/finish verify; create user + credential; mint link token
58 + ceremony proof. With a FRESH ceremony proof on
59 begin, adds a passkey to that account instead
60 (a link token is refused β€” see below).
61POST /api/login/begin discoverable assertion options (rate-limited).
62POST /api/login/finish verify; mint link token + ceremony proof.
63GET /api/blob bearer link token; If-None-Match: "<version>" β†’ 304.
64PUT /api/blob bearer link token; If-Match required, 409 on race;
65 hard size cap.
66GET /api/devices bearer link token; ids + timestamps only.
67DELETE /api/devices/<id> X-Ceremony-Proof (fresh ceremony) β€” NOT a link token.
68DELETE /api/credentials/<id> X-Ceremony-Proof; refuses to remove the last
69 passkey (that would orphan the blob).
70DELETE /api/blob X-Ceremony-Proof (fresh ceremony).
71```
72
73The asymmetry is deliberate: a link token authorises exactly blob read/write
74and the devices listing β€” nothing else. Anything that *changes who can get in*
75(adding a passkey, removing one, revoking devices, deleting the blob) requires
76a live passkey ceremony β€” a stolen token can't lock you out, and it can't
77enrol an attacker's passkey to escalate itself into ceremony power either.
78
79## Build, run, deploy
80
81```sh
82go build ./home # single static binary, UI embedded
83LCHAT_HOME_ORIGIN=http://localhost:1748 ./home & # env: ADDR, LCHAT_HOME_DB
84go test ./home
85```
86
87`LCHAT_HOME_ORIGIN` is the public origin (the WebAuthn RP identity) β€” pick the
88hostname once; changing it orphans existing passkeys. In production it runs as
89the `lchat-home` systemd service on the router box (unix socket, fronted by the
90router): build the `home` binary, copy it up, `systemctl restart lchat-home` β€”
91data-safe; the DB at `/data/home.db` is never touched by a redeploy.
92
93### Optional: serving the planet-scale guide at /infrastructure/
94
95`LCHAT_HOME_INFRASTRUCTURE_DIR=<path>` serves the operator guide
96(`docs/planet-scale-guide/` in this repo) at `/infrastructure/`, read
97straight from that directory on disk β€” unset (the default) means the route
98404s. The guide's source stays in the repo; only a deploy step copies its
99built files onto the box:
100
101```sh
102scp -r docs/planet-scale-guide/* root@<box>:/data/infrastructure/
103# then set LCHAT_HOME_INFRASTRUCTURE_DIR=/data/infrastructure in home's
104# environment and restart
105```
106
107Re-run the `scp` (it overwrites in place) whenever the guide changes; no
108restart is needed for guide-only content updates since it's read from disk on
109every request, not cached in the process. A restart is only needed the first
110time `LCHAT_HOME_INFRASTRUCTURE_DIR` is set.
111