Protocol
The TrueSeal protocol is what a device and a relay must agree on, byte for byte, for a message to get from one device to another. It has no library of its own. trueseal-sync implements the device side and trueseal-relay the relay side, and the Sync-level test vectors pin down the bytes both must produce.
This page describes the protocol as the Developer Preview decides it. Where today’s code still differs, each Reference page lists the gap at its end.
Two layers, two versions
The protocol has two layers, and each carries its own version number.
| Layer | Between | What it covers | Version |
|---|---|---|---|
| Transport | Device and relay | The Noise handshake, the frames inside it, acks, refusals and heartbeats | Transport Version |
| End-to-end | Device and device | The sealed Envelope, addressed encryption, signatures and the Group Manifest | End-to-End Version |
Both versions start at 1, and 0 is reserved. In the preview the Transport Version is 1 and the End-to-End Version is 1. Each version is bound into the cryptography as well as sent as a number. The Transport Version goes into the Noise prologue, and the End-to-End Version goes into the key derivation, the AEAD associated data and every signing input.
There is no negotiation. A component speaks exactly one version of each layer and rejects anything else:
- A relay that gets a Transport Version it doesn’t speak replies with a plaintext
unsupported min..maxand closes the connection. The SDK reports it asconnection=relayVersionUnsupported{min, max}. - A device that receives a message in a newer End-to-End Version leaves it unacknowledged on the relay, so it can be handled after the app upgrades. It holds up to 100 Blobs per device, then acknowledges and drops the oldest.
- A device that receives a message in an older End-to-End Version, or one it can’t decode, acknowledges and drops it.
The device reports each case to its own app, and nothing is sent back to the device that sent the message. A version bump means every device upgrades together, and a Transport Version bump means the relay upgrades with them; Upgrading covers what that does to queued messages.
The relay is always in the path
Devices never connect to each other. Each device opens two kinds of outbound connection to the relay, on two TCP ports, and both run the Noise Protocol Framework with the cipher suite 25519_ChaChaPoly_BLAKE2s. The device checks the relay’s static public key on both, against the key in its Relay Address.
| Push Session | Receive Session | |
|---|---|---|
| Purpose | Hand messages to the relay | Take messages from the device’s Inbox |
| Noise pattern | NK | XX |
| Device key sent | None. A fresh ephemeral key per session. | The device’s static X25519 key |
| Default port | 7701 | 7700 |
| Length | Short. The relay closes it after 30 seconds. | Open as long as the device is online |
Push Session
A device opens a Push Session when its outbox has messages for the relay.
- The device connects to the push port and runs the NK handshake. It has to finish within 10 seconds.
- The device sends one
Pushframe per message. Each frame names the recipient’s public key. - For each
Push, the relay writes the message to the recipient’s Inbox on disk, then repliesAck. If it refuses the message, it repliesErrorwith a reason code and stores nothing. - The device deletes each acknowledged message from its outbox and closes the session.
The relay closes any Push Session still open 30 seconds after it accepted the connection. A message the relay didn’t acknowledge, and didn’t refuse for good, stays in the outbox and goes out again in a later Push Session.
The NK handshake gives the relay no key that belongs to the sending device. The relay can still often tell which device pushed from the timing of the connection, and the server it runs on also sees its IP address. The How it works page lists what the relay sees.
Receive Session
A device keeps one Receive Session open while it’s online.
- The device connects to the receive port and runs the XX handshake within 10 seconds. The relay learns the device’s static public key, which is also the address of its Inbox.
- The relay sends each message in the Inbox as a
Deliverframe, in the order they arrived, then new messages as they arrive. The device takes nothing from its Inbox before the app has registered its message handler. - The device handles each message, then replies
DeliverAck. The relay deletes the message only then, and only from that device’s own Inbox. - The device sends a
Heartbeatevery 25 seconds and the relay echoes it. The relay closes a Receive Session when it has received nothing from the device for 75 seconds.
The relay sends each message once per Receive Session. A message without a DeliverAck stays in the Inbox and is delivered again on the next Receive Session. The relay allows 4 Receive Sessions per device key at once.
Sending and receiving gives the delivery promise this adds up to. Limits and quotas lists the relay’s limits and the refusal each one causes.
Where today’s code differs
The relay doesn’t yet enforce the handshake and session deadlines, the 75-second idle limit or the cap on Receive Sessions per key (trueseal-relay#43). Neither layer carries a version yet (trueseal-sync#93, trueseal-relay#42). Wire Format lists the differences in the frames.
What the protocol leaves to the app
The protocol moves opaque bytes between the members of a group. What the bytes mean, how two conflicting changes merge, and what a new member should receive from the past stay with the app.