Double Ratchet Protocol

Kodium provides a complete, pure-Kotlin implementation of the Double Ratchet Algorithm along with the X3DH (Extended Triple Diffie-Hellman) key agreement protocol. This combination allows you to build secure, end-to-end encrypted (E2EE) peer-to-peer applications, such as a secure chat application.

Overview

The Double Ratchet algorithm combines a cryptographic ratchet based on Diffie-Hellman (DH) key exchanges with a symmetric-key ratchet. This ensures both Forward Secrecy (compromise of current keys does not compromise past messages) and Break-in Recovery or Future Secrecy (compromise of current keys does not compromise future messages).

X3DH establishes a shared secret between two parties asynchronously, acting as the secure foundation to initialize the Double Ratchet sessions.

Example: P2P Secure Chat

This example demonstrates how Alice and Bob can securely exchange messages using Kodium's Double Ratchet implementation across the typical phases of a P2P chat application.

Phase 1: Account Creation & Key Publishing

Users Alice and Bob create their accounts in the chat app independently. During registration, their devices generate the necessary cryptographic keys and publish public data to a central server so they can be contacted later.

import io.kodium.KodiumPrivateKey
import io.kodium.ratchet.X3DH

// Bob creates an account and generates his keys
val bobIdentityKey = KodiumPrivateKey.generate()
val bobSignedPreKey = KodiumPrivateKey.generate()
val bobOneTimePreKey = KodiumPrivateKey.generate()

// Bob publishes his "Public Bundle" to the chat server
val bobBundle = X3DH.PublicBundle(
    identityKey = bobIdentityKey.publicKey,
    signedPreKey = bobSignedPreKey.publicKey,
    oneTimePreKey = bobOneTimePreKey.publicKey
)

// Bob can encode the bundle to a Base58 string to easily send it over the network
// and save it in the central database
val bobBundleString = bobBundle.exportToEncodedString().getOrThrow()

// --- Meanwhile, on Alice's device ---

// Alice also creates an account and generates her keys
val aliceIdentityKey = KodiumPrivateKey.generate()
val aliceEphemeralKey = KodiumPrivateKey.generate() // Generated specifically when she wants to start a chat

Phase 2: Alice Initiates Contact

Alice decides to get to know Bob. She gets his username, decides to add him to her contact list, and starts a chat. Her device fetches Bob's public bundle from the server and computes the shared secret.

Phase 3: Bob Responds & Secure Chat Continues

Bob receives Alice's request. He decides to accept her chat. His device uses his private keys and Alice's provided public keys to compute the identical shared secret and initialize his session.

Session Lifespan: Can a session be used forever?

Technically, yes. Once a DoubleRatchetSession is established, the math allows it to run indefinitely.

The algorithm is specifically designed for long-lived asynchronous messaging:

  • The Symmetric Ratchet ensures forward secrecy for every single message.

  • The Diffie-Hellman (DH) Ratchet introduces new entropy whenever the direction of the conversation changes, ensuring break-in recovery.

You do not need to periodically recreate or expire the session strictly based on a time period.

However, in practice, a session might need to be recreated (requiring a new X3DH exchange) if:

  1. Device Loss/Reset: A user loses their device, wipes their local database, or uninstalls the app without a backup, permanently losing their current session state.

  2. Manual Key Rotation: The application enforces a strict security policy requiring complete identity key rotation.

  3. Irrecoverable Desynchronization: If an extreme number of messages are permanently lost in transit (exceeding the configured MAX_SKIP window), the chains cannot recover and a new session must be established.

Advanced Usage: Context Binding

To prevent cross-protocol attacks, the X3DH and Double Ratchet algorithms support passing an optional applicationInfo (or info) byte array. This ensures that cryptographic keys derived in your application cannot accidentally or maliciously be used to decrypt messages in a completely different application that might be using the exact same underlying keys.

Advanced Usage: Associated Data (AD)

To prevent replay attacks and ensure context binding, you can provide Associated Data when encrypting messages. Both the sender and receiver must provide the exact same Associated Data (such as conversation IDs or protocol versions).

Kodium automatically handles appending the plaintext Ratchet header to the Associated Data to authenticate it during the AEAD encryption process.

Memory Management: Handling Skipped Messages

The Double Ratchet algorithm must store "skipped message keys" to handle messages that arrive out-of-order. If left unbounded, this could lead to memory exhaustion attacks.

Kodium automatically manages this using a Least Recently Used (LRU) eviction policy:

  • By default, each session stores up to 2,000 skipped message keys.

  • When this limit is reached, the oldest skipped key is automatically evicted to make room for new ones.

  • If an ancient message finally arrives after its key has been evicted, decryption will fail for that specific message, but the session remains healthy for all other traffic.

Configuring the Limit

You can customize this limit during session initialization if your application requires a different balance between memory usage and out-of-order resilience:

Persisting Sessions Between App Restarts

Since a Double Ratchet session manages several moving pieces (ratchet keys, chains, and out-of-order message keys), you will need to store its state securely between app restarts.

Kodium's DoubleRatchetSession offers simple APIs to export and import the entire session state as a securely encrypted, Base58-encoded string.

Last updated