Double Ratchet Algorithm

The cryptographic engine behind every Encra message. Every time Alice sends Bob a message, three keys advance β€” and anything the attacker captured before that moment becomes permanently unreadable.

πŸ’‘ TL;DR

Double Ratchet = a symmetric KDF chain (fast, per-message key rotation) combined with a Diffie-Hellman ratchet (periodic key material injection from new ephemeral key pairs). The result: forward secrecy and break-in recovery in a single protocol.

History & origin

The Double Ratchet Algorithm was designed by Trevor Perrin and Moxie Marlinspike at Open Whisper Systems (now Signal Foundation) in 2013, initially shipped in the TextSecure app.1 It evolved from Stuart Anderson's Off-the-Record (OTR) messaging protocol2 by replacing OTR's three-message handshake with an always-on ratchet, eliminating the need to re-negotiate keys mid-conversation.

The formal specification was published in November 20161 and independently analysed by Cohn-Gordon et al. in 20163 and by Alwen, Coretti & Dodis in 2019 (the β€œACD” paper)4 which proved the protocol's security under the standard model. Today the same algorithm runs inside Signal, WhatsApp (1 billion+ users), Facebook Messenger (secret conversations), Skype (private conversations), and Google Messages (RCS E2E).

Three keys, three jobs

Every session maintains exactly three long-lived state values:

Root Key (RK)

64 bytes. Seeded from the X3DH handshake. Advanced only on DH ratchet steps. Acts as the master secret that feeds both sending and receiving chain keys.

Chain Key (CK)

32 bytes. One per direction (send / receive). Advanced on every message. Never leaves the device β€” used only to derive per-message keys.

Message Key (MK)

32 bytes. Derived from the chain key, used to encrypt exactly one message, then zeroed from memory. Enables forward secrecy.

The KDF chain (symmetric ratchet)

For every message sent, the chain key advances once through a keyed hash function (KDF). Two outputs are produced from a single call:

  • KDF(CK, 0x02) β†’ next chain key (replaces the current one)
  • KDF(CK, 0x01) β†’ message key (used once to encrypt, then deleted)

KDF Chain (one direction β€” sender or receiver)

Chain Keyn
KDF(CK, 0x02)
Chain Keyn+1
KDF(CK, 0x01)
Msg Keyn
● encrypt / decrypt, then delete

Because the KDF is a one-way function, the chain can only move forward. An attacker who steals CK_n can derive MK_n, MK_{n+1}, … but cannot reverse-derive CK_{n-1} or any previous message keys β€” those are already gone.

β„Ή Encra's KDF primitive

Encra uses keyed BLAKE2b-256 (libsodium crypto_generichash5) as the KDF, passing the constant byte as the data and the chain key as the BLAKE2b key. BLAKE2b is faster than HMAC-SHA-256 and provides equivalent security for this use-case. The Signal spec uses HMAC-SHA-256; both are valid instantiations of the abstract KDF interface.

The DH ratchet (asymmetric ratchet)

The KDF chain alone provides forward secrecy, but not break-in recovery β€” if an attacker steals the current chain key, all future messages are compromised until the conversation ends. The Diffie-Hellman ratchet fixes this.

Every message header carries the sender's current ephemeral ratchet public key. When the receiver sees a new public key, they:

  1. Perform DH(their_priv, new_pub) β†’ feed the output into the root KDF β†’ derive a new receiving chain key.
  2. Generate a fresh ephemeral key pair of their own.
  3. Perform DH(new_priv, new_pub) β†’ feed into root KDF β†’ derive a new sending chain key.

DH Ratchet step (when a new message arrives with a new DH public key)

