#e2ee #signal #double-ratchet #xeddsa #x-ed-dsa

ratchetx2

A double-ratchet implementation for building an E2EE message exchange app

20 releases

Uses new Rust 2024

new 0.3.7 May 13, 2025
0.3.6 May 13, 2025
0.3.4 Apr 28, 2025
0.2.2 Apr 26, 2025
0.1.5 Apr 25, 2025

#195 in Cryptography

Download history 321/week @ 2025-04-16 1205/week @ 2025-04-23 287/week @ 2025-04-30

1,813 downloads per month
Used in 2 crates

MIT license

71KB
1.5K SLoC

A double-ratchet implementation following Signal.

Also with X3DH and XEdDSA implementation.

E2EE chat app (TUI)

An E2EE chat server-client TUI app base on this crate is available here.

Compared to others

  1. There's no global party state, instead, it is each ratchet having its own state.
  2. It's really double-ratchet (2 kinds of ratchets), DhRootRatchet and MessageRatchet (AKA ChainRatchet).
  3. Header encryption support.
  4. Provide chat parties implementation.
  5. Provide gRPC transport implementation.
  6. Provide X3DH shared key initialization implementation.
  7. Provide XEdDSA implementation.

Features

  • grpc (enabled by default): enable tonic-based X3DHServer/Client and MessageServer/Client (protoc is needed).

Example

Ratchet only example:

use ratchetx2::SharedKeys;
use ratchetx2::rand::SystemRandom;
use ratchetx2::agreement::{EphemeralPrivateKey, X25519};

let shared_keys = SharedKeys {
    secret_key: [0; 32],
    header_key_alice: [1; 32],
    header_key_bob: [2; 32],
};
let mut bob = shared_keys.bob(EphemeralPrivateKey::generate(&X25519, &SystemRandom::new()).unwrap());
let mut alice = shared_keys.alice(&bob.public_key());

// Alice sends first
bob.step_dh_root(&alice.public_key());
assert_eq!(alice, bob); // Alice and Bob have the "same" state
assert_eq!(alice.step_msgs(), bob.step_msgr()); // returning the same message key
assert_eq!(alice.step_msgs(), bob.step_msgr());

// Bob sends
bob.step_dh_root(&alice.public_key());
alice.step_dh_root(&bob.public_key());
assert_eq!(alice, bob);
assert_eq!(bob.step_msgs(), alice.step_msgr());
assert_eq!(bob.step_msgs(), alice.step_msgr());

// Alice sends
alice.step_dh_root(&bob.public_key());
bob.step_dh_root(&alice.public_key());
assert_eq!(alice, bob);
assert_eq!(alice.step_msgs(), bob.step_msgr());
assert_eq!(alice.step_msgs(), bob.step_msgr());

E2EE chat app example:

use ratchetx2::{transport::ChannelTransport, Party, SharedKeys};
use ratchetx2::rand::SystemRandom;
use ratchetx2::agreement::{EphemeralPrivateKey, X25519};

# #[tokio::main]
# async fn main() {
let shared_keys = SharedKeys {
    secret_key: [0; 32],
    header_key_alice: [1; 32],
    header_key_bob: [2; 32],
};
let bob_ratchetx2 = shared_keys.bob(EphemeralPrivateKey::generate(&X25519, &SystemRandom::new()).unwrap());
let alice_ratchetx2 = shared_keys.alice(&bob_ratchetx2.public_key());
let (a, b) = ChannelTransport::new();
let mut alice = Party::new(alice_ratchetx2, a, "AliceBob");
let mut bob = Party::new(bob_ratchetx2, b, "AliceBob");
alice.push("hello world").await.unwrap();
assert_eq!(bob.fetch().await.unwrap().remove(0).unwrap(), b"hello world");
alice.push("hello Bob").await.unwrap();
assert_eq!(bob.fetch().await.unwrap().remove(0).unwrap(), b"hello Bob");
bob.push("hello Alice").await.unwrap();
assert_eq!(alice.fetch().await.unwrap().remove(0).unwrap(), b"hello Alice");
# }

XEdDSA example:

use ratchetx2::xeddsa::XEdDSAPrivateKey;
use ratchetx2::rand::SystemRandom;

let xeddsa = XEdDSAPrivateKey::generate(&SystemRandom::new());
let signature = xeddsa.sign("hello world");
let public_key = xeddsa.compute_public_key();
public_key.verify("hello world", &signature).unwrap();
assert!(public_key.verify("goodbye world", &signature).is_err());
let alice = XEdDSAPrivateKey::generate(&SystemRandom::new());
let bob = XEdDSAPrivateKey::generate(&SystemRandom::new());
assert_eq!(
    alice.agree_ephemeral(bob.compute_public_key().as_ref()).unwrap(),
    bob.agree_ephemeral(alice.compute_public_key().as_ref()).unwrap()
);

X3DH initialize example:

# #[cfg(feature = "grpc")]
# {
use ratchetx2::server::RpcServer;
use ratchetx2::x3dh::X3DHClient;

# #[tokio::main]
# async fn main() {
tokio::spawn(async {
    RpcServer::run("127.0.0.1:3002", None).await.unwrap();
});
// wait server start
tokio::time::sleep(std::time::Duration::from_millis(100)).await;

const SERVER_ADDR: &str = "http://127.0.0.1:3002";

let mut alice_x3dh = X3DHClient::connect(SERVER_ADDR, None, None).await.unwrap();
let mut bob_x3dh = X3DHClient::connect(SERVER_ADDR, None, None).await.unwrap();
bob_x3dh.publish_keys().await.unwrap();
let mut alice = alice_x3dh
    .push_initial_message(&bob_x3dh.public_identity_key(), SERVER_ADDR)
    .await
    .unwrap();
assert_eq!(
    bob_x3dh.list_attempt(&bob_x3dh.public_identity_key()).await.unwrap().pop().unwrap(),
    alice_x3dh.public_identity_key()
);
let mut bob = bob_x3dh
    .handle_initial_message(&alice_x3dh.public_identity_key(), SERVER_ADDR)
    .await
    .unwrap();
alice.push("hello world").await.unwrap();
assert_eq!(
    bob.fetch().await.unwrap().remove(0).unwrap(),
    b"hello world"
);
alice.push("hello Bob").await.unwrap();
assert_eq!(
    bob.fetch().await.unwrap().remove(0).unwrap(),
    b"hello Bob"
);
bob.push("hello Alice").await.unwrap();
assert_eq!(
    alice.fetch().await.unwrap().remove(0).unwrap(),
    b"hello Alice"
);
# }
# }

Dependencies

~14–24MB
~438K SLoC