In my last two posts I introduced my browser messenger and the identity signatures I added to it. This one is about the machinery underneath both: the ratchet. It is the part that decides what happens when something goes wrong — when a key leaks, when messages arrive out of order, when an attacker records everything today and hopes to decrypt it tomorrow.
Why forward secrecy is the whole point
Most people think encryption is about keeping a message secret right now. Forward secrecy is about keeping it secret forever. If someone steals your phone, or a server log, or a session key next year, they should not be able to rewind and read last year's messages.
A ratchet gives you that by never reusing a key. Every message gets a fresh one-time AES-256-GCM key, derived from a chain that moves forward and never goes back. Old keys are not stored anywhere. There is nothing to steal that unlocks the past.
A hybrid chain
My protocol, PQ-MPRBA, builds the chain from two shared secrets at once: ML-KEM-768, the post-quantum key encapsulation mechanism, and classical ECDH P-256. Both feed HKDF-SHA256, which drives a symmetric ratchet chain.
Why both? Because migration is a transition, not a switch. ML-KEM protects against a future quantum attacker. ECDH keeps the whole thing working with today's infrastructure and libraries. The honest label is hybrid — and the day the classical half is no longer needed, it can be dropped without redesigning the chain.
Out-of-order messages and skipped keys
Real networks do not deliver messages in order. If message 5 arrives before message 4, you cannot just throw message 5 away — but you also cannot rewind the chain to decrypt it later without keeping old keys around, which is exactly what forward secrecy forbids.
The standard answer is a bounded skipped-key cache: when a message arrives early, you derive and hold its key just long enough for the missing messages to catch up, then wipe it. In my implementation the cache is deliberately small — at most 100 skipped keys, with a 10-minute lifetime. Anything older than that is gone, and a late message past that window simply cannot be decrypted. That is a real trade-off, and I chose the strict side of it: a lost message is better than a kept key.
Zeroization is not optional
Deleting a key in most languages just marks memory as free. The bytes can sit there until something overwrites them. So after a key is used, my implementation explicitly zeroes the memory that held it.
This is the least glamorous part of the whole protocol and one of the most important. Forward secrecy fails silently if old keys linger in RAM, in a swap file, or in a crash dump. The ratchet only works if "deleted" actually means deleted.
Post-compromise recovery
The other direction matters too. Suppose an attacker does compromise a session key — not the past, but the present. A ratchet heals: as soon as new messages flow, the chain advances past the compromised state, and the attacker is locked out again without anyone having to notice or re-authenticate.
That self-healing property is why ratchets beat the simpler alternative of one long-lived session key. A single key is a single point of failure forever. A chain is a failure with an expiry date.
What is still honest to say
This is an experimental protocol built by one person, and it has not been independently audited. The hybrid design still carries a classical ECDH half, so the whole thing is not purely post-quantum yet — it is labeled as hybrid, on purpose.
But the principles are not experimental: fresh keys per message, bounded skipped keys, real zeroization, and a chain that heals. Those four ideas are what forward secrecy actually is, and they are all in the code.
The code is open source: https://github.com/indirect-b/chat-app
Next time I will write about the group side of the same protocol — what happens to the keys when someone joins or leaves a group chat.
Top comments (1)
The 100-key, 10-minute skipped-key limit is a clear trade-off, because it protects forward secrecy at the cost of late messages. Have you seen real chats hit that limit often, or does it mostly stay theoretical in normal use?