Somewhere right now, somebody is recording your encrypted traffic.
Not reading it. They cannot read it — not yet. The mathematics of elliptic curves and RSA stand between them and your data. But they are recording it, storing it on tape, waiting. Because they believe — and they may be right — that within the next fifteen or twenty years, a sufficiently large quantum computer will be able to factor those numbers and solve those curves, and on that day, every piece of encrypted traffic ever captured will be readable.
This is called the “harvest now, decrypt later” attack. It is not theoretical. Intelligence agencies have been doing it for decades with conventional cryptography, waiting for faster classical computers. Quantum computing changes the timeline from “eventually, maybe” to “certainly, soon.”
Wormhole v2-PQ is built for this future.
What wormhole does
At its simplest: you want to send a file to someone. You run a command. It gives you four tokens — two English words and two short codes:
GLACIER PRISM WX42 3CL6
You read these aloud, or paste them into a message. The other person types them in. A cryptographic tunnel forms. The file transfers. The tunnel collapses. Nothing is stored on any server. No account. No login. No cloud.
This is the same basic idea as Magic Wormhole, which has been moving files between terminals for years. What is different here is what happens between the moment you speak those four tokens and the moment the file arrives.
The sigil
Those four tokens — GLACIER PRISM WX42 3CL6 — are called a sigil. The word is deliberate. A sigil is a symbol with power.
The two words come from a curated list of 8,192 English words — common, unambiguous, easy to say over the phone. Each word carries 13 bits of entropy. The two codes use a reduced character set of 29 symbols: the letters A through Z minus I, O, and S (too similar to 1, 0, and 5), and the digits 2, 3, 4, 6, 7, 9 (minus 0, 1, 5, and 8, which look like O, I, S, and B). Each four-character code carries about 19.4 bits.
Total entropy: roughly 65 bits. That sounds modest until you consider the threat model. The sigil is not a password that guards a vault for years. It is a one-time key that exists for about sixty seconds, and an attacker cannot try it offline. Every guess requires an actual network connection to the sender. At one guess per connection, 2^65 attempts would take longer than the age of the universe.
The sigil is the password. It is also the address. SHA-256 of the canonical sigil, truncated to 16 bytes, becomes the channel ID — the key under which the sender’s connection ticket is stored on the signaling server. Know the sigil, find the sender.
The signaling server
Here is a problem: two computers that have never met need to talk to each other. Both are probably behind NAT — network address translation — which means neither knows the other’s real address. Both think they live at 192.168.1.something.
The signaling server solves this with two HTTP requests:
The sender generates an Iroh QUIC endpoint — essentially, “I am reachable at these addresses, through these relay servers” — serializes it into a ticket, and uploads it:
PUT /signal/{channel_id}
Body: endpoint:ADDR:RELAY:DISCOVERY:...
The receiver, who knows the sigil and therefore can compute the same channel ID, fetches the ticket:
GET /signal/{channel_id}
That is all the signaling server does. It stores a string for up to five minutes. It has no accounts, no sessions, no authentication, no knowledge of what the ticket contains. It is a dead drop — a place where two strangers can leave a note.
The signaling server at wormhole.itys.net does not relay any actual data. It introduces the two peers. After that, they talk directly over QUIC, relayed through Iroh’s infrastructure only if direct connectivity is impossible.
The hybrid handshake
This is where the interesting cryptography happens.
Once the QUIC connection is established, the first bidirectional stream opens with a single byte: 0x00 — this is a handshake. Then a version byte: 0x02 — this is protocol version 2. Then the key exchange begins.
Both peers perform the same steps simultaneously. This is a symmetric protocol — there is no “client” and “server,” no “Alice initiates and Bob responds.” Both start from the sigil and converge on the same key. The sequence is:
Step 1: Both peers create their hello messages.
Each peer starts a SPAKE2 exchange using the sigil as the password. SPAKE2 is a password-authenticated key exchange — it takes a low-entropy password and turns it into a high-entropy shared secret, with the remarkable property that an eavesdropper who records the entire exchange learns nothing about the password. Not one bit. The only way to check a guess is to participate in the protocol, which requires talking to the other peer.
Simultaneously, each peer generates a fresh ML-KEM-768 keypair. ML-KEM — Module Lattice Key Encapsulation Mechanism — is the post-quantum algorithm. It is based on the hardness of the Learning With Errors problem over module lattices, a mathematical structure that quantum computers, as far as anyone knows, cannot efficiently solve. It was standardized as FIPS 203 after years of evaluation by NIST.
The hello message is the SPAKE2 public message concatenated with the ML-KEM encapsulation key: 1,184 bytes of lattice mathematics appended to the elliptic curve message.
Step 2: Both peers exchange hello messages and process them.
Each peer receives the other’s hello, splits it into the SPAKE2 message and the ML-KEM encapsulation key, completes the SPAKE2 exchange to derive a first shared secret, and then encapsulates a fresh random secret to the other peer’s ML-KEM key. Encapsulation is a one-way operation: it produces a ciphertext that only the holder of the corresponding decapsulation key can open, plus a shared secret that both sides will know.
Step 3: Both peers exchange ML-KEM ciphertexts.
Each peer sends 1,088 bytes of post-quantum ciphertext to the other.
Step 4: Both peers decapsulate and derive the session key.
Each peer decapsulates the other’s ciphertext using their own ML-KEM decapsulation key, producing a second shared secret. Now both peers have three independent shared secrets:
- The SPAKE2 shared secret (from the password-authenticated exchange)
- Their own ML-KEM shared secret (from encapsulating to the peer)
- The peer’s ML-KEM shared secret (from decapsulating the peer’s ciphertext)
The ML-KEM secrets are sorted lexicographically so that both peers combine them in the same order regardless of who sent first. All three are concatenated — 96 bytes of input keying material — and fed through HKDF-SHA256:
session_key = HKDF-SHA256(
spake2_ss || mlkem_ss_first || mlkem_ss_second,
info = "wormhole-v2-session"
)
One 32-byte key emerges. Every intermediate secret — the SPAKE2 state, the ML-KEM decapsulation key, the individual shared secrets — is zeroed from memory. The zeroize crate compiles this to writes that the optimizer cannot remove.
Why hybrid
Richard Feynman had a principle: if you cannot explain something simply, you do not understand it well enough. So here is the simple version of why the handshake uses two different kinds of mathematics.
SPAKE2 is based on elliptic curves. Elliptic curves are well-understood, battle-tested, deployed in billions of devices. But Shor’s algorithm, running on a sufficiently large quantum computer, can break them. Nobody has built that computer yet. But if they do, every elliptic curve secret ever exchanged is compromised.
ML-KEM is based on lattice problems. Lattice problems are believed to be hard for quantum computers. But they are newer. Less studied. It is possible — unlikely but possible — that someone will find a classical or quantum attack that breaks them.
So we use both.
If SPAKE2 is broken (by a quantum computer) but ML-KEM holds, the session key is safe — an attacker who knows the SPAKE2 secret still cannot derive the session key without also knowing both ML-KEM secrets.
If ML-KEM is broken (by a breakthrough in lattice cryptography) but SPAKE2 holds, the session key is safe — an attacker who knows the ML-KEM secrets still cannot derive the session key without the SPAKE2 secret, which requires knowing the sigil.
Both must fall simultaneously for the session key to be compromised. Two independent assumptions. Two independent failure modes. The mathematical equivalent of a building with two independent load-bearing structures — either one can hold the building alone.
The seal
After the handshake, both peers independently derive a seal from the session key:
seal_bytes = HKDF(session_key, "wormhole-v2-seal") → 4 bytes
Each byte indexes into a palette of 256 emoji — animals, nature, objects, food — producing a visual fingerprint:
seal: 🐻🌊🔑🍎
Both peers display this seal. If the seals match, the keys match, which means the handshake succeeded, which means no attacker intercepted the exchange. If the seals do not match — if Alice sees 🐻🌊🔑🍎 and Bob sees 🦊⭐🎲🍋 — something is very wrong, and the transfer should be aborted.
This is the same principle as the safety numbers in Signal or the key verification in WhatsApp. But where those use long hex strings that nobody actually compares, four emoji are glanceable. You can verify them in a second.
Two layers of encryption
The QUIC connection itself is encrypted with TLS 1.3, using X25519 key exchange. This is the transport layer — it protects the data in transit against passive eavesdroppers.
But Wormhole v2-PQ does not trust the transport layer alone. Every piece of data — chat messages, file metadata, file chunks — is encrypted again with ChaCha20-Poly1305 using a key derived from the session key. This is the session cipher.
Why encrypt twice? Because the QUIC layer uses classical cryptography. X25519 is an elliptic curve. A quantum computer that breaks X25519 would be able to read the QUIC traffic — but it would find only ciphertext, encrypted with ChaCha20-Poly1305 under a key derived from the hybrid handshake. To read the actual data, the attacker would also need to break the session key, which requires breaking SPAKE2 and ML-KEM simultaneously.
Each file transfer gets its own cipher, derived from the session key with a unique index:
file_key = HKDF(session_key, "wormhole-v2-file-" || file_index)
This means concurrent file transfers use independent nonce counters. No two encryptions ever use the same key-nonce pair — the cardinal sin of symmetric cryptography.
What moves over the wire
The QUIC connection is multiplexed into streams. Each stream starts with a one-byte tag:
| Tag | Stream | Purpose |
|---|---|---|
0x00 | Handshake | SPAKE2 + ML-KEM key exchange |
0x01 | File | Encrypted file transfer |
0x02 | Chat | Encrypted messages |
0x03 | Control | Keepalive, close |
A file transfer looks like this:
[1 byte: 0x01] ← stream kind
[4 bytes BE: length] [encrypted FileMeta] ← name + size
[4 bytes BE: length] [encrypted chunk] ← 64 KiB
[4 bytes BE: length] [encrypted chunk] ← 64 KiB
...
[stream finish] ← EOF
Every frame is length-prefixed with a 4-byte big-endian integer. Every payload is encrypted with ChaCha20-Poly1305, which appends a 16-byte authentication tag. If even one bit is flipped in transit, decryption fails — there is no silent corruption.
The file’s integrity is verified independently with BLAKE3, printed at both ends after transfer. Same hash? Same file, byte for byte.
The network
Wormhole v2-PQ runs on Iroh, a QUIC networking library that handles NAT traversal, hole punching, and relay fallback. Three relay servers — ns-01.rns.sh, ns-02.rns.sh, ns-03.rns.sh — provide connectivity when direct peer-to-peer connections are impossible.
Four connection modes:
- direct — LAN only, no relays. For when both machines are on the same network.
- rns — The default. Uses the three rns.sh relay servers.
- iroh — Uses Iroh’s default n0 relay infrastructure.
- custom — Any HTTPS URL pointing to an Iroh relay.
The relay servers carry encrypted traffic. They cannot read it — they see QUIC packets encrypted with TLS 1.3, containing payloads encrypted with ChaCha20-Poly1305, derived from a key they never possessed. They are plumbing. Important plumbing, but plumbing.
Identities
Wormhole v2-PQ has an optional identity system. You can generate an Ed25519 keypair — a long-term identity stored in ~/.config/wormhole/identity.key, encrypted with XChaCha20-Poly1305 under a key derived from your passphrase via Argon2id.
Identities are separate from the file transfer mechanism. A transfer does not require an identity. The sigil is the only credential needed. But identities enable future features: signed transfers, contact lists, persistent conversations.
The database — SQLite at ~/.config/wormhole/wormhole.db — stores contacts, conversations, message history, and file transfer metadata. All local. Nothing is synced, uploaded, or shared.
What this is really about
Carl Sagan once observed that we live in a society exquisitely dependent on science and technology, in which hardly anyone knows anything about science and technology. The same is true of cryptography. We depend on it for everything — banking, communication, medicine, infrastructure — and almost nobody understands how it works.
The honest truth about post-quantum cryptography is that nobody knows whether we actually need it yet. No quantum computer exists that can break elliptic curve cryptography. It might be five years away. It might be fifty. It might never happen in a practically useful form.
But the harvest-now-decrypt-later attack is real. The data you send today is being recorded by entities with very long time horizons. If that data will still be sensitive in twenty years — and medical records, financial data, legal communications, personal correspondence all will be — then the time to protect it is now. Not when the quantum computer is announced. Now. Before the traffic is captured.
Wormhole v2-PQ is not paranoia. It is the engineering response to a threat with an uncertain timeline and a certain consequence. The cost of being early is a few extra kilobytes in the handshake. The cost of being late is everything you ever sent.
Four words. Two kinds of mathematics. One file, arriving safely on the other side of a tunnel that even a computer running on the laws of quantum mechanics cannot see through.
SPAKE2 password-authenticated key exchange uses the Ed25519 group
(Rust crate: spake2 0.4, identity "wormhole-symmetric").
ML-KEM-768 follows FIPS 203 (Rust crate: ml-kem 0.2), with
1184-byte encapsulation keys and 1088-byte ciphertexts.
Session cipher: ChaCha20-Poly1305 (RFC 8439).
Key derivation: HKDF-SHA256 (RFC 5869).
File integrity: BLAKE3.
Transport: QUIC via Iroh 0.96 (ALPN /wormhole/2).
Memory hygiene: zeroize crate for constant-write secret clearing.
Identity storage: XChaCha20-Poly1305 + Argon2id (RFC 9106).