~/potatohd.orgRU
← All posts
Aug 13, 2026·10 min read

Umbra: designing a messenger so the server knows nothing

E2EEGolibp2psystem design

WhatsApp encrypts messages end to end — that's true, and it's a lot. But encrypting content solves exactly one problem. The server still knows which number writes to which, at what time, how often, and from which devices. The metadata survives, because the whole architecture rests on a phone-bound account and a mailbox the operator runs.

Umbra is an attempt to answer a different question: what does a system need to look like so the middleman can't learn even that? What follows is the architectural fallout of that requirement. It reaches much further than swapping a crypto library.

#What the middleman sees

In the classic design the server is the address book and the post office at once. It knows accounts, it knows the edges between them, and it holds a queue for whoever is offline. Even with the envelopes sealed, the delivery log alone is a rich dataset.

Umbra inverts the unit of addressing: you address not an account but a mailbox, whose identifier is derived from the account root key. A custodian accepts objects into it — versioned, content-addressed, opaque blobs — and knows neither sender nor recipient in any human sense.

The sender device encrypts an object and hands it to a custodian that sees only opaque bytes; the recipient collects it from the mailbox over the inbox protocol
The custodian is a left-luggage office, not a postman: it doesn't know what it holds or for whom

Two details turn that from a claim into a property of the system.

First, the mailbox is never named in the request. A device arrives with an authorization proof, and the custodian derives the mailbox from the account root inside that proof, binding it to the specific network peer on the other end of the stream. You cannot ask for someone else's mailbox — not because it's forbidden, but because the request has no such field. An observed proof can't be replayed either: it's bound to the connection.

Second, the control plane accepts no content. An application still needs a small server-side surface: an entry point into the network, short-lived scoped admission credentials, signed relay cards for when a direct connection doesn't form, and an ordinary "wake this device" for APNs and FCM. That surface has fixed request schemas, bounded values and closed allowlists — and not one endpoint that accepts, stores or returns message text. The push carries a wake signal and nothing else.

#Cryptography: why not just X3DH

WhatsApp and Signal build a session on X3DH: several elliptic-curve Diffie–Hellman exchanges producing a shared secret, then a Double Ratchet that rotates the key on every message.

The problem is well known and isn't about implementation quality: traffic captured today can be decrypted tomorrow, once a large enough quantum computer exists. For a messenger meant to hold years of conversation, that's a real risk rather than a thought experiment.

So Umbra's handshake is PQXDH: the same X25519 exchanges plus an ML-KEM-768 encapsulation. The session secret combines both sources, so recovering it means breaking both — the classical half and the post-quantum one.

A PQXDH handshake of X25519 exchanges and an ML-KEM encapsulation yields a root secret, from which an HMAC chain derives a per-message key
A doubled session root: both the classical and the post-quantum half must break

Then come the details that usually decide whether a scheme survives contact with reality:

  • The message header and both long-term identity keys go into the AEAD's associated data. Signed coverage therefore extends past the content to "who, to whom, and which one in order": a relay can't swap the recipient or reorder messages.
  • A device's long-term key is signed by the device key the account root certified. Without that binding, someone else's device could present a victim's certified identity alongside its own key.
  • A failed decryption never mutates the session, and a forged handshake never consumes a one-time pre-key. Otherwise you've handed the outside world a free way to break a conversation.
  • All of the crypto is pure Go with no cgo — one wire format serves Android through GoMobile, desktop, CLI, and a WebAssembly build for the browser.

Separately: trust isn't decided here. The decrypt function takes a trust predicate that the application resolves against account-root certificates. A crypto layer shouldn't decide who we believe — it should verify that what arrived is what was signed.

#Spam without phone numbers

In WhatsApp the phone number isn't only an identifier, it's the main anti-spam barrier: costly and tied to a real person. Remove the number and you lose the barrier. Its replacement has to work without knowing who you are.

An Umbra mailbox is closed by construction: the custodian refuses any object whose ticket wasn't signed by an issuer the owner authorized. Beyond that there are three ways for a stranger to become reachable, and all three are about economics rather than identity:

  • A signed mailbox policy — "I accept: nothing / an introduction / a stamp / anyone". Policies expire, and an older one can never displace a newer one.
  • Introduction tokens — bounded, revocable permissions carried in a contact invite or QR code. Each redemption is counted and bound to the redeeming device, so an invite observed in transit is useless to anyone else, and a leaked one is capped by its redemption count and expiry.
  • Postage stamps — proof of work bound to the mailbox, the day, and the exact object. The object binding is what makes bulk mail expensive: the work can't be amortised across a thousand recipients or reused for a second message.

The implementation detail that matters: enforcement happens at the offer frame, before any payload transfer. A rejected sender costs the recipient neither bandwidth nor storage. And both sides — custodian and receiving device — check the policy independently.

#What honestly isn't finished

The protocol is proven: two browsers exchange real messages through the same core compiled to WebAssembly. The product isn't. The web client can't yet acknowledge what it collected from the inbox (the call it needs isn't exported in the WASM build), replying requires both sides to have exchanged invites, and there's no interface for doing that. The production custodian was brought up by hand, outside the deploy platform, and is waiting on a root-key ceremony from its owner.

That's exactly the class of work a diagram doesn't show: the architecture holds, and the user's path through the product stops at step three.

#What to take from this

  • Encrypting content is the first step, not the finish. While the server knows who writes to whom, it knows most of what's interesting.
  • Remove the field from the request if you don't want it abused. "The mailbox is never named" is stronger than any permission check.
  • The post-quantum half is added on top of the classical one, not instead of it: the combination is no weaker than the better of the two, and requires breaking both.
  • If you remove one defensive barrier (the phone number), design its replacement in advance. Otherwise an open network becomes a spam network on day one.

// available for hire

Need a website or setup, done right?