Merge 3797eca0bfd87ac5960488507aedc2c48f05e954 into 7c580b5896196735ec09bd0e0d41377a98a9b29c

This commit is contained in:
Jazz Turner-Baggs 2026-01-30 17:59:48 -08:00 committed by GitHub
commit aa805529dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 251 additions and 211 deletions

2
Cargo.lock generated
View File

@ -218,6 +218,7 @@ version = "0.0.1"
dependencies = [
"blake2",
"chacha20poly1305",
"crypto",
"hkdf",
"rand",
"rand_core",
@ -473,6 +474,7 @@ dependencies = [
"crypto",
"hex",
"prost",
"rand",
"rand_core",
"safer-ffi",
"thiserror",

View File

@ -13,6 +13,7 @@ crypto = { path = "../crypto" }
hex = "0.4.3"
prost = "0.14.1"
rand_core = { version = "0.6" }
rand = "0.8.5"
safer-ffi = "0.1.13"
thiserror = "2.0.17"
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }

View File

@ -2,7 +2,7 @@ use chat_proto::logoschat::{
convos::private_v1::{PrivateV1Frame, private_v1_frame::FrameType},
encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption},
};
use crypto::SecretKey;
use crypto::SecretKey32;
use prost::{Message, bytes::Bytes};
use crate::{
@ -15,7 +15,7 @@ use crate::{
pub struct PrivateV1Convo {}
impl PrivateV1Convo {
pub fn new(_seed_key: SecretKey) -> Self {
pub fn new(_seed_key: SecretKey32) -> Self {
Self {}
}

View File

@ -1,13 +1,15 @@
pub use blake2::Digest;
use blake2::{Blake2b, digest};
use prost::bytes::Bytes;
pub use x25519_dalek::{PublicKey, StaticSecret};
pub use crypto::{PrivateKey32, PublicKey32};
// TODO: (P4) Make handing of Keys in Prost easier
pub trait CopyBytes {
fn copy_to_bytes(&self) -> Bytes;
}
impl CopyBytes for PublicKey {
impl CopyBytes for PublicKey32 {
fn copy_to_bytes(&self) -> Bytes {
Bytes::copy_from_slice(self.as_bytes())
}

View File

@ -1,10 +1,10 @@
use blake2::{Blake2b512, Digest};
use std::fmt;
use crate::crypto::{PublicKey, StaticSecret};
use crate::crypto::{PrivateKey32, PublicKey32};
pub struct Identity {
secret: StaticSecret,
secret: PrivateKey32,
}
impl fmt::Debug for Identity {
@ -19,7 +19,7 @@ impl fmt::Debug for Identity {
impl Identity {
pub fn new() -> Self {
Self {
secret: StaticSecret::random(),
secret: PrivateKey32::random(),
}
}
@ -27,11 +27,11 @@ impl Identity {
hex::encode(Blake2b512::digest(self.public_key()))
}
pub fn public_key(&self) -> PublicKey {
PublicKey::from(&self.secret)
pub fn public_key(&self) -> PublicKey32 {
PublicKey32::from(&self.secret)
}
pub fn secret(&self) -> &StaticSecret {
pub fn secret(&self) -> &PrivateKey32 {
&self.secret
}
}

View File

@ -2,10 +2,10 @@ use blake2::{
Blake2bMac,
digest::{FixedOutput, consts::U32},
};
use crypto::{DomainSeparator, PrekeyBundle, SecretKey, X3Handshake};
use crypto::{DomainSeparator, PrekeyBundle, SecretKey32, X3Handshake};
use rand_core::{CryptoRng, RngCore};
use crate::crypto::{PublicKey, StaticSecret};
use crate::crypto::{PrivateKey32, PublicKey32};
type Blake2bMac256 = Blake2bMac<U32>;
@ -21,16 +21,16 @@ pub struct InboxHandshake {}
impl InboxHandshake {
/// Performs
pub fn perform_as_initiator<R: RngCore + CryptoRng>(
identity_keypair: &StaticSecret,
identity_keypair: &PrivateKey32,
recipient_bundle: &PrekeyBundle,
rng: &mut R,
) -> (SecretKey, PublicKey) {
) -> (SecretKey32, PublicKey32) {
// Perform X3DH handshake to get shared secret
let (shared_secret, ephemeral_public) =
InboxKeyExchange::initator(identity_keypair, recipient_bundle, rng);
let seed_key = Self::derive_keys_from_shared_secret(shared_secret);
(seed_key, ephemeral_public)
(seed_key, ephemeral_public.into())
}
/// Perform the Inbox Handshake after receiving a keyBundle
@ -42,12 +42,12 @@ impl InboxHandshake {
/// * `initiator_identity` - Initiator's identity public key
/// * `initiator_ephemeral` - Initiator's ephemeral public key
pub fn perform_as_responder(
identity_keypair: &StaticSecret,
signed_prekey: &StaticSecret,
onetime_prekey: Option<&StaticSecret>,
initiator_identity: &PublicKey,
initiator_ephemeral: &PublicKey,
) -> SecretKey {
identity_keypair: &PrivateKey32,
signed_prekey: &PrivateKey32,
onetime_prekey: Option<&PrivateKey32>,
initiator_identity: &PublicKey32,
initiator_ephemeral: &PublicKey32,
) -> SecretKey32 {
// Perform X3DH to get shared secret
let shared_secret = InboxKeyExchange::responder(
identity_keypair,
@ -61,7 +61,7 @@ impl InboxHandshake {
}
/// Derive keys from X3DH shared secret
fn derive_keys_from_shared_secret(shared_secret: SecretKey) -> SecretKey {
fn derive_keys_from_shared_secret(shared_secret: SecretKey32) -> SecretKey32 {
let seed_key: [u8; 32] = Blake2bMac256::new_with_salt_and_personal(
shared_secret.as_bytes(),
&[], // No salt - input already has high entropy
@ -85,17 +85,17 @@ mod tests {
let mut rng = OsRng;
// Alice (initiator) generates her identity key
let alice_identity = StaticSecret::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey::from(&alice_identity);
let alice_identity = PrivateKey32::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey32::from(&alice_identity);
// Bob (responder) generates his keys
let bob_identity = StaticSecret::random_from_rng(&mut rng);
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
let bob_identity = PrivateKey32::random_from_rng(&mut rng);
let bob_signed_prekey = PrivateKey32::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey32::from(&bob_signed_prekey);
// Create Bob's prekey bundle
let bob_bundle = PrekeyBundle {
identity_key: PublicKey::from(&bob_identity),
identity_key: PublicKey32::from(&bob_identity),
signed_prekey: bob_signed_prekey_pub,
signature: [0u8; 64],
onetime_prekey: None,

View File

@ -5,18 +5,18 @@ use rand_core::OsRng;
use std::collections::HashMap;
use std::rc::Rc;
use crypto::{PrekeyBundle, SecretKey};
use crypto::{PrekeyBundle, SecretKey32};
use crate::context::Introduction;
use crate::conversation::{ChatError, ConversationId, Convo, ConvoFactory, Id, PrivateV1Convo};
use crate::crypto::{Blake2b128, CopyBytes, Digest, PublicKey, StaticSecret};
use crate::crypto::{Blake2b128, CopyBytes, Digest, PrivateKey32, PublicKey32};
use crate::identity::Identity;
use crate::inbox::handshake::InboxHandshake;
use crate::proto;
use crate::types::{AddressedEncryptedPayload, ContentData};
/// Compute the deterministic Delivery_address for an installation
fn delivery_address_for_installation(_: PublicKey) -> String {
fn delivery_address_for_installation(_: PublicKey32) -> String {
// TODO: Implement Delivery Address
"delivery_address".into()
}
@ -24,7 +24,7 @@ fn delivery_address_for_installation(_: PublicKey) -> String {
pub struct Inbox {
ident: Rc<Identity>,
local_convo_id: String,
ephemeral_keys: HashMap<String, StaticSecret>,
ephemeral_keys: HashMap<String, PrivateKey32>,
}
impl<'a> std::fmt::Debug for Inbox {
@ -46,7 +46,7 @@ impl Inbox {
Self {
ident,
local_convo_id,
ephemeral_keys: HashMap::<String, StaticSecret>::new(),
ephemeral_keys: HashMap::<String, PrivateKey32>::new(),
}
}
@ -56,9 +56,9 @@ impl Inbox {
}
pub fn create_bundle(&mut self) -> PrekeyBundle {
let ephemeral = StaticSecret::random();
let ephemeral = PrivateKey32::random_from_rng(&mut OsRng);
let signed_prekey = PublicKey::from(&ephemeral);
let signed_prekey = PublicKey32::from(&ephemeral);
self.ephemeral_keys
.insert(hex::encode(signed_prekey.as_bytes()), ephemeral);
@ -140,7 +140,7 @@ impl Inbox {
fn perform_handshake(
&self,
payload: proto::EncryptedPayload,
) -> Result<(SecretKey, proto::InboxV1Frame), ChatError> {
) -> Result<(SecretKey32, proto::InboxV1Frame), ChatError> {
let handshake = Self::extract_payload(payload)?;
let header = handshake
.header
@ -149,12 +149,12 @@ impl Inbox {
let ephemeral_key = self.lookup_ephemeral_key(&pubkey_hex)?;
let initator_static = PublicKey::from(
let initator_static = PublicKey32::from(
<[u8; 32]>::try_from(header.initiator_static.as_ref())
.map_err(|_| ChatError::BadBundleValue("wrong size - initator static".into()))?,
);
let initator_ephemeral = PublicKey::from(
let initator_ephemeral = PublicKey32::from(
<[u8; 32]>::try_from(header.initiator_ephemeral.as_ref())
.map_err(|_| ChatError::BadBundleValue("wrong size - initator ephemeral".into()))?,
);
@ -193,7 +193,7 @@ impl Inbox {
Ok(frame)
}
fn lookup_ephemeral_key(&self, key: &str) -> Result<&StaticSecret, ChatError> {
fn lookup_ephemeral_key(&self, key: &str) -> Result<&PrivateKey32, ChatError> {
self.ephemeral_keys
.get(key)
.ok_or_else(|| return ChatError::UnknownEphemeralKey())

View File

@ -1,12 +1,12 @@
use crypto::PrekeyBundle;
use x25519_dalek::PublicKey;
use crate::crypto::PublicKey32;
use crate::errors::ChatError;
/// Supplies remote participants with the required keys to use Inbox protocol
pub struct Introduction {
pub installation_key: PublicKey,
pub ephemeral_key: PublicKey,
pub installation_key: PublicKey32,
pub ephemeral_key: PublicKey32,
}
impl From<PrekeyBundle> for Introduction {
@ -48,13 +48,13 @@ impl TryFrom<&[u8]> for Introduction {
.map_err(|_| ChatError::BadParsing("installation_key"))?
.try_into()
.map_err(|_| ChatError::InvalidKeyLength)?;
let installation_key = PublicKey::from(installation_bytes);
let installation_key = PublicKey32::from(installation_bytes);
let ephemeral_bytes: [u8; 32] = hex::decode(parts[1])
.map_err(|_| ChatError::BadParsing("ephemeral_key"))?
.try_into()
.map_err(|_| ChatError::InvalidKeyLength)?;
let ephemeral_key = PublicKey::from(ephemeral_bytes);
let ephemeral_key = PublicKey32::from(ephemeral_bytes);
Ok(Introduction {
installation_key,

View File

@ -1,30 +1,100 @@
use std::fmt::Debug;
pub use generic_array::{GenericArray, typenum::U32};
use generic_array::{GenericArray, typenum::U32};
use rand_core::{CryptoRng, OsRng, RngCore};
use std::{
fmt::Debug,
ops::{Deref, DerefMut},
};
use x25519_dalek;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq)]
pub struct SecretKey([u8; 32]);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct PublicKey32(x25519_dalek::PublicKey);
impl SecretKey {
impl From<&PrivateKey32> for PublicKey32 {
fn from(value: &PrivateKey32) -> Self {
Self(x25519_dalek::PublicKey::from(&value.0))
}
}
impl From<[u8; 32]> for PublicKey32 {
fn from(value: [u8; 32]) -> Self {
Self(x25519_dalek::PublicKey::from(value))
}
}
impl Deref for PublicKey32 {
type Target = x25519_dalek::PublicKey;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for PublicKey32 {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8]> for PublicKey32 {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct PrivateKey32(x25519_dalek::StaticSecret);
impl PrivateKey32 {
pub fn random_from_rng<T: RngCore + CryptoRng>(mut csprng: T) -> Self {
Self(x25519_dalek::StaticSecret::random_from_rng(csprng))
}
//TODO: Remove. Force internal callers provide Rng to make deterministic testing possible
pub fn random() -> PrivateKey32 {
Self::random_from_rng(&mut OsRng)
}
// Convenience function to generate a PublicKey32
pub fn public_key(&self) -> PublicKey32 {
PublicKey32::from(self)
}
}
impl From<[u8; 32]> for PrivateKey32 {
fn from(value: [u8; 32]) -> Self {
Self(x25519_dalek::StaticSecret::from(value))
}
}
impl Deref for PrivateKey32 {
type Target = x25519_dalek::StaticSecret;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq)]
pub struct SecretKey32([u8; 32]);
impl SecretKey32 {
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
}
impl From<[u8; 32]> for SecretKey {
impl From<[u8; 32]> for SecretKey32 {
fn from(value: [u8; 32]) -> Self {
SecretKey(value)
SecretKey32(value)
}
}
impl From<GenericArray<u8, U32>> for SecretKey {
impl From<GenericArray<u8, U32>> for SecretKey32 {
fn from(value: GenericArray<u8, U32>) -> Self {
SecretKey(value.into())
SecretKey32(value.into())
}
}
impl Debug for SecretKey {
impl Debug for SecretKey32 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SecretKey").field(&"<32 bytes>").finish()
}

View File

@ -1,5 +1,5 @@
mod keys;
mod x3dh;
pub use keys::{GenericArray, SecretKey};
pub use keys::{PrivateKey32, PublicKey32, SecretKey32};
pub use x3dh::{DomainSeparator, PrekeyBundle, X3Handshake};

View File

@ -3,17 +3,17 @@ use std::marker::PhantomData;
use hkdf::Hkdf;
use rand_core::{CryptoRng, RngCore};
use sha2::Sha256;
use x25519_dalek::{PublicKey, SharedSecret, StaticSecret};
use x25519_dalek::SharedSecret;
use crate::keys::SecretKey;
use crate::keys::{PrivateKey32, PublicKey32, SecretKey32};
/// A prekey bundle containing the public keys needed to initiate an X3DH key exchange.
#[derive(Clone, Debug)]
pub struct PrekeyBundle {
pub identity_key: PublicKey,
pub signed_prekey: PublicKey,
pub identity_key: PublicKey32,
pub signed_prekey: PublicKey32,
pub signature: [u8; 64],
pub onetime_prekey: Option<PublicKey>,
pub onetime_prekey: Option<PublicKey32>,
}
pub trait DomainSeparator {
@ -35,7 +35,7 @@ impl<D: DomainSeparator> X3Handshake<D> {
dh2: &SharedSecret,
dh3: &SharedSecret,
dh4: Option<&SharedSecret>,
) -> SecretKey {
) -> SecretKey32 {
// Concatenate all DH outputs
let mut km = Vec::new();
km.extend_from_slice(dh1.as_bytes());
@ -52,7 +52,7 @@ impl<D: DomainSeparator> X3Handshake<D> {
hk.expand(Self::domain_separator(), &mut output)
.expect("32 bytes is valid HKDF output length");
// Move into SecretKey so it gets zeroized on drop.
// Move into SecretKey32 so it gets zeroized on drop.
output.into()
}
@ -66,13 +66,13 @@ impl<D: DomainSeparator> X3Handshake<D> {
/// # Returns
/// A tuple of (shared secret bytes, ephemeral public key)
pub fn initator<R: RngCore + CryptoRng>(
identity_keypair: &StaticSecret,
identity_keypair: &PrivateKey32,
recipient_bundle: &PrekeyBundle,
rng: &mut R,
) -> (SecretKey, PublicKey) {
// Generate ephemeral key for this handshake (using StaticSecret for multiple DH operations)
let ephemeral_secret = StaticSecret::random_from_rng(rng);
let ephemeral_public = PublicKey::from(&ephemeral_secret);
) -> (SecretKey32, PublicKey32) {
// Generate ephemeral key for this handshake
let ephemeral_secret = PrivateKey32::random_from_rng(rng);
let ephemeral_public = PublicKey32::from(&ephemeral_secret);
// Perform the 4 Diffie-Hellman operations
let dh1 = identity_keypair.diffie_hellman(&recipient_bundle.signed_prekey);
@ -101,12 +101,12 @@ impl<D: DomainSeparator> X3Handshake<D> {
/// # Returns
/// The derived shared secret bytes
pub fn responder(
identity_keypair: &StaticSecret,
signed_prekey: &StaticSecret,
onetime_prekey: Option<&StaticSecret>,
initiator_identity: &PublicKey,
initiator_ephemeral: &PublicKey,
) -> SecretKey {
identity_keypair: &PrivateKey32,
signed_prekey: &PrivateKey32,
onetime_prekey: Option<&PrivateKey32>,
initiator_identity: &PublicKey32,
initiator_ephemeral: &PublicKey32,
) -> SecretKey32 {
let dh1 = signed_prekey.diffie_hellman(initiator_identity);
let dh2 = identity_keypair.diffie_hellman(initiator_ephemeral);
let dh3 = signed_prekey.diffie_hellman(initiator_ephemeral);
@ -134,18 +134,18 @@ mod tests {
let mut rng = OsRng;
// Alice (initiator) generates her identity key
let alice_identity = StaticSecret::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey::from(&alice_identity);
let alice_identity = PrivateKey32::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey32::from(&alice_identity);
// Bob (responder) generates his keys
let bob_identity = StaticSecret::random_from_rng(&mut rng);
let bob_identity_pub = PublicKey::from(&bob_identity);
let bob_identity = PrivateKey32::random_from_rng(&mut rng);
let bob_identity_pub = PublicKey32::from(&bob_identity);
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
let bob_signed_prekey = PrivateKey32::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey32::from(&bob_signed_prekey);
let bob_onetime_prekey = StaticSecret::random_from_rng(&mut rng);
let bob_onetime_prekey_pub = PublicKey::from(&bob_onetime_prekey);
let bob_onetime_prekey = PrivateKey32::random_from_rng(&mut rng);
let bob_onetime_prekey_pub = PublicKey32::from(&bob_onetime_prekey);
// Create Bob's prekey bundle (with one-time prekey)
let bob_bundle = PrekeyBundle {
@ -177,15 +177,15 @@ mod tests {
let mut rng = OsRng;
// Alice (initiator) generates her identity key
let alice_identity = StaticSecret::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey::from(&alice_identity);
let alice_identity = PrivateKey32::random_from_rng(&mut rng);
let alice_identity_pub = PublicKey32::from(&alice_identity);
// Bob (responder) generates his keys
let bob_identity = StaticSecret::random_from_rng(&mut rng);
let bob_identity_pub = PublicKey::from(&bob_identity);
let bob_identity = PrivateKey32::random_from_rng(&mut rng);
let bob_identity_pub = PublicKey32::from(&bob_identity);
let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey);
let bob_signed_prekey = PrivateKey32::random_from_rng(&mut rng);
let bob_signed_prekey_pub = PublicKey32::from(&bob_signed_prekey);
// Create Bob's prekey bundle (without one-time prekey)
let bob_bundle = PrekeyBundle {

View File

@ -11,8 +11,8 @@ name = "generate-headers"
required-features = ["headers"]
[dependencies]
x25519-dalek = { version="2.0.1", features=["static_secrets"] }
chacha20poly1305 = "0.10.1"
crypto = { path = "../crypto" }
rand_core = "0.6.4"
rand = "0.8.5"
hkdf = "0.12.4"

View File

@ -1,13 +1,14 @@
use double_ratchets::{InstallationKeyPair, RatchetState, hkdf::PrivateV1Domain};
use crypto::PrivateKey32;
use double_ratchets::{RatchetState, hkdf::PrivateV1Domain};
fn main() {
// === Initial shared secret (X3DH / prekey result in real systems) ===
let shared_secret = [42u8; 32];
let bob_dh = InstallationKeyPair::generate();
let bob_dh = PrivateKey32::random();
let mut alice: RatchetState<PrivateV1Domain> =
RatchetState::init_sender(shared_secret, bob_dh.public().clone());
RatchetState::init_sender(shared_secret, bob_dh.public_key());
let mut bob: RatchetState<PrivateV1Domain> = RatchetState::init_receiver(shared_secret, bob_dh);
let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!");

View File

@ -1,11 +1,11 @@
//! Demonstrates out-of-order message handling with skipped keys persistence.
//!
//! Run with: cargo run --example out_of_order_demo --features storage
#[allow(unused_imports)]
use crypto::PrivateKey32;
#[cfg(feature = "storage")]
use double_ratchets::{
InstallationKeyPair, RatchetState, SqliteStorage, StorageConfig, hkdf::DefaultDomain,
state::Header,
RatchetState, SqliteStorage, StorageConfig, hkdf::DefaultDomain, state::Header,
};
fn main() {
@ -22,10 +22,10 @@ fn run_demo() {
// Setup
let shared_secret = [0x42u8; 32];
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = PrivateKey32::random();
let alice_state: RatchetState<DefaultDomain> =
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
let bob_state: RatchetState<DefaultDomain> =
RatchetState::init_receiver(shared_secret, bob_keypair);
@ -81,9 +81,9 @@ fn run_demo() {
.expect("Failed to create storage");
// Re-setup
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = PrivateKey32::random();
let alice_state: RatchetState<DefaultDomain> =
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
let bob_state: RatchetState<DefaultDomain> =
RatchetState::init_receiver(shared_secret, bob_keypair);

View File

@ -1,13 +1,13 @@
use double_ratchets::{InstallationKeyPair, RatchetState, hkdf::PrivateV1Domain};
use double_ratchets::{RatchetState, hkdf::PrivateV1Domain, types::DhPrivateKey};
fn main() {
// === Initial shared secret (X3DH / prekey result in real systems) ===
let shared_secret = [42u8; 32];
let bob_dh = InstallationKeyPair::generate();
let bob_dh = DhPrivateKey::random();
let mut alice: RatchetState<PrivateV1Domain> =
RatchetState::init_sender(shared_secret, bob_dh.public().clone());
RatchetState::init_sender(shared_secret, bob_dh.public_key().clone());
let mut bob: RatchetState<PrivateV1Domain> = RatchetState::init_receiver(shared_secret, bob_dh);
let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!");

View File

@ -3,10 +3,10 @@
//! Run with: cargo run --example storage_demo --features storage
//! For SQLCipher: cargo run --example storage_demo --features sqlcipher
#[allow(unused_imports)]
use crypto::PrivateKey32;
#[cfg(feature = "storage")]
use double_ratchets::{
InstallationKeyPair, RatchetSession, SqliteStorage, StorageConfig, hkdf::PrivateV1Domain,
};
use double_ratchets::{RatchetSession, SqliteStorage, StorageConfig, hkdf::PrivateV1Domain};
fn main() {
println!("=== Double Ratchet Storage Demo ===\n");
@ -140,7 +140,7 @@ fn ensure_tmp_directory() {
fn run_conversation(alice_storage: &mut SqliteStorage, bob_storage: &mut SqliteStorage) {
// === Setup: Simulate X3DH key exchange ===
let shared_secret = [0x42u8; 32]; // In reality, this comes from X3DH
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = PrivateKey32::random();
let conv_id = "conv1";
@ -148,7 +148,7 @@ fn run_conversation(alice_storage: &mut SqliteStorage, bob_storage: &mut SqliteS
alice_storage,
conv_id,
shared_secret,
bob_keypair.public().clone(),
bob_keypair.public_key(),
)
.unwrap();

View File

@ -1,5 +1,5 @@
use crypto::PublicKey32;
use safer_ffi::prelude::*;
use x25519_dalek::PublicKey;
use crate::{
Header, RatchetState,
@ -22,7 +22,7 @@ fn double_ratchet_init_sender(
shared_secret: [u8; 32],
remote_pub: [u8; 32],
) -> repr_c::Box<FFIRatchetState> {
let state = RatchetState::init_sender(shared_secret, PublicKey::from(remote_pub));
let state = RatchetState::init_sender(shared_secret, PublicKey32::from(remote_pub));
Box::new(FFIRatchetState(state)).into()
}

View File

@ -1,19 +1,18 @@
use crypto::PrivateKey32;
use safer_ffi::prelude::*;
use crate::InstallationKeyPair;
#[derive_ReprC]
#[repr(opaque)]
pub struct FFIInstallationKeyPair(pub(crate) InstallationKeyPair);
pub struct FFIInstallationKeyPair(pub(crate) PrivateKey32);
#[ffi_export]
fn installation_key_pair_generate() -> repr_c::Box<FFIInstallationKeyPair> {
Box::new(FFIInstallationKeyPair(InstallationKeyPair::generate())).into()
Box::new(FFIInstallationKeyPair(PrivateKey32::random())).into()
}
#[ffi_export]
fn installation_key_pair_public(keypair: &FFIInstallationKeyPair) -> [u8; 32] {
keypair.0.public().clone().to_bytes()
keypair.0.public_key().to_bytes()
}
#[ffi_export]

View File

@ -1,39 +0,0 @@
use rand_core::OsRng;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::types::SharedSecret;
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct InstallationKeyPair {
secret: StaticSecret,
public: PublicKey,
}
impl InstallationKeyPair {
pub fn generate() -> Self {
let secret = StaticSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret);
Self { secret, public }
}
pub fn dh(&self, their_public: &PublicKey) -> SharedSecret {
self.secret.diffie_hellman(their_public).to_bytes()
}
pub fn public(&self) -> &PublicKey {
&self.public
}
/// Export the secret key as raw bytes for serialization/storage.
pub fn secret_bytes(&self) -> &[u8; 32] {
self.secret.as_bytes()
}
/// Import the secret key from raw bytes.
pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
let secret = StaticSecret::from(bytes);
let public = PublicKey::from(&secret);
Self { secret, public }
}
}

View File

@ -2,14 +2,12 @@ pub mod aead;
pub mod errors;
pub mod ffi;
pub mod hkdf;
pub mod keypair;
pub mod reader;
pub mod state;
#[cfg(feature = "storage")]
pub mod storage;
pub mod types;
pub use keypair::InstallationKeyPair;
pub use state::{Header, RatchetState};
#[cfg(feature = "storage")]
pub use storage::{RatchetSession, SessionError, SqliteStorage, StorageConfig, StorageError};

View File

@ -1,16 +1,15 @@
use std::{collections::HashMap, marker::PhantomData};
use crypto::PublicKey32;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
use x25519_dalek::PublicKey;
use zeroize::{Zeroize, Zeroizing};
use crate::{
aead::{decrypt, encrypt},
errors::RatchetError,
hkdf::{DefaultDomain, HkdfInfo, kdf_chain, kdf_root},
keypair::InstallationKeyPair,
reader::Reader,
types::{ChainKey, MessageKey, Nonce, RootKey, SharedSecret},
types::{ChainKey, DhPrivateKey, MessageKey, Nonce, RootKey, SharedSecret},
};
/// Current binary format version.
@ -28,14 +27,14 @@ pub struct RatchetState<D: HkdfInfo = DefaultDomain> {
pub sending_chain: Option<ChainKey>,
pub receiving_chain: Option<ChainKey>,
pub dh_self: InstallationKeyPair,
pub dh_remote: Option<PublicKey>,
pub dh_self: DhPrivateKey,
pub dh_remote: Option<PublicKey32>,
pub msg_send: u32,
pub msg_recv: u32,
pub prev_chain_len: u32,
pub skipped_keys: HashMap<(PublicKey, u32), MessageKey>,
pub skipped_keys: HashMap<(PublicKey32, u32), MessageKey>,
pub(crate) _domain: PhantomData<D>,
}
@ -104,8 +103,7 @@ impl<D: HkdfInfo> RatchetState<D> {
write_option(&mut buf, self.sending_chain);
write_option(&mut buf, self.receiving_chain);
let dh_secret = self.dh_self.secret_bytes();
buf.extend_from_slice(dh_secret);
buf.extend_from_slice(self.dh_self.as_bytes());
write_option(&mut buf, dh_remote);
@ -141,10 +139,10 @@ impl<D: HkdfInfo> RatchetState<D> {
let receiving_chain = reader.read_option()?;
let mut dh_self_bytes: [u8; 32] = reader.read_array()?;
let dh_self = InstallationKeyPair::from_secret_bytes(dh_self_bytes);
let dh_self = DhPrivateKey::from(dh_self_bytes);
dh_self_bytes.zeroize();
let dh_remote = reader.read_option()?.map(PublicKey::from);
let dh_remote = reader.read_option()?.map(PublicKey32::from);
let msg_send = reader.read_u32()?;
let msg_recv = reader.read_u32()?;
@ -153,7 +151,7 @@ impl<D: HkdfInfo> RatchetState<D> {
let skipped_count = reader.read_u32()? as usize;
let mut skipped_keys = HashMap::with_capacity(skipped_count);
for _ in 0..skipped_count {
let pk = PublicKey::from(reader.read_array::<32>()?);
let pk = PublicKey32::from(reader.read_array::<32>()?);
let msg_num = reader.read_u32()?;
let mk: MessageKey = reader.read_array()?;
skipped_keys.insert((pk, msg_num), mk);
@ -198,7 +196,7 @@ impl<'de, D: HkdfInfo> Deserialize<'de> for RatchetState<D> {
/// Public header attached to every encrypted message (unencrypted but authenticated).
#[derive(Clone, Debug)]
pub struct Header {
pub dh_pub: PublicKey,
pub dh_pub: PublicKey32,
pub msg_num: u32,
pub prev_chain_len: u32,
}
@ -233,12 +231,12 @@ impl<D: HkdfInfo> RatchetState<D> {
/// # Returns
///
/// A new `RatchetState` ready to send the first message.
pub fn init_sender(shared_secret: SharedSecret, remote_pub: PublicKey) -> Self {
let dh_self = InstallationKeyPair::generate();
pub fn init_sender(shared_secret: SharedSecret, remote_pub: PublicKey32) -> Self {
let dh_self = DhPrivateKey::random();
// Initial DH
let dh_out = dh_self.dh(&remote_pub);
let (root_key, sending_chain) = kdf_root::<D>(&shared_secret, &dh_out);
let dh_out = dh_self.diffie_hellman(&remote_pub);
let (root_key, sending_chain) = kdf_root::<D>(&shared_secret, dh_out.as_bytes());
Self {
root_key,
@ -271,7 +269,7 @@ impl<D: HkdfInfo> RatchetState<D> {
/// # Returns
///
/// A new `RatchetState` ready to receive the first message.
pub fn init_receiver(shared_secret: SharedSecret, dh_self: InstallationKeyPair) -> Self {
pub fn init_receiver(shared_secret: SharedSecret, dh_self: DhPrivateKey) -> Self {
Self {
root_key: shared_secret,
@ -296,9 +294,9 @@ impl<D: HkdfInfo> RatchetState<D> {
/// # Arguments
///
/// * `remote_pub` - The new DH public key from the sender.
pub fn dh_ratchet_receive(&mut self, remote_pub: PublicKey) {
let dh_out = self.dh_self.dh(&remote_pub);
let (new_root, recv_chain) = kdf_root::<D>(&self.root_key, &dh_out);
pub fn dh_ratchet_receive(&mut self, remote_pub: PublicKey32) {
let dh_out = self.dh_self.diffie_hellman(&remote_pub);
let (new_root, recv_chain) = kdf_root::<D>(&self.root_key, dh_out.as_bytes());
self.root_key = new_root;
self.receiving_chain = Some(recv_chain);
@ -312,9 +310,9 @@ impl<D: HkdfInfo> RatchetState<D> {
pub fn dh_ratchet_send(&mut self) {
let remote = self.dh_remote.expect("no remote DH key");
self.dh_self = InstallationKeyPair::generate();
let dh_out = self.dh_self.dh(&remote);
let (new_root, send_chain) = kdf_root::<D>(&self.root_key, &dh_out);
self.dh_self = DhPrivateKey::random();
let dh_out = self.dh_self.diffie_hellman(&remote);
let (new_root, send_chain) = kdf_root::<D>(&self.root_key, dh_out.as_bytes());
self.root_key = new_root;
self.sending_chain = Some(send_chain);
@ -345,7 +343,7 @@ impl<D: HkdfInfo> RatchetState<D> {
*chain = next_chain;
let header = Header {
dh_pub: self.dh_self.public().clone(),
dh_pub: PublicKey32::from(&self.dh_self),
msg_num: self.msg_send,
prev_chain_len: self.prev_chain_len,
};
@ -478,10 +476,10 @@ mod tests {
let shared_secret = [0x42; 32];
// Bob generates his long-term keypair
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = DhPrivateKey::random();
// Alice initializes as sender, knowing Bob's public key
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public_key());
// Bob initializes as receiver with his private key
let bob = RatchetState::init_receiver(shared_secret, bob_keypair);
@ -554,7 +552,7 @@ mod tests {
bob.decrypt_message(&ct, header).unwrap();
// Bob performs DH ratchet by trying to send
let old_bob_pub = bob.dh_self.public().clone();
let old_bob_pub = bob.dh_self.public_key();
let (bob_ct, bob_header) = {
let mut b = bob.clone();
b.encrypt_message(b"reply")
@ -562,7 +560,7 @@ mod tests {
assert_ne!(bob_header.dh_pub, old_bob_pub);
// Alice receives Bob's message with new DH pub → ratchets
let old_alice_pub = alice.dh_self.public().clone();
let old_alice_pub = alice.dh_self.public_key();
let old_root = alice.root_key;
// Even if decrypt fails (wrong key), ratchet should happen
@ -605,7 +603,7 @@ mod tests {
// Tamper with header (change DH pub byte)
let mut tampered_pub_bytes = header.dh_pub.to_bytes();
tampered_pub_bytes[0] ^= 0xff;
header.dh_pub = PublicKey::from(tampered_pub_bytes);
header.dh_pub = PublicKey32::from(tampered_pub_bytes);
let result = bob.decrypt_message(&ct, header);
assert!(result.is_err());
@ -688,8 +686,8 @@ mod tests {
restored.dh_remote.map(|pk| pk.to_bytes())
);
assert_eq!(
alice.dh_self.public().to_bytes(),
restored.dh_self.public().to_bytes()
alice.dh_self.public_key().to_bytes(),
restored.dh_self.public_key().to_bytes()
);
}

View File

@ -1,11 +1,10 @@
use x25519_dalek::PublicKey;
use crypto::PublicKey32;
use crate::{
InstallationKeyPair,
errors::RatchetError,
hkdf::HkdfInfo,
state::{Header, RatchetState},
types::SharedSecret,
types::{DhPrivateKey, SharedSecret},
};
use super::{SqliteStorage, StorageError};
@ -82,7 +81,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> {
storage: &'a mut SqliteStorage,
conversation_id: impl Into<String>,
shared_secret: SharedSecret,
remote_pub: PublicKey,
remote_pub: PublicKey32,
) -> Result<Self, StorageError> {
let state = RatchetState::<D>::init_sender(shared_secret, remote_pub);
Self::create(storage, conversation_id, state)
@ -93,7 +92,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> {
storage: &'a mut SqliteStorage,
conversation_id: impl Into<String>,
shared_secret: SharedSecret,
dh_self: InstallationKeyPair,
dh_self: DhPrivateKey,
) -> Result<Self, StorageError> {
let conversation_id = conversation_id.into();
if storage.exists(&conversation_id)? {
@ -180,7 +179,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{hkdf::DefaultDomain, keypair::InstallationKeyPair, storage::StorageConfig};
use crate::{hkdf::DefaultDomain, storage::StorageConfig, types::DhPrivateKey};
fn create_test_storage() -> SqliteStorage {
SqliteStorage::new(StorageConfig::InMemory).unwrap()
@ -191,9 +190,9 @@ mod tests {
let mut storage = create_test_storage();
let shared_secret = [0x42; 32];
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = DhPrivateKey::random();
let alice: RatchetState<DefaultDomain> =
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
// Create session
{
@ -214,9 +213,9 @@ mod tests {
let mut storage = create_test_storage();
let shared_secret = [0x42; 32];
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = DhPrivateKey::random();
let alice: RatchetState<DefaultDomain> =
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
// Create and encrypt
{
@ -238,9 +237,9 @@ mod tests {
let mut storage = create_test_storage();
let shared_secret = [0x42; 32];
let bob_keypair = InstallationKeyPair::generate();
let bob_keypair = DhPrivateKey::random();
let alice: RatchetState<DefaultDomain> =
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
let bob: RatchetState<DefaultDomain> =
RatchetState::init_receiver(shared_secret, bob_keypair);
@ -278,8 +277,8 @@ mod tests {
let mut storage = create_test_storage();
let shared_secret = [0x42; 32];
let bob_keypair = InstallationKeyPair::generate();
let bob_pub = bob_keypair.public().clone();
let bob_keypair = DhPrivateKey::random();
let bob_pub = bob_keypair.public_key();
// First call creates
{

View File

@ -280,7 +280,7 @@ fn blob_to_array<const N: usize>(blob: Vec<u8>) -> [u8; N] {
#[cfg(test)]
mod tests {
use super::*;
use crate::{hkdf::DefaultDomain, keypair::InstallationKeyPair};
use crate::hkdf::DefaultDomain;
fn create_test_storage() -> SqliteStorage {
SqliteStorage::new(StorageConfig::InMemory).unwrap()
@ -288,8 +288,8 @@ mod tests {
fn create_test_state() -> (RatchetState<DefaultDomain>, RatchetState<DefaultDomain>) {
let shared_secret = [0x42; 32];
let bob_keypair = InstallationKeyPair::generate();
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
let bob_keypair = DhPrivateKey::random();
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public_key());
let bob = RatchetState::init_receiver(shared_secret, bob_keypair);
(alice, bob)
}
@ -309,8 +309,8 @@ mod tests {
assert_eq!(alice.msg_recv, loaded.msg_recv);
assert_eq!(alice.prev_chain_len, loaded.prev_chain_len);
assert_eq!(
alice.dh_self.public().to_bytes(),
loaded.dh_self.public().to_bytes()
alice.dh_self.public_key().as_bytes(),
loaded.dh_self.public_key().as_bytes()
);
}

View File

@ -3,8 +3,8 @@ use crate::{
state::{RatchetState, SkippedKey},
types::MessageKey,
};
use crypto::PublicKey32;
use thiserror::Error;
use x25519_dalek::PublicKey;
#[derive(Debug, Error)]
pub enum StorageError {
@ -53,16 +53,20 @@ impl<D: HkdfInfo> From<&RatchetState<D>> for RatchetStateRecord {
impl RatchetStateRecord {
pub fn into_ratchet_state<D: HkdfInfo>(self, skipped_keys: Vec<SkippedKey>) -> RatchetState<D> {
use crate::keypair::InstallationKeyPair;
use std::collections::HashMap;
use std::marker::PhantomData;
let dh_self = InstallationKeyPair::from_secret_bytes(self.dh_self_secret);
let dh_remote = self.dh_remote.map(PublicKey::from);
let dh_self = DhPrivateKey::from(self.dh_self_secret);
let dh_remote = self.dh_remote.map(PublicKey32::from);
let skipped: HashMap<(PublicKey, u32), MessageKey> = skipped_keys
let skipped: HashMap<(PublicKey32, u32), MessageKey> = skipped_keys
.into_iter()
.map(|sk| ((PublicKey::from(sk.public_key), sk.msg_num), sk.message_key))
.map(|sk| {
(
(PublicKey32::from(sk.public_key), sk.msg_num),
sk.message_key,
)
})
.collect();
RatchetState {

View File

@ -1,3 +1,8 @@
use crypto::PrivateKey32;
/// Type alias for diffie-hellman private keys.
pub type DhPrivateKey = PrivateKey32;
/// Type alias for root keys (32 bytes).
pub type RootKey = [u8; 32];
/// Type alias for chain keys (sending/receiving, 32 bytes).