backroom / relay / docs / security
Security model
The relay never sees plaintext and holds no keys. Confidentiality is not its job, and it is structurally unable to provide it: that inability is the design, not a limitation of it.
The dumb-pipe principle
There is no key material anywhere in the relay codebase, and the relay never parses, transforms, decrypts, or logs a frame's payload. Its only jobs are:
- Routing. Match the two peers presenting the same room id and forward binary frames between them, verbatim.
- Availability. Stay up so the two ends can find each other.
Any "improvement" that inspects or logs frame contents would move confidentiality off the endpoints and onto one box in a data centre, which is exactly what the design refuses. The endpoints seal everything before it reaches the wire and open it only after it arrives; the relay carries ciphertext or it carries nothing.
The access model has two parts
End-to-end encryption on the endpoints
Both devices derive an AES-256-GCM key from a secret exchanged during offline pairing (a QR code scanned in person). Every frame is sealed under that key with a fresh nonce and an authentication tag, so the relay cannot read a frame and cannot forge one that the receiver would accept. The derivation and frame format are specified in the protocol.
The room id is an unguessable capability
The room id is derived one-way (HKDF-SHA256) from the same pairing secret, giving 144 bits of derived material. Whoever holds it can connect to the room, so it is treated as a secret everywhere: validated for entropy floor on arrival, never logged in full, and never enumerable through any endpoint.
There is no relay-side auth secret, deliberately. A shared
relay secret would have to be distributed to every client and would become
a single revocation point, which is strictly worse than an unguessable
per-pair capability. The relay admits at most two
connections per room, so even a party who somehow learns a room id cannot
join an already-established pair: the third connection is refused with
close code 1008.
Threat model: what each attacker actually gets
| Adversary | What they get | What they do not get |
|---|---|---|
| A network observer between a device and the relay | Nothing. The hop is TLS (wss://), and inside it the payload is sealed again end to end. |
Room ids, frame contents, keys. |
| Full compromise of the relay host | Room ids, frame sizes and timing, source IPs, the ability to drop or delay traffic. | Plaintext, keys, or the ability to forge a frame the endpoints would accept. A forged frame does not decrypt and is dropped before anything acts on it. |
| Someone who learns a room id | The ability to occupy an empty seat in that room and receive ciphertext they cannot open. | Entry to an established pair (third connection refused), or anything readable: without the key they hold noise. |
| An attacker replaying captured ciphertext | Nothing durable. The endpoint protocol binds direction and freshness: a frame is refused when reflected at its sender, and expires 120 seconds after it was sent. | Replayed commands. See freshness and direction. |
| A flood of connections or giant frames | Throttled or refused: per-IP token buckets, a global room cap, a frame size ceiling, and small send buffers. | Memory exhaustion or a relay that silently degrades for paired users. |
Abuse resistance
A public endpoint with no accounts still has to survive the open internet, so every resource is bounded:
- Frame size is capped (1 MiB by default). An oversized frame closes the connection with
1009. - Rooms are capped globally (4096 by default), so a flood of distinct room ids cannot grow the registry without bound.
- New connections are rate limited per source IP with a token bucket (1 per second sustained, burst of 20, by default). Idle buckets are evicted so a stream of distinct spoofed sources cannot grow that map either.
- Send buffers are small (32 frames). A peer that cannot absorb its traffic is torn down rather than buffered for; the relay is a pipe, not a mailbox.
- Dead peers are reaped by pings within about 40 seconds, freeing their room seat.
Behind the CDN, nginx restores real client addresses from the edge's forwarding header, over a validated set of edge ranges, so per-IP limits apply to actual clients rather than to a handful of edge proxies. See operations for how that is configured unspoofably.
Logging policy
The relay logs connection lifecycle events only: joins, leaves, refusals, and rate-limit hits. A room id appears in logs only as a six-character prefix, never in full, because the full id is the capability to reach a pair. Frame payloads are never logged anywhere, including at debug level, because no code path has them in a loggable form in the first place.
Where TLS lives
In production the relay binary speaks plain WebSocket on localhost and nginx terminates TLS in front of it, with a real certificate. The binary doing no TLS is a simplification, not a gap: the plaintext hop never leaves the machine, and there is one fewer certificate store to get wrong. The payload inside is end-to-end sealed regardless, so even the TLS layer is defence in depth for metadata rather than the confidentiality boundary.