Security Architecture

Message Encryption

Overview

After X3DH establishes the initial session secret, the Double Ratchet algorithm takes over. The Double Ratchet derives a unique encryption key for each message, ensuring that if one message’s key is compromised, past and future messages are still secure. It combines two ratcheting mechanisms:

  1. Symmetric ratchet: Each message advances the encryption key using HMAC-SHA256
  2. DH ratchet: Periodically (once per side of the conversation), a new Diffie-Hellman key pair is generated, combined with the root key, and the keys are re-derived

Symmetric Ratchet (Per-Message Keying)

Each sending chain maintains a “chain key”. When the sender wants to send a message:

  1. The current chain key is used to derive a message key via HMAC-SHA256:

    message_key = HMAC-SHA256(key=chain_key, data="message_key")
    
  2. The chain key is advanced:

    next_chain_key = HMAC-SHA256(key=chain_key, data="chain_key")
    
  3. The message is encrypted using AES-256-GCM with the message_key and a fresh random nonce

  4. The message_key is immediately deleted

Because the chain key advances deterministically and cannot be reversed (HMAC is one-way), an attacker with knowledge of the current chain key cannot decrypt past messages—they cannot recompute old message_key values.

DH Ratchet (Per-Reply Keying)

When the recipient replies, a new Diffie-Hellman step occurs:

  1. The recipient generates a fresh Curve25519 key pair (RK_B_private, RK_B_public)
  2. The recipient computes DH(RK_B_private, RK_A_public), where RK_A_public was the sender’s previous ratchet key
  3. This DH shared secret is combined with the current root key via HKDF to derive a new root key and a new sending chain key
  4. The recipient includes RK_B_public in the reply message header

When the original sender receives the reply:

  1. The sender extracts RK_B_public from the reply header
  2. The sender computes DH(RK_A_private, RK_B_public), producing the same shared secret
  3. The sender derives the new root key and receiving chain key using the same HKDF computation
  4. The sender generates and sends a new RK_A_public in the next outgoing message

The key insight is that after the DH ratchet step, both participants have the same new root key, and future messages derive keys from this new root. If an attacker compromises the session state at any point before the next DH ratchet, the attacker knows all messages sent after that compromise—but once the next DH ratchet occurs, future messages are secure again.

Forward Secrecy

The symmetric ratchet provides message-level forward secrecy: deleting a message key immediately after use makes past messages unrecoverable.

The DH ratchet provides break-in recovery: if a participant’s session state is compromised at time T (before the next DH ratchet step), messages before T are lost, but as soon as the next DH ratchet occurs (triggered by the next reply), future messages are secure again. The attacker cannot use the compromised state to derive future keys because future keys depend on newly generated ephemeral key pairs that the attacker does not know.

Out-of-Order Message Handling

The Double Ratchet handles messages that arrive out of order. Each message is tagged with a chain key generation counter. If a message arrives with a chain key counter ahead of the current state, the recipient:

  1. Skips forward in the symmetric ratchet to reach the target counter
  2. Derives and stores all intermediate message keys (in case earlier messages arrive later)
  3. Decrypts the out-of-order message
  4. Discards the intermediate keys after a retention window

This allows the protocol to tolerate network reordering without requiring retransmission or blocking on message order.

Authenticated Encryption

Every message is encrypted with AES-256-GCM, which provides both confidentiality and authenticity. The authentication tag ensures that an attacker cannot modify ciphertext or forge messages—any tampering is detected during decryption.

Session State on Disk

Decrypted message keys are never stored on disk. The receiving chain key (and generation counter) are stored, allowing the device to derive message keys for out-of-order arrivals, but the message keys themselves are derived fresh for each decryption and then discarded. This ensures that even if an attacker gains access to the device’s stored session state (e.g., via forensic analysis after the device is compromised), they cannot bulk-decrypt messages—they can only read messages that arrive after the compromise.