Alice
ratchet keypair:
A_priv / A_pub
receives:
B_pub (new)
DH(A_priv, B_pub)
β†’ new root key
β†’ recv chain key
generates new:
A_privβ€² / A_pubβ€²
DH(A_privβ€², B_pub)
β†’ send chain key
A_pubβ€² sent in next msg header
⇄
B_pub sent in Bob's msg header
Bob
ratchet keypair:
B_priv / B_pub
receives:
A_pub (new)
DH(B_priv, A_pub)
β†’ new root key
β†’ recv chain key
generates new:
B_privβ€² / B_pubβ€²
DH(B_privβ€², A_pub)
β†’ send chain key

Because fresh DH key material is mixed into the root key, any chain-key compromise is healed after the next DH step. An attacker would need to also compromise the new ephemeral private key β€” which was just generated and never transmitted.

Combined conversation flow

In practice, both ratchets run together: the KDF chain advances on every message; the DH ratchet advances whenever the receiver sees a new ratchet public key in the message header.

Message flow β€” KDF + DH ratchets combined
Aliceβ†’Msg 1 (header: A_pub)MK₁
Alice→Msg 2 (header: A_pub)MK₂
Bob←Msg 3 (header: B_pubβ€²)MK₃
Alice→Msg 4 (header: A_pub′)MK₄
Bob←Msg 5 (header: B_pubβ€³)MKβ‚…
Each MK is used once and then deleted β€” compromising MK₃ reveals nothing about MK₁, MKβ‚‚, MKβ‚„, or MKβ‚….

Encra implementation

The full implementation lives in packages/core/src/crypto/ratchet.ts. The public API is a single class:

packages/core/src/crypto/ratchet.ts (excerpt)typescript
export class DoubleRatchet {
  /** Start a session as the sender (Alice) after X3DH handshake */
  static initSender(
    sharedSecret: Uint8Array,        // 32-byte secret from X25519
    recipientPublicKey: Uint8Array,  // Bob's long-term or signed pre-key
  ): DoubleRatchet

  /** Start a session as the receiver (Bob) after X3DH handshake */
  static initReceiver(
    sharedSecret: Uint8Array,
    localKeyPair: KeyPair,           // Bob's ratchet key pair
  ): DoubleRatchet

  /** Encrypt a plaintext string. Returns header + ciphertext + nonce. */
  encrypt(plaintext: string): EncryptedMessage

  /** Decrypt an EncryptedMessage. Throws DecryptionFailedError on any failure. */
  decrypt(msg: EncryptedMessage): string
}
EncryptedMessage typetypescript
interface EncryptedMessage {
  header: {
    publicKey: Uint8Array   // sender's current ratchet public key
    n: number               // message number within the sending chain
    pn: number              // message number of last msg in previous sending chain
  }
  ciphertext: Uint8Array    // XSalsa20-Poly1305 ciphertext
  nonce: Uint8Array         // 24-byte random nonce
}

Session initialization

Sessions are bootstrapped with the shared secret from X25519 key exchange. Alice feeds it into the root KDF along with a fresh ephemeral key pair; Bob starts with just the shared secret and his ratchet key pair.

Session initializationtypescript
import { generateKeyPair, deriveSharedSecret } from '@encra/core'
import { DoubleRatchet } from '@encra/core'

// --- Alice's device ---
const aliceKeys = await generateKeyPair()
const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobPublicKey)

const aliceRatchet = DoubleRatchet.initSender(sharedSecret, bobPublicKey)
const encrypted = aliceRatchet.encrypt('Hello Bob!')
// β†’ { header, ciphertext, nonce }

// --- Bob's device ---
const bobRatchet = DoubleRatchet.initReceiver(sharedSecret, bobKeyPair)
const plaintext = bobRatchet.decrypt(encrypted)
// β†’ 'Hello Bob!'

Out-of-order message handling

Network reordering is common in mobile environments. When Alice sends messages 1–5 but Bob receives them in order 1, 3, 2, 5, 4 β€” the ratchet must still decrypt all of them correctly.

The pn (previous-chain-N) field in the header tells the receiver how many messages were sent in the previous sending chain. When a DH ratchet step is detected, Bob computes and stores all skipped message keys from the previous chain before advancing. Skipped keys for the current chain are stored as they are encountered. Both pools are bounded by MAX_SKIP_KEYS = 1000 to prevent memory exhaustion attacks.

