# 🤖 The home server

A tiny, anonymous, content-blind, passkey-gated blob store — the optional
backup half of the onboarding design
(`docs/onboarding-and-home-server.md` in the design repo). It lets a member
keep an **encrypted** list of their chat servers (and, later, credentials)
restorable on a fresh device with one passkey ceremony.

It is **not** a Matrix-style homeserver: it holds no messages, no roster, no
identity any instance recognises. It disappearing inconveniences you exactly
once — you lose the backup, not the chats.

## The additive-only invariant (non-negotiable)

**A client with no enrolment record makes no requests to any home server,
ever** — no passive pings, no "check for a backup" lookups. Enrolment is
created only by an explicit user action on this page. A web-only user who
declines never appears in any home-server log. Instances are not involved at
all; the only relationship is client ↔ home.

## Who sees what

| Party | Sees |
|---|---|
| An instance | your per-instance public key + ciphertext (unchanged — instances don't know this server exists) |
| The home server (if enrolled) | one WebAuthn credential public key per passkey, one opaque **padded** blob, device link tokens (hashed), IPs/timing |
| A stolen device link token | ciphertext reads + overwrites (the server keeps the last 5 versions; destructive ops need a fresh ceremony) |
| A compromised enrolled device | the blob — but it already holds live sessions to every instance in it, so nothing new |
| Nobody | the relationship between your identities on different instances, except your own devices |

## What it stores (per user)

- **users** — a random id. No email, no username, no display name; the
  credential is the identity.
- **credentials** — WebAuthn credential public keys (+ sign count/flags).
- **one blob** — opaque client-encrypted bytes. The plaintext is padded to a
  fixed 16 KiB *before* encryption so length leaks nothing; the last 5
  versions are retained as overwrite protection.
- **device link tokens** — bearer tokens minted after a verified ceremony,
  stored **hashed** (like the messenger's device tokens).
- **ceremony proofs** — 5-minute hashed proofs minted by a verified ceremony;
  the only credential destructive ops accept.

## Crypto (all client-side, `web/static/crypto.js`)

Passkey PRF output → HKDF → per-credential **KEK**. A random long-lived
**DEK** encrypts the blob (AES-GCM); the DEK is wrapped once per enrolled
passkey's KEK (envelope encryption — a second passkey is recovery redundancy).
The PRF output never leaves the client; the serializer strips PRF results
before anything is sent. No PRF support = no enrolment on that device;
add-device links remain the universal fallback.

## API

```
POST /api/register/begin      WebAuthn attestation options (rate-limited).
POST /api/register/finish     verify; create user + credential; mint link token
                              + ceremony proof. With a FRESH ceremony proof on
                              begin, adds a passkey to that account instead
                              (a link token is refused — see below).
POST /api/login/begin         discoverable assertion options (rate-limited).
POST /api/login/finish        verify; mint link token + ceremony proof.
GET  /api/blob                bearer link token; If-None-Match: "<version>" → 304.
PUT  /api/blob                bearer link token; If-Match required, 409 on race;
                              hard size cap.
GET  /api/devices             bearer link token; ids + timestamps only.
DELETE /api/devices/<id>      X-Ceremony-Proof (fresh ceremony) — NOT a link token.
DELETE /api/credentials/<id>  X-Ceremony-Proof; refuses to remove the last
                              passkey (that would orphan the blob).
DELETE /api/blob              X-Ceremony-Proof (fresh ceremony).
```

The asymmetry is deliberate: a link token authorises exactly blob read/write
and the devices listing — nothing else. Anything that *changes who can get in*
(adding a passkey, removing one, revoking devices, deleting the blob) requires
a live passkey ceremony — a stolen token can't lock you out, and it can't
enrol an attacker's passkey to escalate itself into ceremony power either.

## Build, run, deploy

```sh
go build ./home                    # single static binary, UI embedded
LCHAT_HOME_ORIGIN=http://localhost:1748 ./home &   # env: ADDR, LCHAT_HOME_DB
go test ./home
```

`LCHAT_HOME_ORIGIN` is the public origin (the WebAuthn RP identity) — pick the
hostname once; changing it orphans existing passkeys. In production it runs as
the `lchat-home` systemd service on the router box (unix socket, fronted by the
router): build the `home` binary, copy it up, `systemctl restart lchat-home` —
data-safe; the DB at `/data/home.db` is never touched by a redeploy.

### Optional: serving the planet-scale guide at /infrastructure/

`LCHAT_HOME_INFRASTRUCTURE_DIR=<path>` serves the operator guide
(`docs/planet-scale-guide/` in this repo) at `/infrastructure/`, read
straight from that directory on disk — unset (the default) means the route
404s. The guide's source stays in the repo; only a deploy step copies its
built files onto the box:

```sh
scp -r docs/planet-scale-guide/* root@<box>:/data/infrastructure/
# then set LCHAT_HOME_INFRASTRUCTURE_DIR=/data/infrastructure in home's
# environment and restart
```

Re-run the `scp` (it overwrites in place) whenever the guide changes; no
restart is needed for guide-only content updates since it's read from disk on
every request, not cached in the process. A restart is only needed the first
time `LCHAT_HOME_INFRASTRUCTURE_DIR` is set.
