diff --git a/Cargo.lock b/Cargo.lock index e9850d5..69ff283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/conversations/Cargo.toml b/conversations/Cargo.toml index 0fd90ed..46f4b1f 100644 --- a/conversations/Cargo.toml +++ b/conversations/Cargo.toml @@ -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"] } diff --git a/conversations/src/conversation/privatev1.rs b/conversations/src/conversation/privatev1.rs index c40c18e..a9a0aed 100644 --- a/conversations/src/conversation/privatev1.rs +++ b/conversations/src/conversation/privatev1.rs @@ -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 {} } diff --git a/conversations/src/crypto.rs b/conversations/src/crypto.rs index ecf0d11..5807c31 100644 --- a/conversations/src/crypto.rs +++ b/conversations/src/crypto.rs @@ -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()) } diff --git a/conversations/src/identity.rs b/conversations/src/identity.rs index c5646a7..8ee5e1e 100644 --- a/conversations/src/identity.rs +++ b/conversations/src/identity.rs @@ -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 } } diff --git a/conversations/src/inbox/handshake.rs b/conversations/src/inbox/handshake.rs index bbcb088..8fe1bfe 100644 --- a/conversations/src/inbox/handshake.rs +++ b/conversations/src/inbox/handshake.rs @@ -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; @@ -21,16 +21,16 @@ pub struct InboxHandshake {} impl InboxHandshake { /// Performs pub fn perform_as_initiator( - 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, diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs index f117a3c..b4c4552 100644 --- a/conversations/src/inbox/inbox.rs +++ b/conversations/src/inbox/inbox.rs @@ -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, local_convo_id: String, - ephemeral_keys: HashMap, + ephemeral_keys: HashMap, } impl<'a> std::fmt::Debug for Inbox { @@ -46,7 +46,7 @@ impl Inbox { Self { ident, local_convo_id, - ephemeral_keys: HashMap::::new(), + ephemeral_keys: HashMap::::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()) diff --git a/conversations/src/inbox/introduction.rs b/conversations/src/inbox/introduction.rs index 0955e11..8d5ea63 100644 --- a/conversations/src/inbox/introduction.rs +++ b/conversations/src/inbox/introduction.rs @@ -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 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, diff --git a/crypto/src/keys.rs b/crypto/src/keys.rs index 1b78ea7..26e83e8 100644 --- a/crypto/src/keys.rs +++ b/crypto/src/keys.rs @@ -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(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> for SecretKey { +impl From> for SecretKey32 { fn from(value: GenericArray) -> 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() } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index c543994..f1f74e9 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -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}; diff --git a/crypto/src/x3dh.rs b/crypto/src/x3dh.rs index 7b869d8..be8e8a7 100644 --- a/crypto/src/x3dh.rs +++ b/crypto/src/x3dh.rs @@ -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, + pub onetime_prekey: Option, } pub trait DomainSeparator { @@ -35,7 +35,7 @@ impl X3Handshake { 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 X3Handshake { 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 X3Handshake { /// # Returns /// A tuple of (shared secret bytes, ephemeral public key) pub fn initator( - 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 X3Handshake { /// # 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 { diff --git a/double-ratchets/Cargo.toml b/double-ratchets/Cargo.toml index de78550..1e5b0be 100644 --- a/double-ratchets/Cargo.toml +++ b/double-ratchets/Cargo.toml @@ -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" diff --git a/double-ratchets/examples/double_ratchet_basic.rs b/double-ratchets/examples/double_ratchet_basic.rs index 297cc10..0533af6 100644 --- a/double-ratchets/examples/double_ratchet_basic.rs +++ b/double-ratchets/examples/double_ratchet_basic.rs @@ -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 = - RatchetState::init_sender(shared_secret, bob_dh.public().clone()); + RatchetState::init_sender(shared_secret, bob_dh.public_key()); let mut bob: RatchetState = RatchetState::init_receiver(shared_secret, bob_dh); let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!"); diff --git a/double-ratchets/examples/out_of_order_demo.rs b/double-ratchets/examples/out_of_order_demo.rs index a2dbb4d..1d0f35e 100644 --- a/double-ratchets/examples/out_of_order_demo.rs +++ b/double-ratchets/examples/out_of_order_demo.rs @@ -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 = - RatchetState::init_sender(shared_secret, bob_keypair.public().clone()); + RatchetState::init_sender(shared_secret, bob_keypair.public_key()); let bob_state: RatchetState = 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 = - RatchetState::init_sender(shared_secret, bob_keypair.public().clone()); + RatchetState::init_sender(shared_secret, bob_keypair.public_key()); let bob_state: RatchetState = RatchetState::init_receiver(shared_secret, bob_keypair); diff --git a/double-ratchets/examples/serialization_demo.rs b/double-ratchets/examples/serialization_demo.rs index 76a5878..455b116 100644 --- a/double-ratchets/examples/serialization_demo.rs +++ b/double-ratchets/examples/serialization_demo.rs @@ -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 = - RatchetState::init_sender(shared_secret, bob_dh.public().clone()); + RatchetState::init_sender(shared_secret, bob_dh.public_key().clone()); let mut bob: RatchetState = RatchetState::init_receiver(shared_secret, bob_dh); let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!"); diff --git a/double-ratchets/examples/storage_demo.rs b/double-ratchets/examples/storage_demo.rs index ce05bd4..de8ed5c 100644 --- a/double-ratchets/examples/storage_demo.rs +++ b/double-ratchets/examples/storage_demo.rs @@ -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(); diff --git a/double-ratchets/src/ffi/doubleratchet.rs b/double-ratchets/src/ffi/doubleratchet.rs index e09cebd..0a7957b 100644 --- a/double-ratchets/src/ffi/doubleratchet.rs +++ b/double-ratchets/src/ffi/doubleratchet.rs @@ -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 { - 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() } diff --git a/double-ratchets/src/ffi/key.rs b/double-ratchets/src/ffi/key.rs index ee8a496..a355e8e 100644 --- a/double-ratchets/src/ffi/key.rs +++ b/double-ratchets/src/ffi/key.rs @@ -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 { - 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] diff --git a/double-ratchets/src/keypair.rs b/double-ratchets/src/keypair.rs deleted file mode 100644 index 7943646..0000000 --- a/double-ratchets/src/keypair.rs +++ /dev/null @@ -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 } - } -} diff --git a/double-ratchets/src/lib.rs b/double-ratchets/src/lib.rs index f2cd789..d7c609c 100644 --- a/double-ratchets/src/lib.rs +++ b/double-ratchets/src/lib.rs @@ -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}; diff --git a/double-ratchets/src/state.rs b/double-ratchets/src/state.rs index 48ad359..ed9f15b 100644 --- a/double-ratchets/src/state.rs +++ b/double-ratchets/src/state.rs @@ -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 { pub sending_chain: Option, pub receiving_chain: Option, - pub dh_self: InstallationKeyPair, - pub dh_remote: Option, + pub dh_self: DhPrivateKey, + pub dh_remote: Option, 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, } @@ -104,8 +103,7 @@ impl RatchetState { 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 RatchetState { 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 RatchetState { 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 { /// 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 RatchetState { /// # 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::(&shared_secret, &dh_out); + let dh_out = dh_self.diffie_hellman(&remote_pub); + let (root_key, sending_chain) = kdf_root::(&shared_secret, dh_out.as_bytes()); Self { root_key, @@ -271,7 +269,7 @@ impl RatchetState { /// # 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 RatchetState { /// # 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::(&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::(&self.root_key, dh_out.as_bytes()); self.root_key = new_root; self.receiving_chain = Some(recv_chain); @@ -312,9 +310,9 @@ impl RatchetState { 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::(&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::(&self.root_key, dh_out.as_bytes()); self.root_key = new_root; self.sending_chain = Some(send_chain); @@ -345,7 +343,7 @@ impl RatchetState { *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() ); } diff --git a/double-ratchets/src/storage/session.rs b/double-ratchets/src/storage/session.rs index 399af8d..36f16cb 100644 --- a/double-ratchets/src/storage/session.rs +++ b/double-ratchets/src/storage/session.rs @@ -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, shared_secret: SharedSecret, - remote_pub: PublicKey, + remote_pub: PublicKey32, ) -> Result { let state = RatchetState::::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, shared_secret: SharedSecret, - dh_self: InstallationKeyPair, + dh_self: DhPrivateKey, ) -> Result { 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 = - 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 = - 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 = - RatchetState::init_sender(shared_secret, bob_keypair.public().clone()); + RatchetState::init_sender(shared_secret, bob_keypair.public_key()); let bob: RatchetState = 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 { diff --git a/double-ratchets/src/storage/sqlite.rs b/double-ratchets/src/storage/sqlite.rs index 2c061f8..f2e9ba1 100644 --- a/double-ratchets/src/storage/sqlite.rs +++ b/double-ratchets/src/storage/sqlite.rs @@ -280,7 +280,7 @@ fn blob_to_array(blob: Vec) -> [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, RatchetState) { 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() ); } diff --git a/double-ratchets/src/storage/types.rs b/double-ratchets/src/storage/types.rs index 6a1cd80..9a60416 100644 --- a/double-ratchets/src/storage/types.rs +++ b/double-ratchets/src/storage/types.rs @@ -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 From<&RatchetState> for RatchetStateRecord { impl RatchetStateRecord { pub fn into_ratchet_state(self, skipped_keys: Vec) -> RatchetState { - 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 { diff --git a/double-ratchets/src/types.rs b/double-ratchets/src/types.rs index bbad3a8..db0edb1 100644 --- a/double-ratchets/src/types.rs +++ b/double-ratchets/src/types.rs @@ -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).