The relay (the server that carries sealed messages between phones) cannot be blinded completely. To decide whether to make a phone ring like a call or nudge it silently in the background, it has to read something about the message. The whole game is making that "something" as coarse and uninformative as physics allows, and then proving the relay can't cheat to learn more.
Sealed Sender and Sender Unlinkability hid the sender. This one is about the header: the small set of fields on the outside of each message that tell the relay where to send it and how to handle it. The discipline is keeping that small set from becoming a metadata channel: a way to learn facts about your messages (who, when, what kind) without ever reading their content.
An incoming voice call must make the phone ring (full-screen, override silent mode, wake the CPU hard) within a second, or the feature is broken.
A "someone reacted to your post" should at most nudge quietly, and often should not wake the phone at all. A background sync between your own devices should be invisible.
Those are wildly different handling decisions, and the relay is the thing that has to make them.
So the relay must read how to handle each message.
The danger is that "how to handle" usually correlates tightly with "what kind of message this is," and the kind of message is exactly the metadata we are trying to hide.
If the header said type = reaction, or type = typing_indicator, or type = call_offer, the
relay would learn the texture of every conversation: who reacts to whom, who is typing, when
calls happen.
The design splits the header in two.
Inside the seal (the encrypted part of the envelope, which only the recipient's phone can
open) rides the true type, one of a couple of dozen fine-grained kinds: message,
reaction, post, call_offer, sync_request, sync_delta, device_list, huddle
turns (one speaker's turn in a live group session), receipts, and so on. Only Bob's phone,
after decrypting, ever sees this. It is what the app actually acts on.
On the outside, where the relay reads it, rides a single coarse field: the WakeClass, with just six values:
| WakeClass | Handling | Example inner types (hidden) |
|---|---|---|
voip |
ring hard, never suppressed | a call offer |
alert |
user-visible notification | a direct message |
sync |
visible banner; must be able to relaunch a force-quit sibling | a sync request |
ack |
tiny, non-waking receipt | a delivery/read receipt |
call_tail |
never wakes (call teardown) | hang-up housekeeping |
silent |
never wakes on its own | almost everything else |
sync is the one that looks out of place, and it is worth a sentence. It is loud on purpose.
A silent push notification cannot relaunch a sibling (another device on your own account) once
the user has force-quit the app there. And relaunching one is exactly what a newly added device
needs, so that it can be served its first snapshot of your data. This class fires during
onboarding and not in steady state, so making it visible costs no ongoing notification churn.
The last row is where the privacy lives.
The mapping from the fine types down to six coarse classes is a fixed, many-to-one
projection (a lookup table that sends every fine type to exactly one coarse class, with
no randomness and no exceptions), built so that most types collapse into silent.
Posts, reactions, huddle turns, sync deltas, device-list changes: all of it lands on the same
coarse value, indistinguishable from one another to the relay.
The relay learns "this is a background thing, do not wake for it" and nothing finer.
This is exactly the pattern modern protocols converged on. In MLS (Messaging Layer
Security, the group-messaging standard; RFC 9420),
the encrypted PrivateMessage hides the content type from the server. In TLS (the
encryption protocol behind the padlock in your browser), Encrypted Client Hello hides
which site you're connecting to behind a shared front. The relay's WakeClass is the same idea:
reveal the minimum the transport needs to function, seal the rest.
The timestamp gets the same treatment. The created_at the relay sees is rounded to a
5-minute bucket, so the relay can enforce message expiry without learning the precise
instant you hit send. That blurs the fine timing that traffic analysis feeds on: the craft of
inferring who is talking to whom, and about what, from nothing but the sizes and timings of
sealed messages, without ever opening one.
Splitting the header raises an immediate question. The WakeClass is outside the seal, so
the relay (or anyone who tampers with the envelope in flight) can change it. What stops
a malicious relay from rewriting silent to voip (the ring-hard class for calls) to
make your phone ring at 3am? Or, more insidiously, from using the WakeClass as a covert
channel: a field that is supposed to be routing plumbing, quietly repurposed to signal
something to whoever is watching?
Here is the elegant part: the recipient does not trust the outer WakeClass; it re-derives it. After decrypting, Bob's phone reads the true inner type, computes what the WakeClass should be (the same fixed projection), and checks it against the outer value the relay used. If they disagree, the message is rejected outright.
Follow the consequences.
The outer WakeClass can only ever affect how the phone was woken. It is a physical, best-effort hint. It can never affect what the message is, or whether it is accepted, because that is governed by the sealed inner type Bob verifies.
So the worst a malicious relay can do by lying about the class is degrade the notification: fail to ring for a real call, or ring for something that should not have rung.
That is a denial-of-service (an attack that breaks a feature rather than stealing a secret), and a visible one. It is not a privacy break.
This "the untrusted field is only a hint, and the trusted field is re-derived and checked" shape is worth internalizing: it recurs whenever you must expose a routing hint to an adversary without letting the hint become authority.
Claims like "two different message types are indistinguishable on the wire" are exactly the kind of thing that is easy to assert and hard to actually have.
So this one is not just asserted. It is proved by machine.
The property is stated and checked in the formal corpus, the project's library of machine-checked proofs: two messages sharing a WakeClass are indistinguishable to the relay, meaning the envelope leaks nothing beyond the coarse class. It is checked in two different styles of model. A symbolic model treats encryption as a perfect black box and checks the protocol's logic for flaws. A computational model drops that idealization and bounds what any efficient attacker could actually compute. The claim is checked in both.
The relay is not blind, and we don't pretend it is. From the envelope it still sees:
to): the bytes have to go somewhere, so this is
unavoidable. But it is a 32-byte value both ends derive from their shared channel key and the
current hour. It names neither a person nor a key, and it is a different address next hour,voip is distinguishable from a
silent. A ringing call is inherently different from a background sync, and no amount of
cleverness hides "a call is happening" from the machine that has to make the phone ring.
What is hidden is everything finer: which of the many silent things this is, whether a
message is a reaction or a document, the exact type.So the residual is: the relay knows a message of some coarse urgency class landed on this address in this five-minute window. That is the irreducible minimum for a transport that must route and must wake, and every finer fact about the message is sealed, verified end to end, and provably absent from what the relay reads.
Next we take away the last thing you might assume a relay has: a place to keep the message. A Relay That Holds Nothing.
PrivateMessage hides
content type from the delivery service; the direct analogue of WakeClass.