Architecture
Three components and one spec. Each piece does one job, can be swapped out, and doesn’t know more than it has to about the others.
The components
- trueseal-noise — Noise Protocol channels (XX + NK). Knows nothing about sync.
- trueseal-sync — the sync engine. Identity, pairing, group manifest, addressed encryption, outbox.
- trueseal-relay — a zero-knowledge blob router. Holds ciphertext for offline recipients and delivers it when they come back. Self-hostable.
- trueseal-protocol — the wire spec everyone speaks. Just docs, no library.
Who owns what
| Concern | Owner |
|---|---|
| Noise Protocol handshakes | trueseal-noise |
| Encrypted transport channels | trueseal-noise |
| Device identity (keypair generation, persistence) | trueseal-sync |
| Pairing ceremony | trueseal-sync |
| Group manifest (membership, versioning) | trueseal-sync |
| Envelope construction and addressed encryption | trueseal-sync |
| Operation log and outbox replay | trueseal-sync |
| Blob routing and deferred delivery | trueseal-relay |
| Blob retention and TTL reaping | trueseal-relay |
| Wire protocol specification | trueseal-protocol |
What the relay actually sees
Two things, and that’s it:
- Which noise keys have an active Receive Session. Unavoidable. If you want online devices to get their messages immediately, the relay has to know who’s online.
- The recipient public key on each envelope. It needs that to route the blob to the right inbox.
It doesn’t see who sent a blob (push sessions use a throwaway keypair, so the sender is unlinkable), it has no concept of groups, and it can’t read payloads because everything is encrypted to the recipient before it ever hits the wire.
Compromise the relay completely — grab the binary, dump the database, sniff every packet — and you walk away with a list of public keys that received blobs. Nothing else.
Sending a blob
When a device calls send():
-
Read the Group Manifest. trueseal-sync pulls the current list of members and their noise + signing public keys.
-
Build one Envelope per recipient. For each member, trueseal-sync does an ephemeral X25519 key agreement against the recipient’s static public key, then encrypts the payload with ChaCha20-Poly1305. The sender’s
author_pub(Ed25519 signing key) gets prepended inside the plaintext, so it’s invisible to the relay. Then the envelope is signed oversequence || parent_hashes || recipient_pub || ciphertextwith the sender’s Ed25519 key.The result: a blob the relay can route (it sees
recipient_pub) but can’t read or tamper with undetected. -
Open an anonymous Push Session. A fresh ephemeral X25519 keypair is generated just for this push, and Noise NK is used to connect to the relay. NK authenticates the relay (the device verifies the relay’s static public key) but transmits no stable client identity. From the relay’s point of view it’s an unlinkable peer, with no way to connect this push to any device or Receive Session.
-
Send all N envelopes over the same Push Session, then close it.
-
Relay stores or forwards. Each envelope goes into the recipient’s Inbox. If that recipient has an active Receive Session, it’s handed over immediately. Otherwise it sits there until they reconnect.
-
Outbox tracks delivery. Every envelope is marked undelivered in the local Operation Log until the relay confirms receipt. If the sender drops offline before that confirmation, the envelope rides along on the next reconnect.
Receiving a blob
When a device connects to the relay:
-
Open a Receive Session. A Noise XX handshake using the device’s stable noise keypair. Both sides authenticate each other. The relay marks the device online and starts forwarding blobs as they come in.
-
Drain the inbox. Any blobs being held for the device get delivered over the session. They’re not deleted yet — deletion waits for the ack.
-
Send DeliverAck. Once the client has durably written each blob, it sends a DeliverAck with the blob’s ID and the relay deletes it. If the session drops before the ack lands, the blob survives and gets re-delivered next time. Clients have to be ready for duplicates.
-
Decrypt and verify. For each envelope, trueseal-sync decrypts with the device’s private key, peels
author_puboff the first 32 bytes of plaintext, verifies the signature against it, and finally checksauthor_pubagainst the current Group Manifest. Anything from a device not in the manifest gets dropped on the floor. -
Fire the callback. The plaintext lands in the caller’s
on_messagehandler. The caller doesn’t touch keys, sessions, or signatures at any point.
Pairing
Before two devices can sync anything, they need each other’s public keys. That’s pairing:
-
The initiating device generates a Pairing Token — its noise public key and its Ed25519 signing public key, encoded opaquely. It gets passed out-of-band (QR code, AirDrop, link, whatever fits).
-
The joining device reads the token and sends a
Pairmessage: an envelope addressed to the initiator, encrypted with the initiator’s public key, containing the joiner’s own keys. -
The initiator gets the
Pairmessage, fireson_member_request, and the caller callsaccept_member()to admit the new device. -
A fresh Group Manifest listing both members is signed by the accepting device and pushed to everyone in the group.
The relay’s role here is just step 2: it sees one blob addressed to the initiator and routes it. It has no idea that blob happens to be a pairing request.