#ratchet #double-ratchet #deterministic #encryption #async #kdf #async-messaging #message-header #unit-testing #xchacha20-poly1305

enigma-double-ratchet

Standalone Signal-style double ratchet focused on deterministic, reusable async messaging primitives

1 unstable release

0.1.0 Dec 15, 2025

#1969 in Cryptography


Used in enigma-protocol

MIT license

34KB
810 lines

enigma-ratchet

Rust implementation of a standalone Signal-style Double Ratchet optimized for reuse in asynchronous systems. The crate focuses on determinism, explicit error handling, strict serialization, and easy integration into higher level protocols.

Features

  • X25519 Diffie-Hellman ratchet with HKDF-SHA256 based root and chain KDFs
  • XChaCha20-Poly1305 authenticated encryption with caller-supplied associated data
  • Stable binary message header format with canonical encoding tests
  • Bounded skipped-key storage for out-of-order delivery and replay defense
  • Zeroization of all secret material on drop
  • Comprehensive unit tests covering protocol state, KDF behavior, framing, negative flows, and limit enforcement

Quickstart

use enigma_ratchet::{RatchetKeyPair, RatchetState};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let shared_secret = [42u8; 32];
    let bob_keys = RatchetKeyPair::generate();
    let bob_pub = bob_keys.public();

    let mut alice = RatchetState::new_alice(shared_secret, bob_pub, b"handshake")?;
    let mut bob = RatchetState::new_bob(shared_secret, bob_keys, b"handshake")?;

    let packet = alice.encrypt(b"hello", b"ad")?;
    let plaintext = bob.decrypt(&packet, b"ad")?;
    assert_eq!(plaintext, b"hello");

    let response = bob.encrypt(b"hi", b"ad")?;
    let echoed = alice.decrypt(&response, b"ad")?;
    assert_eq!(echoed, b"hi");
    Ok(())
}

new_bob does not know the remote key at creation time. Its set_remote_dh helper allows the responder to preload a known DH public key before the first ciphertext if the surrounding handshake provides it.

Serialization and Framing

Each ciphertext is framed as len(header) || header || nonce || ciphertext+tag where the header length is encoded in a two-byte big-endian integer and header length is always forty bytes. Headers contain the sender Diffie-Hellman public key plus message counters and are included in the AEAD associated data along with the application-provided context hash. The deterministic encoding is exercised in both positive and negative tests.

Testing

Run the full suite with:

cargo test

The tests cover KDF rules, header encoding, packet framing, sequential and out-of-order delivery, skip-limit enforcement, replay detection, corrupted packets, and mismatched initialization secrets. All critical responsibilities have dedicated assertions. Refer to the documents in docs/ for deeper protocol, API, and security notes.

Dependencies

~3–4.5MB
~85K SLoC