| 1 | # π€ Invite redesign β short links, server-blind key delivery |
| 2 | |
| 3 | Status: Phase 1 (link + QR). Phase 2 (spoken word-code via PAKE) is parked. |
| 4 | |
| 5 | ## Goal |
| 6 | |
| 7 | Short, single-use, revocable invites with multi-device support and admin |
| 8 | re-issue β **without the server ever storing anything that could recover the |
| 9 | group key.** Same hard guarantee as today ("compromise the server, get |
| 10 | nothing"), but nicer onboarding and manageable invites. |
| 11 | |
| 12 | ## Crypto (load-bearing) |
| 13 | |
| 14 | - Invite secret `T` = 128-bit random β ~22 base64url chars, carried in the URL |
| 15 | **fragment**: `β¦/join/<label>#<T>`. The fragment is never sent to the server. |
| 16 | - The client derives two values from `T` (HKDF-SHA256, domain-separated): |
| 17 | - `id = HKDF(T, "lchat-invite-id")` β the server's lookup handle. |
| 18 | - `wrapKey = HKDF(T, "lchat-invite-wrap")` β an AES-GCM key. |
| 19 | - Minting (admin's browser, which holds the group key via the master link): |
| 20 | `wrapped = AES-GCM(groupKeyRaw, wrapKey)`. The browser POSTs only `{id, wrapped}`. |
| 21 | - **The server stores `{id, wrapped}` and never sees `T`** β it can't unwrap. |
| 22 | A stolen DB yields `{hash-like id, ciphertext}`; recovering the key needs the |
| 23 | 128-bit `T`, which never left the client. |
| 24 | |
| 25 | The path segment (`<label>`) is cosmetic (server-visible, guessable) β all |
| 26 | entropy lives in `T`. |
| 27 | |
| 28 | ## Data model (additive β old invites/tokens keep working) |
| 29 | |
| 30 | - `users(id, label, created_at)` β a person. |
| 31 | - `key_invites(id, user_id, wrapped, created_at, expires_at, used_at)` β |
| 32 | server-blind wrapped key per invite; `id` is the HKDF handle. |
| 33 | - `device_tokens(token_hash, user_id, label, created_at)` β ongoing transport |
| 34 | credential (hashed, like the admin token), one per device. |
| 35 | |
| 36 | Messages remain opaque blobs; `author_hash` (already present from message |
| 37 | editing) keeps working per device. Full "messages attributed to users" UI is |
| 38 | out of Phase 1 scope β the model just makes it possible later. |
| 39 | |
| 40 | ## Flows |
| 41 | |
| 42 | **Mint (admin):** browser generates `T`, derives `id`/`wrapKey`, wraps the group |
| 43 | key β `POST /api/admin/invites {label, id, wrapped, expires}`. Server creates the |
| 44 | user (if new) + `key_invite`. Browser shows the link `β¦/join/<label>#<T>` + a QR. |
| 45 | |
| 46 | **Join (new device):** |
| 47 | 1. Open `β¦/join/<label>#<T>` β `T` from the fragment. |
| 48 | 2. `GET /api/invite/<id>` β `wrapped`; unwrap with `wrapKey` β group key (stored |
| 49 | locally, same session shape as today: `{token, key, name}`). |
| 50 | 3. `POST /api/invite/<id>/claim` β server marks it used (single-use) and issues a |
| 51 | **device token**. Ongoing auth (messages/WebSocket/push) uses that token; `T` |
| 52 | is one-time and never sent. |
| 53 | |
| 54 | **Add another device / re-issue a lost one:** mint a fresh invite (new `T`, new |
| 55 | wrapped blob) from any device that holds the key β admin, or your own existing |
| 56 | device. QR is the clean desktopβmobile path. |
| 57 | |
| 58 | ## Transport auth (additive) |
| 59 | |
| 60 | `tokenOK(token)` accepts **either** a current non-revoked invite token (old model) |
| 61 | **or** a valid device token (new model). So existing members are unaffected and |
| 62 | new joiners use device tokens. |
| 63 | |
| 64 | ## Migration |
| 65 | |
| 66 | Fully additive β no data loss, nothing forced: |
| 67 | - Old `#k=<key>` links and existing tokens keep working. |
| 68 | - The existing group key is reused (the admin's browser wraps it) β no key |
| 69 | regeneration, no re-onboarding. |
| 70 | - Old-style links age out as people move to new invites. |
| 71 | |
| 72 | The join page handles both: fragment `k=<key>` β old flow; raw `<T>` β new flow. |
| 73 | |
| 74 | ## Phase 2 (parked): spoken word-code |
| 75 | |
| 76 | Four-ish words via **PAKE/SAS** (Magic-Wormhole style): the server relays an |
| 77 | ephemeral key exchange it can't read; the words authenticate the live exchange |
| 78 | against MITM; the key goes device-to-device. Low entropy is cryptographically |
| 79 | safe in that construction. Documented now, built later. |
| 80 | |