From fe23c39321bb08b2d5af0d88347231091063d4a4 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 22 Jan 2026 06:39:09 +0700 Subject: [PATCH] PrivateV1 Convo Initialization via Inbox (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Load orginal protofiles * Change package name * Add prost generation * Remove placeholders * Add generated files + imports * replace with chat-proto * Add XK0 * auto formatting * Initial implementation of PrivateV1 initialization * Add ConvoFactory trait * Hook up indentity placeholder * Remove RemoteInbox until it’s needed * Simplify Identity ownership * Clean up x3handshake * Move inbox encryption * Simplify inbox encryption * Cleanup warnings * Add todos * Update chat-proto crate * Publickey Handling * Reorg Inbox handshake * Update Inbox convoId * Remove file structure headers * Update ConvoID * Add Domain Separator trait * Remove Convo trait functions * Rename Context * Add SecretKey * Add workspace dependency * update KE name * Update comments for clarity * Remove Xk0 references * Bump chat_proto version and relock --- Cargo.lock | 217 +++++++++++++++- Cargo.toml | 4 + conversations/Cargo.toml | 7 + conversations/src/api.rs | 6 +- conversations/src/context.rs | 63 +++-- conversations/src/conversation.rs | 52 ++-- conversations/src/conversation/group_test.rs | 18 +- conversations/src/conversation/privatev1.rs | 53 +++- conversations/src/crypto.rs | 17 ++ conversations/src/errors.rs | 8 + conversations/src/identity.rs | 43 ++++ conversations/src/inbox.rs | 33 +-- conversations/src/inbox/handshake.rs | 120 +++++++++ conversations/src/inbox/inbox.rs | 247 +++++++++++++++++++ conversations/src/lib.rs | 45 +--- conversations/src/proto.rs | 5 + conversations/src/types.rs | 13 + conversations/src/utils.rs | 8 + crypto/Cargo.toml | 14 ++ crypto/src/keys.rs | 31 +++ crypto/src/lib.rs | 5 + crypto/src/x3dh.rs | 214 ++++++++++++++++ rust_toolchain.toml | 3 + 23 files changed, 1095 insertions(+), 131 deletions(-) create mode 100644 conversations/src/identity.rs create mode 100644 conversations/src/inbox/handshake.rs create mode 100644 conversations/src/inbox/inbox.rs create mode 100644 conversations/src/proto.rs create mode 100644 conversations/src/types.rs create mode 100644 conversations/src/utils.rs create mode 100644 crypto/Cargo.toml create mode 100644 crypto/src/keys.rs create mode 100644 crypto/src/lib.rs create mode 100644 crypto/src/x3dh.rs create mode 100644 rust_toolchain.toml diff --git a/Cargo.lock b/Cargo.lock index 1297bf0..ed54022 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,9 +9,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "blake2" version = "0.10.6" @@ -27,9 +39,15 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cfg-if" version = "1.0.4" @@ -60,6 +78,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chat-proto" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/chat_proto#21cd52b0649c959f43eec19e1edad12451ccc382" +dependencies = [ + "prost", +] + [[package]] name = "cipher" version = "0.4.4" @@ -71,6 +97,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -80,13 +112,27 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "generic-array 1.3.5", + "hkdf", + "rand_core", + "sha2", + "x25519-dalek", + "xeddsa", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core", "typenum", ] @@ -100,6 +146,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", + "digest", "fiat-crypto", "rustc_version", "subtle", @@ -117,6 +164,27 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "digest" version = "0.10.7" @@ -143,6 +211,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -200,6 +298,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "generic-array" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +dependencies = [ + "rustversion", + "typenum", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -217,6 +325,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -251,7 +365,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -263,6 +377,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "libc" version = "0.2.178" @@ -273,7 +396,14 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" name = "logos-chat" version = "0.1.0" dependencies = [ + "blake2", + "chat-proto", + "crypto", + "hex", + "prost", + "rand_core", "thiserror", + "x25519-dalek", ] [[package]] @@ -310,6 +440,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -358,6 +498,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "quote" version = "1.0.42" @@ -486,12 +649,42 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2-const-stable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stabby" version = "36.2.2" @@ -695,6 +888,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xeddsa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2460c9a9c9d1331ff6801e87badb517faa6b6758e5fb585eb27daf7622c6d5ad" +dependencies = [ + "curve25519-dalek", + "derive_more", + "ed25519", + "ed25519-dalek", + "rand", + "sha2", + "x25519-dalek", + "zeroize", +] + [[package]] name = "zerocopy" version = "0.8.31" diff --git a/Cargo.toml b/Cargo.toml index bc37360..4a118c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,9 @@ resolver = "3" members = [ "conversations", + "crypto", "double-ratchets", ] + +[workspace.dependencies] +blake2 = "0.10" diff --git a/conversations/Cargo.toml b/conversations/Cargo.toml index 83a7e1e..678ee3c 100644 --- a/conversations/Cargo.toml +++ b/conversations/Cargo.toml @@ -7,4 +7,11 @@ edition = "2024" crate-type = ["staticlib"] [dependencies] +blake2.workspace = true +chat-proto = { git = "https://github.com/logos-messaging/chat_proto" } +crypto = { path = "../crypto" } +hex = "0.4.3" +prost = "0.14.1" +rand_core = { version = "0.6" } thiserror = "2.0.17" +x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } diff --git a/conversations/src/api.rs b/conversations/src/api.rs index b177807..2e7ed4b 100644 --- a/conversations/src/api.rs +++ b/conversations/src/api.rs @@ -8,9 +8,9 @@ pub enum ErrorCode { BadConvoId = -2, } -use crate::context::Ctx; +use crate::context::Context; -pub type ContextHandle = *mut Ctx; +pub type ContextHandle = *mut Context; /// Creates a new libchat Ctx /// @@ -18,7 +18,7 @@ pub type ContextHandle = *mut Ctx; /// Opaque handle to the store. Must be freed with conversation_store_destroy() #[unsafe(no_mangle)] pub extern "C" fn create_context() -> ContextHandle { - let store = Box::new(Ctx::new()); + let store = Box::new(Context::new()); Box::into_raw(store) // Leak the box, return raw pointer } diff --git a/conversations/src/context.rs b/conversations/src/context.rs index 5c0dd11..0c78331 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -1,29 +1,46 @@ -use crate::conversation::{ConversationId, ConversationIdOwned, ConversationStore, PrivateV1Convo}; +use std::rc::Rc; -pub struct PayloadData { - pub delivery_address: String, - pub data: Vec, -} +use crypto::PrekeyBundle; -pub struct ContentData { - pub conversation_id: String, - pub data: Vec, -} +use crate::{ + conversation::{ConversationId, ConversationIdOwned, ConversationStore}, + identity::Identity, + inbox::Inbox, + types::{ContentData, PayloadData}, +}; -pub struct Ctx { +// This is the main entry point to the conversations api. +// Ctx manages lifetimes of objects to process and generate payloads. +pub struct Context { + _identity: Rc, store: ConversationStore, + inbox: Inbox, } -impl Ctx { +impl Context { pub fn new() -> Self { + let identity = Rc::new(Identity::new()); + let inbox = Inbox::new(Rc::clone(&identity)); // Self { + _identity: identity, store: ConversationStore::new(), + inbox, } } - pub fn create_private_convo(&mut self, _content: &[u8]) -> ConversationIdOwned { - let new_convo = PrivateV1Convo::new(); - self.store.insert(new_convo) + pub fn create_private_convo( + &mut self, + remote_bundle: &PrekeyBundle, + content: String, + ) -> ConversationIdOwned { + let (convo, _payloads) = self + .inbox + .invite_to_private_convo(remote_bundle, content) + .unwrap_or_else(|_| todo!("Log/Surface Error")); + + self.store.insert_convo(convo) + + // TODO: Change return type to handle outbout packets. } pub fn send_content(&mut self, _convo_id: ConversationId, _content: &[u8]) -> Vec { @@ -42,3 +59,21 @@ impl Ctx { }) } } + +#[cfg(test)] + +mod tests { + use super::*; + use crate::conversation::GroupTestConvo; + + #[test] + fn convo_store_get() { + let mut store: ConversationStore = ConversationStore::new(); + + let new_convo = GroupTestConvo::new(); + let convo_id = store.insert_convo(new_convo); + + let convo = store.get_mut(&convo_id).ok_or_else(|| 0); + convo.unwrap(); + } +} diff --git a/conversations/src/conversation.rs b/conversations/src/conversation.rs index 9485719..84327b7 100644 --- a/conversations/src/conversation.rs +++ b/conversations/src/conversation.rs @@ -3,46 +3,55 @@ use std::fmt::Debug; use std::sync::Arc; pub use crate::errors::ChatError; - -///////////////////////////////////////////////// -// Type Definitions -///////////////////////////////////////////////// +use crate::types::ContentData; pub type ConversationId<'a> = &'a str; pub type ConversationIdOwned = Arc; -///////////////////////////////////////////////// -// Trait Definitions -///////////////////////////////////////////////// - -pub trait Convo: Debug { +pub trait Id: Debug { fn id(&self) -> ConversationId; - fn send_frame(&mut self, message: &[u8]) -> Result<(), ChatError>; - fn handle_frame(&mut self, message: &[u8]) -> Result<(), ChatError>; } -///////////////////////////////////////////////// -// Structs -///////////////////////////////////////////////// +pub trait ConvoFactory: Id + Debug { + fn handle_frame( + &mut self, + encoded_payload: &[u8], + ) -> Result<(Box, Vec), ChatError>; +} + +pub trait Convo: Id + Debug { + fn send_message(&mut self, content: &[u8]) -> Result, ChatError>; +} pub struct ConversationStore { conversations: HashMap, Box>, + factories: HashMap, Box>, } impl ConversationStore { pub fn new() -> Self { Self { conversations: HashMap::new(), + factories: HashMap::new(), } } - pub fn insert(&mut self, conversation: impl Convo + 'static) -> ConversationIdOwned { + pub fn insert_convo(&mut self, conversation: impl Convo + Id + 'static) -> ConversationIdOwned { let key: ConversationIdOwned = Arc::from(conversation.id()); self.conversations .insert(key.clone(), Box::new(conversation)); key } + pub fn register_factory( + &mut self, + handler: impl ConvoFactory + Id + 'static, + ) -> ConversationIdOwned { + let key: ConversationIdOwned = Arc::from(handler.id()); + self.factories.insert(key.clone(), Box::new(handler)); + key + } + pub fn get(&self, id: ConversationId) -> Option<&(dyn Convo + '_)> { self.conversations.get(id).map(|c| c.as_ref()) } @@ -51,17 +60,22 @@ impl ConversationStore { Some(self.conversations.get_mut(id)?.as_mut()) } + pub fn get_factory(&mut self, id: ConversationId) -> Option<&mut (dyn ConvoFactory + '_)> { + Some(self.factories.get_mut(id)?.as_mut()) + } + pub fn conversation_ids(&self) -> impl Iterator + '_ { self.conversations.keys().cloned() } -} -///////////////////////////////////////////////// -// Modules -///////////////////////////////////////////////// + pub fn factory_ids(&self) -> impl Iterator + '_ { + self.factories.keys().cloned() + } +} mod group_test; mod privatev1; +use crate::proto::EncryptedPayload; pub use group_test::GroupTestConvo; pub use privatev1::PrivateV1Convo; diff --git a/conversations/src/conversation/group_test.rs b/conversations/src/conversation/group_test.rs index 7719ad3..077f240 100644 --- a/conversations/src/conversation/group_test.rs +++ b/conversations/src/conversation/group_test.rs @@ -1,4 +1,6 @@ -use crate::conversation::{ChatError, ConversationId, Convo}; +use chat_proto::logoschat::encryption::EncryptedPayload; + +use crate::conversation::{ChatError, ConversationId, Convo, Id}; #[derive(Debug)] pub struct GroupTestConvo {} @@ -9,19 +11,15 @@ impl GroupTestConvo { } } -impl Convo for GroupTestConvo { +impl Id for GroupTestConvo { fn id(&self) -> ConversationId { // implementation "grouptest" } +} - fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> { - // todo!("Not Implemented") - Ok(()) - } - - fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> { - // todo!("Not Implemented") - Ok(()) +impl Convo for GroupTestConvo { + fn send_message(&mut self, _content: &[u8]) -> Result, ChatError> { + Ok(vec![]) } } diff --git a/conversations/src/conversation/privatev1.rs b/conversations/src/conversation/privatev1.rs index 15d27aa..baa2266 100644 --- a/conversations/src/conversation/privatev1.rs +++ b/conversations/src/conversation/privatev1.rs @@ -1,25 +1,56 @@ -use crate::conversation::{ChatError, ConversationId, Convo}; +use chat_proto::logoschat::{ + convos::private_v1::{PrivateV1Frame, private_v1_frame::FrameType}, + encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption}, +}; +use crypto::SecretKey; +use prost::{Message, bytes::Bytes}; + +use crate::{ + conversation::{ChatError, ConversationId, Convo, Id}, + utils::timestamp_millis, +}; #[derive(Debug)] pub struct PrivateV1Convo {} impl PrivateV1Convo { - pub fn new() -> Self { + pub fn new(_seed_key: SecretKey) -> Self { Self {} } + + fn encrypt(&self, frame: PrivateV1Frame) -> EncryptedPayload { + // TODO: Integrate DR + + EncryptedPayload { + encryption: Some(Encryption::Doubleratchet(Doubleratchet { + dh: Bytes::from(vec![]), + msg_num: 0, + prev_chain_len: 1, + ciphertext: Bytes::from(frame.encode_to_vec()), + aux: "".into(), + })), + } + } +} + +impl Id for PrivateV1Convo { + fn id(&self) -> ConversationId { + // TODO: implementation + "private_v1_convo_id" + } } impl Convo for PrivateV1Convo { - fn id(&self) -> ConversationId { - // implementation - "private_v1_convo_id" - } + fn send_message(&mut self, content: &[u8]) -> Result, ChatError> { + let frame = PrivateV1Frame { + conversation_id: self.id().into(), + sender: "delete".into(), + timestamp: timestamp_millis(), + frame_type: Some(FrameType::Content(content.to_vec().into())), + }; - fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> { - todo!("Not Implemented") - } + let ef = self.encrypt(frame); - fn handle_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> { - todo!("Not Implemented") + Ok(vec![ef]) } } diff --git a/conversations/src/crypto.rs b/conversations/src/crypto.rs index e69de29..ecf0d11 100644 --- a/conversations/src/crypto.rs +++ b/conversations/src/crypto.rs @@ -0,0 +1,17 @@ +pub use blake2::Digest; +use blake2::{Blake2b, digest}; +use prost::bytes::Bytes; +pub use x25519_dalek::{PublicKey, StaticSecret}; + +pub trait CopyBytes { + fn copy_to_bytes(&self) -> Bytes; +} + +impl CopyBytes for PublicKey { + fn copy_to_bytes(&self) -> Bytes { + Bytes::copy_from_slice(self.as_bytes()) + } +} + +#[allow(dead_code)] +pub type Blake2b128 = Blake2b; diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index 098b048..b9bb8dc 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -4,4 +4,12 @@ pub use thiserror::Error; pub enum ChatError { #[error("protocol error: {0:?}")] Protocol(String), + #[error("Failed to decode payload: {0}")] + DecodeError(#[from] prost::DecodeError), + #[error("incorrect bundle value: {0:?}")] + UnexpectedPayload(String), + #[error("unexpected payload contents: {0}")] + BadBundleValue(String), + #[error("handshake initiated with a unknown ephemeral key")] + UnknownEphemeralKey(), } diff --git a/conversations/src/identity.rs b/conversations/src/identity.rs new file mode 100644 index 0000000..c5646a7 --- /dev/null +++ b/conversations/src/identity.rs @@ -0,0 +1,43 @@ +use blake2::{Blake2b512, Digest}; +use std::fmt; + +use crate::crypto::{PublicKey, StaticSecret}; + +pub struct Identity { + secret: StaticSecret, +} + +impl fmt::Debug for Identity { + // Manually implement debug to not reveal secret key material + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Identity") + .field("public_key", &self.public_key()) + .finish_non_exhaustive() + } +} + +impl Identity { + pub fn new() -> Self { + Self { + secret: StaticSecret::random(), + } + } + + pub fn address(&self) -> String { + hex::encode(Blake2b512::digest(self.public_key())) + } + + pub fn public_key(&self) -> PublicKey { + PublicKey::from(&self.secret) + } + + pub fn secret(&self) -> &StaticSecret { + &self.secret + } +} + +impl Default for Identity { + fn default() -> Self { + Self::new() + } +} diff --git a/conversations/src/inbox.rs b/conversations/src/inbox.rs index 662ee3f..0322818 100644 --- a/conversations/src/inbox.rs +++ b/conversations/src/inbox.rs @@ -1,31 +1,4 @@ -use crate::conversation::{ChatError, ConversationId, Convo}; +mod handshake; +mod inbox; -#[derive(Debug)] -pub struct Inbox { - address: String, -} - -impl Inbox { - pub fn new(address: impl Into) -> Self { - Self { - address: address.into(), - } - } -} - -impl Convo for Inbox { - fn id(&self) -> ConversationId { - self.address.as_ref() - } - - fn send_frame(&mut self, _message: &[u8]) -> Result<(), ChatError> { - todo!("Not Implemented") - } - - fn handle_frame(&mut self, message: &[u8]) -> Result<(), ChatError> { - if message.len() == 0 { - return Err(ChatError::Protocol("Example error".into())); - } - todo!("Not Implemented") - } -} +pub use inbox::Inbox; diff --git a/conversations/src/inbox/handshake.rs b/conversations/src/inbox/handshake.rs new file mode 100644 index 0000000..bbcb088 --- /dev/null +++ b/conversations/src/inbox/handshake.rs @@ -0,0 +1,120 @@ +use blake2::{ + Blake2bMac, + digest::{FixedOutput, consts::U32}, +}; +use crypto::{DomainSeparator, PrekeyBundle, SecretKey, X3Handshake}; +use rand_core::{CryptoRng, RngCore}; + +use crate::crypto::{PublicKey, StaticSecret}; + +type Blake2bMac256 = Blake2bMac; + +pub struct InboxDomain; +impl DomainSeparator for InboxDomain { + const BYTES: &'static [u8] = b"logos_chat_inbox"; +} + +type InboxKeyExchange = X3Handshake; + +pub struct InboxHandshake {} + +impl InboxHandshake { + /// Performs + pub fn perform_as_initiator( + identity_keypair: &StaticSecret, + recipient_bundle: &PrekeyBundle, + rng: &mut R, + ) -> (SecretKey, PublicKey) { + // 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) + } + + /// Perform the Inbox Handshake after receiving a keyBundle + /// + /// # Arguments + /// * `identity_keypair` - Your long-term identity key pair + /// * `signed_prekey` - Your signed prekey (private) + /// * `onetime_prekey` - Your one-time prekey (private, if used) + /// * `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 { + // Perform X3DH to get shared secret + let shared_secret = InboxKeyExchange::responder( + identity_keypair, + signed_prekey, + onetime_prekey, + initiator_identity, + initiator_ephemeral, + ); + + Self::derive_keys_from_shared_secret(shared_secret) + } + + /// Derive keys from X3DH shared secret + fn derive_keys_from_shared_secret(shared_secret: SecretKey) -> SecretKey { + let seed_key: [u8; 32] = Blake2bMac256::new_with_salt_and_personal( + shared_secret.as_bytes(), + &[], // No salt - input already has high entropy + b"InboxV1-Seed", + ) + .unwrap() + .finalize_fixed() + .into(); // digest uses an incompatible version of GenericArray. use array as intermediary + + seed_key.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + #[test] + fn test_inbox_encryption_initialization() { + 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); + + // 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); + + // Create Bob's prekey bundle + let bob_bundle = PrekeyBundle { + identity_key: PublicKey::from(&bob_identity), + signed_prekey: bob_signed_prekey_pub, + signature: [0u8; 64], + onetime_prekey: None, + }; + + // Alice performs handshake + let (alice_secret, alice_ephemeral_pub) = + InboxHandshake::perform_as_initiator(&alice_identity, &bob_bundle, &mut rng); + + // Bob performs handshake + let bob_secret = InboxHandshake::perform_as_responder( + &bob_identity, + &bob_signed_prekey, + None, + &alice_identity_pub, + &alice_ephemeral_pub, + ); + + // Both should derive the same root key + assert_eq!(alice_secret, bob_secret); + } +} diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs new file mode 100644 index 0000000..bb919e2 --- /dev/null +++ b/conversations/src/inbox/inbox.rs @@ -0,0 +1,247 @@ +use chat_proto::logoschat::inbox::InboxV1Frame; +use hex; +use prost::Message; +use prost::bytes::Bytes; +use rand_core::OsRng; +use std::collections::HashMap; +use std::rc::Rc; + +use crypto::{PrekeyBundle, SecretKey}; + +use crate::conversation::{ChatError, ConversationId, Convo, ConvoFactory, Id, PrivateV1Convo}; +use crate::crypto::{Blake2b128, CopyBytes, Digest, PublicKey, StaticSecret}; +use crate::identity::Identity; +use crate::inbox::handshake::InboxHandshake; +use crate::proto::{self}; +use crate::types::ContentData; + +pub struct Inbox { + ident: Rc, + local_convo_id: String, + ephemeral_keys: HashMap, +} + +impl<'a> std::fmt::Debug for Inbox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Inbox") + .field("ident", &self.ident) + .field("convo_id", &self.local_convo_id) + .field( + "ephemeral_keys", + &format!("<{} keys>", self.ephemeral_keys.len()), + ) + .finish() + } +} + +impl Inbox { + pub fn new(ident: Rc) -> Self { + let local_convo_id = ident.address(); + Self { + ident, + local_convo_id, + ephemeral_keys: HashMap::::new(), + } + } + + fn compute_local_convo_id(addr: &str) -> String { + let hash = Blake2b128::digest(format!("{}:{}:{}", "logoschat", "inboxV1", addr)); + hex::encode(hash) + } + + pub fn create_bundle(&mut self) -> PrekeyBundle { + let ephemeral = StaticSecret::random(); + + let signed_prekey = PublicKey::from(&ephemeral); + self.ephemeral_keys + .insert(hex::encode(signed_prekey.as_bytes()), ephemeral); + + PrekeyBundle { + identity_key: self.ident.public_key(), + signed_prekey: signed_prekey, + signature: [0u8; 64], + onetime_prekey: None, + } + } + + pub fn invite_to_private_convo( + &self, + remote_bundle: &PrekeyBundle, + initial_message: String, + ) -> Result<(PrivateV1Convo, Vec), ChatError> { + let mut rng = OsRng; + + let (seed_key, ephemeral_pub) = + InboxHandshake::perform_as_initiator(&self.ident.secret(), remote_bundle, &mut rng); + + let mut convo = PrivateV1Convo::new(seed_key); + + let mut initial_payloads = convo.send_message(initial_message.as_bytes())?; + + // Wrap First payload in Invite + if let Some(first_message) = initial_payloads.get_mut(0) { + let old = first_message.clone(); + let frame = Self::wrap_in_invite(old); + + // TODO: Encrypt frame + let ciphertext = frame.encode_to_vec(); + + let header = proto::InboxHeaderV1 { + initiator_static: self.ident.public_key().copy_to_bytes(), + initiator_ephemeral: ephemeral_pub.copy_to_bytes(), + responder_static: remote_bundle.identity_key.copy_to_bytes(), + responder_ephemeral: remote_bundle.signed_prekey.copy_to_bytes(), + }; + + let handshake = proto::InboxHandshakeV1 { + header: Some(header), + payload: Bytes::from_owner(ciphertext), + }; + + *first_message = proto::EncryptedPayload { + encryption: Some(proto::Encryption::InboxHandshake(handshake)), + }; + } + + Ok((convo, initial_payloads)) + } + + fn wrap_in_invite(payload: proto::EncryptedPayload) -> proto::InboxV1Frame { + let invite = proto::InvitePrivateV1 { + discriminator: "default".into(), + initial_message: Some(payload), + }; + + proto::InboxV1Frame { + frame_type: Some( + chat_proto::logoschat::inbox::inbox_v1_frame::FrameType::InvitePrivateV1(invite), + ), + } + } + + fn perform_handshake( + &self, + payload: proto::EncryptedPayload, + ) -> Result<(SecretKey, proto::InboxV1Frame), ChatError> { + let handshake = Self::extract_payload(payload)?; + let header = handshake + .header + .ok_or(ChatError::UnexpectedPayload("InboxV1Header".into()))?; + let pubkey_hex = hex::encode(header.responder_ephemeral.as_ref()); + + let ephemeral_key = self.lookup_ephemeral_key(&pubkey_hex)?; + + let initator_static = PublicKey::from( + <[u8; 32]>::try_from(header.initiator_static.as_ref()) + .map_err(|_| ChatError::BadBundleValue("wrong size - initator static".into()))?, + ); + + let initator_ephemeral = PublicKey::from( + <[u8; 32]>::try_from(header.initiator_ephemeral.as_ref()) + .map_err(|_| ChatError::BadBundleValue("wrong size - initator ephemeral".into()))?, + ); + + let seed_key = InboxHandshake::perform_as_responder( + self.ident.secret(), + ephemeral_key, + None, + &initator_static, + &initator_ephemeral, + ); + + // TODO: Decrypt Content + let frame = InboxV1Frame::decode(handshake.payload)?; + Ok((seed_key, frame)) + } + + fn extract_payload( + payload: proto::EncryptedPayload, + ) -> Result { + let Some(proto::Encryption::InboxHandshake(handshake)) = payload.encryption else { + return Err(ChatError::Protocol( + "Expected inboxhandshake encryption".into(), + )); + }; + + Ok(handshake) + } + + fn decrypt_frame( + enc_payload: proto::InboxHandshakeV1, + ) -> Result { + let frame_bytes = enc_payload.payload; + // TODO: decrypt payload + let frame = proto::InboxV1Frame::decode(frame_bytes)?; + Ok(frame) + } + + fn lookup_ephemeral_key(&self, key: &str) -> Result<&StaticSecret, ChatError> { + self.ephemeral_keys + .get(key) + .ok_or_else(|| return ChatError::UnknownEphemeralKey()) + } +} + +impl Id for Inbox { + fn id(&self) -> ConversationId { + &self.local_convo_id + } +} + +impl ConvoFactory for Inbox { + fn handle_frame( + &mut self, + message: &[u8], + ) -> Result<(Box, Vec), ChatError> { + if message.len() == 0 { + return Err(ChatError::Protocol("Example error".into())); + } + + let ep = proto::EncryptedPayload::decode(message)?; + let (seed_key, frame) = self.perform_handshake(ep)?; + + match frame.frame_type.unwrap() { + chat_proto::logoschat::inbox::inbox_v1_frame::FrameType::InvitePrivateV1( + _invite_private_v1, + ) => { + let convo = PrivateV1Convo::new(seed_key); + // TODO: Update PrivateV1 Constructor with DR, initial_message + Ok((Box::new(convo), vec![])) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invite_privatev1_roundtrip() { + let saro_ident = Identity::new(); + let saro_inbox = Inbox::new(saro_ident.into()); + + let raya_ident = Identity::new(); + let mut raya_inbox = Inbox::new(raya_ident.into()); + + let bundle = raya_inbox.create_bundle(); + let (_, payloads) = saro_inbox + .invite_to_private_convo(&bundle, "hello".into()) + .unwrap(); + + let encrypted_payload = payloads + .get(0) + .expect("RemoteInbox::invite_to_private_convo did not generate any payloads"); + + let mut buf = Vec::new(); + encrypted_payload.encode(&mut buf).unwrap(); + + // Test handle_frame with valid payload + let result = raya_inbox.handle_frame(&buf); + + assert!( + result.is_ok(), + "handle_frame should accept valid encrypted payloads" + ); + } +} diff --git a/conversations/src/lib.rs b/conversations/src/lib.rs index 0f6b57e..a94ca3e 100644 --- a/conversations/src/lib.rs +++ b/conversations/src/lib.rs @@ -1,15 +1,17 @@ mod api; mod context; mod conversation; +mod crypto; mod errors; +mod identity; mod inbox; +mod proto; +mod types; +mod utils; pub use api::*; #[cfg(test)] -use conversation::{ConversationStore, GroupTestConvo, PrivateV1Convo}; -use inbox::Inbox; - mod tests { use super::*; @@ -113,41 +115,4 @@ mod tests { assert_eq!(result, -1, "Should return ERR_BAD_PTR for null pointer"); } - - #[test] - fn convo_store_get() { - let mut store: ConversationStore = ConversationStore::new(); - - let new_convo = GroupTestConvo::new(); - let convo_id = store.insert(new_convo); - - let convo = store.get_mut(&convo_id).ok_or_else(|| 0); - convo.unwrap(); - } - - #[test] - fn multi_convo_example() { - let mut store: ConversationStore = ConversationStore::new(); - - let raya = Inbox::new("Raya"); - let saro = PrivateV1Convo::new(); - let pax = GroupTestConvo::new(); - - store.insert(raya); - store.insert(saro); - let convo_id = store.insert(pax); - - for id in store.conversation_ids().collect::>() { - let a = store.get_mut(&id).unwrap(); - a.send_frame(b"test message").unwrap(); - println!("Conversation ID: {} :: {:?}", id, a); - } - - for id in store.conversation_ids().collect::>() { - let a = store.get_mut(&id).unwrap(); - let _ = a.handle_frame(&[0x1, 0x2]); - } - - println!("ID -> {}", store.get(&convo_id).unwrap().id()); - } } diff --git a/conversations/src/proto.rs b/conversations/src/proto.rs new file mode 100644 index 0000000..af9b43f --- /dev/null +++ b/conversations/src/proto.rs @@ -0,0 +1,5 @@ +pub use chat_proto::logoschat::encryption::encrypted_payload::Encryption; +pub use chat_proto::logoschat::encryption::inbox_handshake_v1::InboxHeaderV1; +pub use chat_proto::logoschat::encryption::{EncryptedPayload, InboxHandshakeV1}; +pub use chat_proto::logoschat::inbox::InboxV1Frame; +pub use chat_proto::logoschat::invite::InvitePrivateV1; diff --git a/conversations/src/types.rs b/conversations/src/types.rs new file mode 100644 index 0000000..0717065 --- /dev/null +++ b/conversations/src/types.rs @@ -0,0 +1,13 @@ +// This struct represents Outbound data. +// It wraps an encoded payload with a delivery address, so it can be handled by the delivery service. +pub struct PayloadData { + pub delivery_address: String, + pub data: Vec, +} + +// This struct represents the result of processed inbound data. +// It wraps content payload with a conversation_id +pub struct ContentData { + pub conversation_id: String, + pub data: Vec, +} diff --git a/conversations/src/utils.rs b/conversations/src/utils.rs new file mode 100644 index 0000000..306e898 --- /dev/null +++ b/conversations/src/utils.rs @@ -0,0 +1,8 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn timestamp_millis() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64 +} diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml new file mode 100644 index 0000000..9437f82 --- /dev/null +++ b/crypto/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crypto" +version = "0.1.0" +edition = "2024" + +[dependencies] +x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } +hkdf = "0.12" +sha2 = "0.10" +rand_core = { version = "0.6", features = ["getrandom"] } +ed25519-dalek = "2.2.0" +xeddsa = "1.0.2" +zeroize = {version = "1.8.2", features= ["derive"]} +generic-array = "1.3.5" diff --git a/crypto/src/keys.rs b/crypto/src/keys.rs new file mode 100644 index 0000000..1b78ea7 --- /dev/null +++ b/crypto/src/keys.rs @@ -0,0 +1,31 @@ +use std::fmt::Debug; + +pub use generic_array::{GenericArray, typenum::U32}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq)] +pub struct SecretKey([u8; 32]); + +impl SecretKey { + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl From<[u8; 32]> for SecretKey { + fn from(value: [u8; 32]) -> Self { + SecretKey(value) + } +} + +impl From> for SecretKey { + fn from(value: GenericArray) -> Self { + SecretKey(value.into()) + } +} + +impl Debug for SecretKey { + 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 new file mode 100644 index 0000000..c543994 --- /dev/null +++ b/crypto/src/lib.rs @@ -0,0 +1,5 @@ +mod keys; +mod x3dh; + +pub use keys::{GenericArray, SecretKey}; +pub use x3dh::{DomainSeparator, PrekeyBundle, X3Handshake}; diff --git a/crypto/src/x3dh.rs b/crypto/src/x3dh.rs new file mode 100644 index 0000000..7b869d8 --- /dev/null +++ b/crypto/src/x3dh.rs @@ -0,0 +1,214 @@ +use std::marker::PhantomData; + +use hkdf::Hkdf; +use rand_core::{CryptoRng, RngCore}; +use sha2::Sha256; +use x25519_dalek::{PublicKey, SharedSecret, StaticSecret}; + +use crate::keys::SecretKey; + +/// 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 signature: [u8; 64], + pub onetime_prekey: Option, +} + +pub trait DomainSeparator { + const BYTES: &'static [u8]; +} + +pub struct X3Handshake { + _phantom: PhantomData, +} + +impl X3Handshake { + fn domain_separator() -> &'static [u8] { + D::BYTES + } + + /// Derive the shared secret from DH outputs using HKDF-SHA256 + fn derive_shared_secret( + dh1: &SharedSecret, + dh2: &SharedSecret, + dh3: &SharedSecret, + dh4: Option<&SharedSecret>, + ) -> SecretKey { + // Concatenate all DH outputs + let mut km = Vec::new(); + km.extend_from_slice(dh1.as_bytes()); + km.extend_from_slice(dh2.as_bytes()); + km.extend_from_slice(dh3.as_bytes()); + if let Some(dh4) = dh4 { + km.extend_from_slice(dh4.as_bytes()); + } + + // Use HKDF to derive the shared secret + // Using "X3DH" as the info parameter as per Signal protocol + let hk = Hkdf::::new(None, &km); + let mut output = [0u8; 32]; + hk.expand(Self::domain_separator(), &mut output) + .expect("32 bytes is valid HKDF output length"); + + // Move into SecretKey so it gets zeroized on drop. + output.into() + } + + /// Perform X3DH key agreement as the initiator + /// + /// # Arguments + /// * `identity_keypair` - Initiator's long-term identity key pair + /// * `recipient_bundle` - Recipient's prekey bundle + /// * `rng` - Cryptographically secure random number generator + /// + /// # Returns + /// A tuple of (shared secret bytes, ephemeral public key) + pub fn initator( + identity_keypair: &StaticSecret, + 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); + + // Perform the 4 Diffie-Hellman operations + let dh1 = identity_keypair.diffie_hellman(&recipient_bundle.signed_prekey); + let dh2 = ephemeral_secret.diffie_hellman(&recipient_bundle.identity_key); + let dh3 = ephemeral_secret.diffie_hellman(&recipient_bundle.signed_prekey); + let dh4 = recipient_bundle + .onetime_prekey + .as_ref() + .map(|opk| ephemeral_secret.diffie_hellman(opk)); + + // Combine all DH outputs into shared secret + let shared_secret = Self::derive_shared_secret(&dh1, &dh2, &dh3, dh4.as_ref()); + + (shared_secret, ephemeral_public) + } + + /// Perform X3DH key agreement as the responder + /// + /// # Arguments + /// * `identity_keypair` - Responder's long-term identity key pair + /// * `signed_prekey` - Responder's signed prekey (private) + /// * `onetime_prekey` - Responder's one-time prekey (private, if used) + /// * `initiator_identity` - Initiator's identity public key + /// * `initiator_ephemeral` - Initiator's ephemeral public key + /// + /// # 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 { + let dh1 = signed_prekey.diffie_hellman(initiator_identity); + let dh2 = identity_keypair.diffie_hellman(initiator_ephemeral); + let dh3 = signed_prekey.diffie_hellman(initiator_ephemeral); + let dh4 = onetime_prekey.map(|opk| opk.diffie_hellman(initiator_ephemeral)); + + // Combine all DH outputs into shared secret + Self::derive_shared_secret(&dh1, &dh2, &dh3, dh4.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand_core::OsRng; + + pub struct TestProtocol; + impl DomainSeparator for TestProtocol { + const BYTES: &'static [u8] = b"x3dh_tests_v1"; + } + + type X3DH = X3Handshake; + + #[test] + fn test_x3dh_with_onetime_key() { + 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); + + // Bob (responder) generates his keys + let bob_identity = StaticSecret::random_from_rng(&mut rng); + let bob_identity_pub = PublicKey::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_onetime_prekey = StaticSecret::random_from_rng(&mut rng); + let bob_onetime_prekey_pub = PublicKey::from(&bob_onetime_prekey); + + // Create Bob's prekey bundle (with one-time prekey) + let bob_bundle = PrekeyBundle { + identity_key: bob_identity_pub, + signed_prekey: bob_signed_prekey_pub, + signature: [0u8; 64], // Placeholder for signature + onetime_prekey: Some(bob_onetime_prekey_pub), + }; + + // Alice performs X3DH + let (alice_shared_secret, alice_ephemeral_pub) = + X3DH::initator(&alice_identity, &bob_bundle, &mut rng); + + // Bob performs X3DH + let bob_shared_secret = X3DH::responder( + &bob_identity, + &bob_signed_prekey, + Some(&bob_onetime_prekey), + &alice_identity_pub, + &alice_ephemeral_pub, + ); + + // Both should derive the same shared secret + assert_eq!(alice_shared_secret, bob_shared_secret); + } + + #[test] + fn test_x3dh_without_onetime_key() { + 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); + + // Bob (responder) generates his keys + let bob_identity = StaticSecret::random_from_rng(&mut rng); + let bob_identity_pub = PublicKey::from(&bob_identity); + + let bob_signed_prekey = StaticSecret::random_from_rng(&mut rng); + let bob_signed_prekey_pub = PublicKey::from(&bob_signed_prekey); + + // Create Bob's prekey bundle (without one-time prekey) + let bob_bundle = PrekeyBundle { + identity_key: bob_identity_pub, + signed_prekey: bob_signed_prekey_pub, + signature: [0u8; 64], // Placeholder for signature + onetime_prekey: None, + }; + + // Alice performs X3DH + let (alice_shared_secret, alice_ephemeral_pub) = + X3DH::initator(&alice_identity, &bob_bundle, &mut rng); + + // Bob performs X3DH + let bob_shared_secret = X3DH::responder( + &bob_identity, + &bob_signed_prekey, + None, + &alice_identity_pub, + &alice_ephemeral_pub, + ); + + // Both should derive the same shared secret + assert_eq!(alice_shared_secret, bob_shared_secret); + } +} diff --git a/rust_toolchain.toml b/rust_toolchain.toml new file mode 100644 index 0000000..02d090f --- /dev/null +++ b/rust_toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] \ No newline at end of file