⚠ Skipped key lifetime

Skipped message keys are held in memory until used or the session is closed. They are not persisted to IndexedDB in the current implementation. If the receiver process restarts before a skipped message arrives, that message will fail to decrypt. Persistence of skipped keys is on the roadmap.

Security properties

Security propertyEncraDetails
Forward Secrecyβœ“ YesPast messages cannot be decrypted even if the current session state is compromised. Message keys are deleted immediately after use.
Break-in Recoveryβœ“ YesAfter an attacker gains session state, future message security is automatically restored on the next DH ratchet step.
Message Integrityβœ“ YesXSalsa20-Poly1305 provides authenticated encryption β€” any bit-flip in transit is detected and the message rejected.
Replay Protectionβœ“ YesMessage counters (N) are tracked. A replayed message index within a chain will be skipped or rejected.
Out-of-order deliveryβœ“ YesSkipped message keys are stored (up to MAX_SKIP_KEYS = 1000) so messages can arrive and decrypt out of order.
Deniabilityβœ“ YesShared ephemeral DH keys mean neither party can cryptographically prove a message was sent by the other. Same as Signal.
Identity hidingβœ— Out of scopeMessage headers contain the sender's ratchet public key in plaintext. Transport-layer privacy (TLS / Tor) is required for anonymity.
Group messagingβœ— Out of scopeDouble Ratchet is a pairwise protocol. Sender Keys (Signal) or MLS (RFC 9420) are needed for groups.

🚨 What Double Ratchet does NOT protect against

  • Compromised endpoint β€” if an attacker installs malware on Alice's device and reads plaintext from the screen or memory, no protocol can help.
  • Long-term identity key theft β€” stealing the X25519 long-term private key enables MITM on future sessions. Protect it with IndexedDB + device biometrics.
  • Traffic analysis β€” message sizes, timing, and frequency are visible to the server. Use sealed-sender envelopes and padding for metadata resistance.

Signal spec vs Encra implementation

AspectSignal specEncra
KDF primitiveHMAC-SHA-256BLAKE2b-256 (libsodium crypto_generichash)
DH functionCurve25519 X25519X25519 via libsodium crypto_scalarmult
Message encryptionAES-256-CBC + HMAC or AES-GCMXSalsa20-Poly1305 (libsodium crypto_secretbox)
Header encryptionOptional (HKDF-SHA-256)Not yet implemented (roadmap)
MAX_SKIP_KEYSβ‰₯ 1000 (recommendation)1000 (hard limit)
Skipped key persistenceImplementation-definedIn-memory only (roadmap: IndexedDB)
Initial keyingX3DH handshakeX25519 ECDH (simplified β€” no pre-key bundles yet)

References

  1. Perrin, T. & Marlinspike, M. β€” The Double Ratchet Algorithm (Signal Foundation, 2016)
  2. Borisov, N., Goldberg, I. & Brewer, E. β€” Off-the-Record Communication (ACM WPES 2004)
  3. Cohn-Gordon, K. et al. β€” On the Formal Security of Signal (IEEE EuroS&P 2017, ePrint 2016/221)
  4. Alwen, J., Coretti, S. & Dodis, Y. β€” The Double Ratchet: Security Notions, Proofs, and Modularization (EUROCRYPT 2019, ePrint 2019/800)
  5. libsodium β€” Generic hashing (crypto_generichash / BLAKE2b)
  6. Perrin, T. & Marlinspike, M. β€” The X3DH Key Agreement Protocol (Signal Foundation, 2016)
  7. Marlinspike, M. β€” Advanced cryptographic ratcheting (Signal blog, 2013)

See also

Encra AI

Ask me anything Β· docs, code, troubleshooting

Hi, I'm Encra AI

I can explain concepts, generate starter code, troubleshoot errors, and guide your setup.

May make mistakes Β· verify critical crypto details