05 Security · Metadata
The envelope does not name you.
Content encryption is table stakes. The harder fight is the envelope — who, to whom, how much, when. Here is exactly how far we take it, and where it stops.
What the envelope actually says.
Sealed sender — wire version 1
This is the envelope the relay actually carries. The sender's Profile ID and certificate sit inside an ML-KEM-1024-sealed body only the recipient can open; the address on the outside is the recipient's Profile ID, because the relay has to deliver it. A second envelope that blinds the recipient too is specified and implemented, and refused by default until it can spool for a phone that is offline.
- recipient_profile_id The address. The recipient’s public Profile ID, in the clear, and also the AEAD’s authenticated data — so a relay cannot re-address the envelope to someone else. Plaintext
- outer_kem_ct ML-KEM-1024 ciphertext to the recipient’s published key. 1,568 bytes, always. Opaque
- outer_nonce 12 random bytes. Opaque
- outer_ciphertext ChaCha20-Poly1305 over the sealed body: the sender’s Profile ID, the operator-signed sender certificate, and the Q-Ratchet frame. Ciphertext
- version, conversation_id, idempotency_key, message_type_hint Routing and de-duplication fields, in the clear. Plaintext
Four more columns sit on the stored row and are disclosed here because the relay writes them: messages.sender_device_id (the sending device slot, for multi-device routing), messages.transport (which lane carried the row), conversations.settings (the group settings blob, sealed by the client), and conversations.group_avatar_url (the avatar object key, in the clear until the avatar seal turns on). The full column census is on the ledger page.
Size stops being a fingerprint.
Every message payload — and every receipt and typing frame — snaps to one of nine sizes, 256 B to 64 KiB, at the WebSocket transport before it leaves the device; that ladder sits outside the ratchet's authenticated frame, so the relay and the stored row still see exact ciphertext lengths, and moving it inside the seal is protocol version 5. Voice notes snap to EVM-Pad buckets inside their seal. Photo, video and document seals round up to a 64 KiB boundary — a coarse grid, not a fixed bucket set. These are the constants in code, not an illustration.
| Ladder | Buckets | Range | Sized for |
|---|---|---|---|
| Text (transport) | 9 | 256 B → 64 KiB | every content payload and every typing and receipt frame — ios/Qeryx/Services/WebSocketService.swift MessageBucket |
| Voice note | EVM-Pad buckets | inside the seal | AAC at about 8 KB/s — roughly 2 s to 4 minutes |
| Media file | — | next 64 KiB boundary | photo, video and document seals round up before one ChaCha20-Poly1305 box |
| Shadow ID | 9 | 64 B → 16 KiB | the compartment ladder — crypto-core/src/q_pad_ladder.rs; in development |
The fine print on traffic shape
Padding hides length within a bucket; it does not hide that a message was sent, and timing correlation by a global passive observer remains an open problem for every store-and-forward messenger, including this one. QLEAP’s three-hop routing will raise the cost of linking endpoints; it is in development, and it would not make the problem vanish when it lands. Until then the pad ladder and the sealed envelope above carry this alone.
The storage contract, present tense.
| Data | By design | Roadmap |
|---|---|---|
| Delivered messages | deleted within about 25 minutes of the delivery receipt; a group message sealed to each member, or one read with Stealth Mode on or read receipts off, waits for the 30-day horizon | unchanged |
| Undelivered queue | 30 days; the purge sweep runs every 15 minutes | tiered ladder: 24 h / 4 h / zero-storage |
| Photos, videos, files and group voice notes | stored apart from their messages, encrypted, with an upload record naming the uploader: up to 45 days after upload, delivered or not | — |
| Envelope commitments | expire with their hour window | unchanged |
The 30-day figure1 is the relay's configured default — operator-tunable between 1 and 90 days, and published by the relay itself at /api/v1/transparency/retention. The tiered ladder is roadmap, and it will be written here in the present tense only when the purge code ships.
The hard questions.
Can the relay build my contact graph?
It already holds part of one. Group names and descriptions and all content are end-to-end encrypted; the group avatar, the admin and owner identifiers, and the roster of accounts each conversation delivers to are held by the relay for routing. Sender identifiers are stripped from the stored message row (sealed sender), though the relay authenticates the sender at send time and keeps a per-direction pair counter for up to 180 days. Sealed sender covers text-class messages and small payloads: an image, and any voice or video frame over the envelope budget, is delivered on a lane that does name the sender to the relay for as long as the row lives. Undelivered messages queue with routing metadata under a published retention ladder. Undelivered messages queue with routing metadata under a published retention ladder. What it does hold is the roster each conversation delivers to — that is the routing surface, it is published field by field, and a delivery capability that removes it is designed and not yet built.