From af3ff3c6a2901510d8b3814d4b33b9cd9fcae81f Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:04:45 -0700 Subject: [PATCH 01/39] Add GroupV1 --- Cargo.lock | 4 + Cargo.toml | 5 +- core/conversations/src/context.rs | 433 +++++++++++++++--- core/conversations/src/conversation.rs | 33 +- .../src/conversation/group_v1.rs | 433 ++++++++++++++++++ core/conversations/src/ctx.rs | 36 ++ core/conversations/src/errors.rs | 18 + core/conversations/src/external_traits.rs | 35 ++ core/conversations/src/inbox_v2.rs | 383 ++++++++++++++++ core/conversations/src/lib.rs | 7 + core/conversations/src/test_utils.rs | 310 +++++++++++++ core/conversations/src/types.rs | 54 ++- core/conversations/src/utils.rs | 63 +++ core/crypto/src/lib.rs | 2 + core/sqlite/src/lib.rs | 14 +- core/storage/src/store.rs | 5 + crates/client/src/client.rs | 4 +- crates/client/src/delivery.rs | 6 - crates/client/src/delivery_in_process.rs | 2 +- crates/client/src/lib.rs | 6 +- extensions/delivery/Cargo.toml | 6 + extensions/delivery/src/lib.rs | 5 + extensions/delivery/src/local_bcast.rs | 58 +++ 23 files changed, 1840 insertions(+), 82 deletions(-) create mode 100644 core/conversations/src/conversation/group_v1.rs create mode 100644 core/conversations/src/ctx.rs create mode 100644 core/conversations/src/external_traits.rs create mode 100644 core/conversations/src/inbox_v2.rs create mode 100644 core/conversations/src/test_utils.rs delete mode 100644 crates/client/src/delivery.rs create mode 100644 extensions/delivery/Cargo.toml create mode 100644 extensions/delivery/src/lib.rs create mode 100644 extensions/delivery/src/local_bcast.rs diff --git a/Cargo.lock b/Cargo.lock index 274c8bb..1e397a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,10 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "delivery" +version = "0.1.0" + [[package]] name = "der" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index db5f220..448b133 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,12 @@ members = [ "core/storage", "crates/client", "crates/client-ffi", - "bin/chat-cli", + "bin/chat-cli", "extensions/delivery", ] +# default-members = [ "core/*", "crates/*"] +default-members = [ "core/*"] + [workspace.dependencies] blake2 = "0.10" libchat = { path = "core/conversations" } diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index 9a48de7..0266aa1 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -1,39 +1,50 @@ use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; -use crypto::{Identity, PublicKey}; -use storage::{ChatStore, ConversationKind}; +use crate::conversation::{Convo, GroupConvo, GroupV1Convo, IdentityProvider}; +use crate::ctx::{self, ClientCtx}; use crate::account::LogosAccount; +use crate::{DeliveryService, RegistrationService}; use crate::{ - conversation::{Conversation, ConversationId, Convo, Id, PrivateV1Convo}, + conversation::{Conversation, ConversationId, Id, PrivateV1Convo}, errors::ChatError, inbox::Inbox, + inbox_v2::{GroupInitializer, InboxV2}, proto::{EncryptedPayload, EnvelopeV1, Message}, types::{AddressedEnvelope, ContentData}, }; +use crypto::{Identity, PublicKey}; +use storage::{ChatStore, ConversationKind}; pub use crate::conversation::ConversationIdOwned; pub use crate::inbox::Introduction; // This is the main entry point to the conversations api. // Ctx manages lifetimes of objects to process and generate payloads. -pub struct Context { +pub struct Context { _identity: Rc, - inbox: Inbox, - store: Rc>, - #[allow(unused)] // TODO: (P2) Remove once Account integrated in future PR. - account: LogosAccount, + client_ctx: ClientCtx, + inbox: Inbox, + pq_inbox: InboxV2, + store: Rc>, } -impl Context { +impl Context { /// Opens or creates a Context with the given storage configuration. /// /// If an identity exists in storage, it will be restored. /// Otherwise, a new identity will be created with the given name and saved. - pub fn new_from_store(name: impl Into, store: S) -> Result { + pub fn new_from_store( + name: impl Into, + delivery: DS, + contact_reg: RS, + store: CS, + ) -> Result { let name = name.into(); + let store = Rc::new(RefCell::new(store)); + let mut ctx = ClientCtx::new(delivery, contact_reg, store.clone()); // Load or create identity let identity = if let Some(identity) = store.borrow().load_identity()? { @@ -47,9 +58,18 @@ impl Context { let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&store), Rc::clone(&identity)); + let pq_inbox = InboxV2::new(); + + // Subscribe + ctx.ds() + .subscribe(pq_inbox.delivery_address()) + .map_err(ChatError::generic)?; + Ok(Self { _identity: identity, + client_ctx: ctx, inbox, + pq_inbox, store, account: LogosAccount::new_test(name.as_str()), }) @@ -58,10 +78,17 @@ impl Context { /// Creates a new in-memory Context (for testing). /// /// Uses in-memory SQLite database. Each call creates a new isolated database. - pub fn new_with_name(name: impl Into, chat_store: S) -> Self { + pub fn new_with_name( + name: impl Into, + delivery: DS, + contact_reg: RS, + chat_store: CS, + ) -> Result { let name = name.into(); let identity = Identity::new(&name); + let chat_store = Rc::new(RefCell::new(chat_store)); + let mut ctx = ClientCtx::new(delivery, contact_reg, chat_store.clone()); chat_store .borrow_mut() .save_identity(&identity) @@ -69,13 +96,27 @@ impl Context { let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&chat_store), Rc::clone(&identity)); + let mut pq_inbox = InboxV2::new(); + pq_inbox.register(&mut ctx)?; - Self { + ctx.ds() + .subscribe(pq_inbox.delivery_address()) + .map_err(ChatError::generic)?; + + Ok(Self { _identity: identity, + client_ctx: ctx, + pq_inbox, inbox, + store: chat_store, account: LogosAccount::new_test(name.as_str()), - } + }) + } + + /// Returns the unique identifier associated with the account + pub fn account_id(&self) -> String { + self.pq_inbox.account.friendly_name() } pub fn installation_name(&self) -> &str { @@ -96,7 +137,7 @@ impl Context { .invite_to_private_convo(remote_bundle, content, Rc::clone(&self.store)) .unwrap_or_else(|_| todo!("Log/Surface Error")); - let remote_id = Inbox::::inbox_identifier_for_key(*remote_bundle.installation_key()); + let remote_id = Inbox::::inbox_identifier_for_key(*remote_bundle.installation_key()); let payload_bytes = payloads .into_iter() .map(|p| p.into_envelope(remote_id.clone())) @@ -106,6 +147,23 @@ impl Context { Ok((convo_id, payload_bytes)) } + pub fn create_group_convo( + &mut self, + participants: &[&str], + ) -> Result>, ChatError> { + let mut convo = self.pq_inbox.create_group_v1(&mut self.client_ctx)?; + self.client_ctx + .store() + .save_conversation(&storage::ConversationMeta { + local_convo_id: convo.id().to_string(), + remote_convo_id: "0".into(), + kind: ConversationKind::GroupV1, + })?; + convo.add_member(&mut self.client_ctx, participants)?; + + Ok(Box::new(convo)) + } + pub fn list_conversations(&self) -> Result, ChatError> { let records = self.store.borrow().load_conversations()?; Ok(records @@ -119,41 +177,47 @@ impl Context { convo_id: ConversationId, content: &[u8], ) -> Result, ChatError> { - let convo = self.load_convo(convo_id)?; - - match convo { - Conversation::Private(mut convo) => { - let payloads = convo.send_message(content)?; - let remote_id = convo.remote_id(); - - Ok(payloads - .into_iter() - .map(|p| p.into_envelope(remote_id.clone())) - .collect()) - } - } + let mut convo = self.load_convo(convo_id)?; + let payloads = convo.send_message(content)?; + let remote_id = convo.remote_id(); + Ok(payloads + .into_iter() + .map(|p| p.into_envelope(remote_id.clone())) + .collect()) } // Decode bytes and send to protocol for processing. pub fn handle_payload(&mut self, payload: &[u8]) -> Result, ChatError> { let env = EnvelopeV1::decode(payload)?; + let e2 = env.clone(); // TODO: Impl Conversation hinting let convo_id = env.conversation_hint; - let enc = EncryptedPayload::decode(env.payload)?; + + let a = self.pq_inbox.id(); match convo_id { - c if c == self.inbox.id() => self.dispatch_to_inbox(enc), - c if self.store.borrow().has_conversation(&c)? => self.dispatch_to_convo(&c, enc), - _ => Ok(None), + c if c == self.inbox.id() => self.dispatch_to_inbox(&env.payload), + c if c == self.pq_inbox.id() => self.dispatch_to_inbox2(&env.payload), + c if self.store.borrow().has_conversation(&c)? => { + self.dispatch_to_convo(&c, &env.payload) + } + _ => Ok(Some(ContentData { + conversation_id: "".into(), + data: vec![], + is_new_convo: false, + })), } } // Dispatch encrypted payload to Inbox, and register the created Conversation fn dispatch_to_inbox( &mut self, - enc_payload: EncryptedPayload, + enc_payload_bytes: &[u8], ) -> Result, ChatError> { - let public_key_hex = Inbox::::extract_ephemeral_key_hex(&enc_payload)?; + // EncryptedPayloads are not used by GroupConvos at this time, else this can be performed in `handle_payload` + // TODO: (P1) reconcile envelope parsing between Covno and GroupConvo + let enc_payload = EncryptedPayload::decode(enc_payload_bytes)?; + let public_key_hex = Inbox::::extract_ephemeral_key_hex(&enc_payload)?; let (convo, content) = self.inbox .handle_frame(enc_payload, &public_key_hex, Rc::clone(&self.store))?; @@ -168,20 +232,22 @@ impl Context { Ok(content) } + // Dispatch encrypted payload to Inbox, and register the created Conversation + fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result, ChatError> { + self.pq_inbox.handle_frame(&mut self.client_ctx, payload)?; + + Ok(None) + } + // Dispatch encrypted payload to its corresponding conversation fn dispatch_to_convo( &mut self, convo_id: ConversationId, - enc_payload: EncryptedPayload, + enc_payload_bytes: &[u8], ) -> Result, ChatError> { - let convo = self.load_convo(convo_id)?; - - match convo { - Conversation::Private(mut convo) => { - let result = convo.handle_frame(enc_payload)?; - Ok(result) - } - } + let enc_payload = EncryptedPayload::decode(enc_payload_bytes)?; + let mut convo = self.load_convo(convo_id)?; + convo.handle_frame(enc_payload) } pub fn create_intro_bundle(&mut self) -> Result, ChatError> { @@ -190,7 +256,7 @@ impl Context { } /// Loads a conversation from DB by constructing it from metadata. - fn load_convo(&self, convo_id: ConversationId) -> Result, ChatError> { + fn load_convo(&mut self, convo_id: ConversationId) -> Result, ChatError> { let record = self .store .borrow() @@ -204,8 +270,37 @@ impl Context { record.local_convo_id, record.remote_convo_id, )?; - Ok(Conversation::Private(private_convo)) + Ok(Box::new(private_convo)) } + ConversationKind::GroupV1 => Ok(Box::new( + self.pq_inbox + .load_mls_convo(&mut self.client_ctx, record.local_convo_id)?, + )), + ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( + "unsupported conversation type: {}", + record.kind.as_str() + ))), + } + } + + fn load_group_convo( + &mut self, + convo_id: ConversationId, + ) -> Result>, ChatError> { + let record = self + .store + .borrow() + .load_conversation(convo_id)? + .ok_or_else(|| ChatError::NoConvo(convo_id.into()))?; + + match record.kind { + ConversationKind::PrivateV1 => { + Err(ChatError::NoConvo("This is not a group convo".into())) + } + ConversationKind::GroupV1 => Ok(Box::new( + self.pq_inbox + .load_mls_convo(&mut self.client_ctx, record.local_convo_id)?, + )), ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( "unsupported conversation type: {}", record.kind.as_str() @@ -214,17 +309,40 @@ impl Context { } } +impl GroupInitializer + for Context +{ + fn on_new_group_convo( + &self, + convo: impl crate::conversation::GroupConvo, + ) -> Result<(), ChatError> { + todo!() + } +} + #[cfg(test)] mod tests { + use std::{ + any::Any, + ops::{Deref, DerefMut}, + }; + use sqlite::{ChatStorage, StorageConfig}; use storage::{ConversationStore, IdentityStore}; use tempfile::tempdir; + use crate::{ + test_utils::{EphemeralRegistry, LocalBroadcaster, MemStore}, + utils::hex_trunc, + }; + use super::*; + type TestContext = Context; + fn send_and_verify( - sender: &mut Context, - receiver: &mut Context, + sender: &mut TestContext, + receiver: &mut TestContext, convo_id: ConversationId, content: &[u8], ) { @@ -238,10 +356,171 @@ mod tests { assert!(!received.is_new_convo); // Check that `is_new_convo` is FALSE } + // Simple client Functionality for testing + struct Client { + inner: Context, + on_content: Option>, + } + + impl Client { + fn init( + ctx: Context, + cb: Option, + ) -> Self { + Client { + inner: ctx, + on_content: cb.map(|f| Box::new(f) as Box), + } + } + + fn process_messages(&mut self) { + while let Some(data) = self.client_ctx.ds().poll() { + let res = self.handle_payload(&data).unwrap(); + if let Some(cb) = &self.on_content { + match res { + Some(content_data) => cb(content_data), + None => continue, + } + } + } + } + + fn convo( + &mut self, + convo_id: &str, + ) -> Box> { + // TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync + self.load_group_convo(convo_id).unwrap() + } + } + + impl Deref for Client { + type Target = Context; + + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + + // Higher order function to handle printing + fn pretty_print(prefix: impl Into) -> Box { + let prefix = prefix.into(); + return Box::new(move |c: ContentData| { + let cid = hex_trunc(c.conversation_id.as_bytes()); + let content = String::from_utf8(c.data).unwrap(); + println!("{} ({}) {}", prefix, cid, content) + }); + } + + fn process(clients: &mut Vec) { + for client in clients { + client.process_messages(); + } + } + + #[test] + fn create_group() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + + let saro_ctx = + Context::new_with_name("saro", ds.new_consumer(), rs.clone(), MemStore::new()).unwrap(); + let raya_ctx = + Context::new_with_name("raya", ds.clone(), rs.clone(), MemStore::new()).unwrap(); + + let mut clients = vec![ + Client::init(saro_ctx, Some(pretty_print(" Saro "))), + Client::init(raya_ctx, Some(pretty_print(" Raya "))), + ]; + + const SARO: usize = 0; + const RAYA: usize = 1; + + let raya_id = clients[RAYA].account_id(); + let s_convo = clients[SARO] + .create_group_convo(&[raya_id.as_ref()]) + .unwrap(); + + let CONVO_ID = s_convo.id(); + + // Raya can read this message because + // 1) It was sent after add_members was committed, and + // 2) LocalBroadcaster provides historical messages. + + clients[SARO] + .convo(CONVO_ID) + .send_content( + &mut clients[SARO].client_ctx, + b"ok who broke the group chat again", + ) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + clients[RAYA] + .convo(CONVO_ID) + .send_content( + &mut clients[RAYA].client_ctx, + b"it was literally working five minutes ago", + ) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); + clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax")))); + const PAX: usize = 2; + + let pax_id = clients[PAX].account_id(); + clients[SARO] + .convo(CONVO_ID) + .add_member(&mut clients[SARO].client_ctx, &[pax_id.as_ref()]) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + clients[PAX] + .convo(CONVO_ID) + .send_content( + &mut clients[PAX].client_ctx, + b"ngl the key rotation is cooked", + ) + .unwrap(); + + // clients[SARO].process_messages(); + + process(&mut clients); + + clients[SARO] + .convo(CONVO_ID) + .send_content( + &mut clients[SARO].client_ctx, + b"bro we literally just added you to the group ", + ) + .unwrap(); + + process(&mut clients); + // process(&mut clients); + } + #[test] fn ctx_integration() { - let mut saro = Context::new_with_name("saro", ChatStorage::in_memory()); - let mut raya = Context::new_with_name("raya", ChatStorage::in_memory()); + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + + let mut saro = + Context::new_with_name("saro", ds.clone(), rs.clone(), ChatStorage::in_memory()) + .unwrap(); + let mut raya = Context::new_with_name("raya", ds, rs, ChatStorage::in_memory()).unwrap(); // Raya creates intro bundle and sends to Saro let bundle = raya.create_intro_bundle().unwrap(); @@ -274,8 +553,10 @@ mod tests { #[test] fn identity_persistence() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap(); - let ctx1 = Context::new_with_name("alice", store1); + let ctx1 = Context::new_with_name("alice", ds, rs, store1).unwrap(); let pubkey1 = ctx1._identity.public_key(); let name1 = ctx1.installation_name().to_string(); @@ -291,8 +572,10 @@ mod tests { let db_path = dir.path().join("chat.sqlite"); let db_path = db_path.to_string_lossy().into_owned(); + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap(); - let ctx = Context::new_from_store("alice", store).unwrap(); + let ctx = Context::new_from_store("alice", ds, rs, store).unwrap(); let pubkey = ctx._identity.public_key(); drop(ctx); @@ -305,8 +588,12 @@ mod tests { #[test] fn conversation_metadata_persistence() { - let mut alice = Context::new_with_name("alice", ChatStorage::in_memory()); - let mut bob = Context::new_with_name("bob", ChatStorage::in_memory()); + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let mut alice = + Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()) + .unwrap(); + let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); let bundle = alice.create_intro_bundle().unwrap(); let intro = Introduction::try_from(bundle.as_slice()).unwrap(); @@ -323,8 +610,12 @@ mod tests { #[test] fn conversation_full_flow() { - let mut alice = Context::new_with_name("alice", ChatStorage::in_memory()); - let mut bob = Context::new_with_name("bob", ChatStorage::in_memory()); + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let mut alice = + Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()) + .unwrap(); + let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); let bundle = alice.create_intro_bundle().unwrap(); let intro = Introduction::try_from(bundle.as_slice()).unwrap(); @@ -364,4 +655,38 @@ mod tests { .expect("bob should receive"); assert_eq!(content.data, b"alice reply"); } + + #[test] + fn bcast_test() { + let mut a = LocalBroadcaster::new(); + let mut b = a.new_consumer(); + + a.subscribe("a".into()).unwrap(); + b.subscribe("b".into()).unwrap(); + + { + let e = AddressedEnvelope { + delivery_address: "a".into(), + data: (1..4).collect(), + }; + a.publish(e.clone()).unwrap(); + + let result = a.poll(); + assert!(result.unwrap() == e.data); + assert!(a.poll().is_none()); + } + + { + let e = AddressedEnvelope { + delivery_address: "b".into(), + data: (4..10).collect(), + }; + a.publish(e.clone()).unwrap(); + + dbg!(&b); + let result = b.poll(); + assert!(result.unwrap() == e.data); + assert!(b.poll().is_none()); + } + } } diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 1580d78..c172e97 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -1,12 +1,18 @@ +pub mod group_v1; mod privatev1; -use crate::types::{AddressedEncryptedPayload, ContentData}; +use crate::{ + DeliveryService, RegistrationService, + ctx::ClientCtx, + types::{AddressedEncryptedPayload, ContentData}, +}; use chat_proto::logoschat::encryption::EncryptedPayload; use std::fmt::Debug; use std::sync::Arc; -use storage::{ConversationKind, ConversationStore, RatchetStore}; +use storage::{ChatStore, ConversationKind, ConversationStore, RatchetStore}; pub use crate::errors::ChatError; +pub use group_v1::{GroupV1Convo, IdentityProvider, LogosMlsProvider}; pub use privatev1::PrivateV1Convo; pub type ConversationId<'a> = &'a str; @@ -36,6 +42,29 @@ pub trait Convo: Id + Debug { fn convo_type(&self) -> ConversationKind; } +pub trait GroupConvo: Convo { + fn add_member( + &mut self, + ctx: &mut ClientCtx, + members: &[&str], + ) -> Result<(), ChatError>; + + // Default implementation which dispatches envelopes to the DeliveryService + fn send_content( + &mut self, + ctx: &mut ClientCtx, + content: &[u8], + ) -> Result<(), ChatError> { + let payloads = self.send_message(content)?; + for payload in payloads { + ctx.ds() + .publish(payload.into_envelope(self.id().into())) + .map_err(|e| ChatError::Delivery(e.to_string()))?; + } + Ok(()) + } +} + pub enum Conversation { Private(PrivateV1Convo), } diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs new file mode 100644 index 0000000..9bd35a6 --- /dev/null +++ b/core/conversations/src/conversation/group_v1.rs @@ -0,0 +1,433 @@ +use std::cell::{Ref, RefCell}; +use std::rc::Rc; + +use blake2::{Blake2b, Digest, digest::consts::U6}; +use crypto::Ed25519VerifyingKey; +use openmls::prelude::*; +use openmls::{prelude::tls_codec::Deserialize, treesync::RatchetTree}; +use openmls_libcrux_crypto::Provider as LibcruxProvider; + +use openmls::prelude::MlsMessageBodyIn; +use openmls_traits::signatures::Signer as OpenMlsSigner; +use openmls_traits::storage::StorageProvider; +use prost::Message; + +use crate::{ + AddressedEnvelope, DeliveryService, RegistrationService, + conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, + ctx::ClientCtx, + types::{AddressedEncryptedPayload, ContentData}, +}; +use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; +use storage::{ChatStore, ConversationKind}; + +pub trait IdentityProvider: OpenMlsSigner { + fn friendly_name(&self) -> String; + fn public_key(&self) -> Ed25519VerifyingKey; + // fn installation_key() -> u8; +} + +pub trait MlsInitializer { + fn invite_to_group_v1( + &self, + ctx: &mut ClientCtx, + account_id: &str, + welcome: &MlsMessageOut, + // ratchet_tree: RatchetTree, // Embedded + ) -> Result<(), ChatError>; +} + +pub trait MlsCtx { + type IDENT: IdentityProvider; + type INIT: MlsInitializer; + + fn ident(&self) -> &Self::IDENT; + fn provider(&self) -> Ref<'_, LibcruxProvider>; + fn init(&self) -> &Self::INIT; + + // Build an MLS Credential from the supplied IdentityProvider + fn get_credential(&self) -> CredentialWithKey; +} + +pub trait LogosMlsProvider: OpenMlsProvider {} + +pub trait GroupMlsStorageV1 { + fn save_state(&self, state: &[u8]); + fn load_state(&self) -> Vec; +} + +pub struct GroupV1Convo { + ctx: Rc>, + pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility + convo_id: String, +} + +impl std::fmt::Debug for GroupV1Convo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GroupV1Convo") + .field("name", &self.ctx.borrow().ident().friendly_name()) + .field("convo_id", &self.convo_id) + .field("mls_epoch", &self.mls_group.epoch()) + .finish_non_exhaustive() + } +} + +impl GroupV1Convo { + pub fn new(ctx: Ctx, ds: &mut DS) -> Self { + let config = Self::mls_create_config(); + let ctx = Rc::new(RefCell::new(ctx)); + let mls_group = { + let ctx_ref = ctx.borrow(); + MlsGroup::new( + &*ctx_ref.provider(), + ctx_ref.ident(), + &config, + ctx_ref.get_credential(), + ) + .unwrap() + }; + let convo_id = hex::encode(mls_group.group_id().as_slice()); + Self::subscribe(ds, &convo_id); + + println!( + "@ Create Convo: {}. {}. d:{} dc:{}", + ctx.borrow().ident().friendly_name(), + convo_id, + Self::delivery_address_from_id(&convo_id), + Self::ctrl_delivery_address_from_id(&convo_id) + ); + Self { + ctx, + mls_group, + convo_id, + } + } + + pub fn new_from_welcome( + ctx: Rc>, + ds: &mut DS, + welcome: Welcome, + ) -> Self { + let mls_group = { + let ctx_borrow = ctx.borrow(); + let provider = ctx_borrow.provider(); + + StagedWelcome::build_from_welcome(&*provider, &Self::mls_join_config(), welcome) + .unwrap() + .build() + .unwrap() + .into_group(&*provider) + .unwrap() + }; + + let convo_id = hex::encode(mls_group.group_id().as_slice()); + Self::subscribe(ds, &convo_id); + + println!( + "@ Welcome Convo: I:{}. {}. d:{} dc:{}", + ctx.borrow().ident().friendly_name(), + convo_id, + Self::delivery_address_from_id(&convo_id), + Self::ctrl_delivery_address_from_id(&convo_id) + ); + + GroupV1Convo { + ctx, + mls_group, + convo_id, + } + } + + pub fn load( + ctx: Rc>, + ds: &mut DS, + convo_id: String, + group_id: GroupId, + ) -> Result { + let Some(mls_group) = MlsGroup::load(ctx.borrow().provider().storage(), &group_id) + .map_err(ChatError::generic)? + else { + return Err(ChatError::NoConvo("mls group not found".into())); + }; + + // println!( + // "\n>>> {}. {:?}", + // ctx.borrow().ident().friendly_name(), + // mls_group + // ); + Self::subscribe(ds, &convo_id); + + Ok(GroupV1Convo { + ctx, + mls_group, + convo_id, + }) + } + + fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { + ds.subscribe(Self::delivery_address_from_id(&convo_id)) + .map_err(ChatError::generic)?; + ds.subscribe(Self::ctrl_delivery_address_from_id(&convo_id)) + .map_err(ChatError::generic)?; + + Ok(()) + } + + pub fn ratchet_tree(&self) -> RatchetTree { + self.mls_group.export_ratchet_tree() + } + + fn mls_create_config() -> MlsGroupCreateConfig { + MlsGroupCreateConfig::builder() + .ciphersuite(Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519) + .use_ratchet_tree_extension(true) // This is handy for now, until there is central store for this data + .build() + } + + fn mls_join_config() -> MlsGroupJoinConfig { + MlsGroupJoinConfig::builder().build() + } + + fn delivery_address_from_id(convo_id: &str) -> String { + let hash = Blake2b::::new() + .chain_update("delivery_addr|") + .chain_update(convo_id) + .finalize(); + hex::encode(hash) + } + + fn delivery_address(&self) -> String { + Self::delivery_address_from_id(&self.convo_id) + } + + fn ctrl_delivery_address_from_id(convo_id: &str) -> String { + let hash = Blake2b::::new() + .chain_update("ctrl_delivery_addr|") + .chain_update(convo_id) + .finalize(); + hex::encode(hash) + } + + fn ctrl_delivery_address(&self) -> String { + Self::ctrl_delivery_address_from_id(&self.convo_id) + } + + fn key_package_for_account( + &self, + ctx: &mut ClientCtx, + ident: &str, + ) -> Result { + let retrieved_bytes = ctx + .contact_registry() + .retreive(ident) + .map_err(|e| ChatError::Generic(e.to_string()))?; + + // dbg!(ctx.contact_registry()); + let Some(keypkg_bytes) = retrieved_bytes else { + return Err(ChatError::Protocol("Contact Not Found".into())); + }; + + let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?; + let keypkg = key_package_in.validate( + self.ctx.borrow().provider().crypto(), + ProtocolVersion::Mls10, + )?; //TODO: P3 - Hardcoded Protocol Version + Ok(keypkg) + } + + fn save_state(&self, store: &CS) {} +} + +impl Id for GroupV1Convo { + fn id(&self) -> ConversationId<'_> { + &self.convo_id + } +} + +impl Convo for GroupV1Convo { + fn send_message( + &mut self, + content: &[u8], + ) -> Result, ChatError> { + let ctx_ref = self.ctx.borrow(); + let provider = ctx_ref.provider(); + let mls_message_out = self + .mls_group + .create_message(&*provider, ctx_ref.ident(), content) + .unwrap(); + + let a = AddressedEncryptedPayload { + delivery_address: self.delivery_address(), + data: EncryptedPayload { + encryption: Some( + chat_proto::logoschat::encryption::encrypted_payload::Encryption::Plaintext( + Plaintext { + payload: mls_message_out.to_bytes().unwrap().into(), + }, + ), + ), + }, + }; + + Ok(vec![a]) + } + + fn handle_frame( + &mut self, + encoded_payload: EncryptedPayload, + ) -> Result, ChatError> { + use chat_proto::logoschat::encryption::encrypted_payload::Encryption; + + let bytes = match encoded_payload.encryption { + Some(Encryption::Plaintext(pt)) => pt.payload, + _ => { + return Err(ChatError::ProtocolExpectation( + "None", + "Some(Encryption::Plaintext)".into(), + )); + } + }; + + let mls_message = + MlsMessageIn::tls_deserialize_exact_bytes(&bytes).map_err(ChatError::generic)?; + + let protocol_message: ProtocolMessage = mls_message + .try_into_protocol_message() + .map_err(ChatError::generic)?; + + let ctx_borrow = self.ctx.borrow(); + let provider = ctx_borrow.provider(); + + if protocol_message.epoch() < self.mls_group.epoch() { + // TODO: (!) Determine how to handle messages for old epochs. Minimally log this. + return Ok(None); + } + + let processed = self + .mls_group + .process_message(&*provider, protocol_message) + .map_err(ChatError::generic)?; + + match processed.into_content() { + ProcessedMessageContent::ApplicationMessage(msg) => Ok(Some(ContentData { + conversation_id: hex::encode(self.mls_group.group_id().as_slice()), + data: msg.into_bytes(), + is_new_convo: false, + })), + ProcessedMessageContent::StagedCommitMessage(commit) => { + self.mls_group + .merge_staged_commit(&*provider, *commit) + .map_err(ChatError::generic)?; + Ok(None) + } + x => { + println!("Unhabled Message ttype {:?}", x); + Ok(None) + } + } + } + + fn remote_id(&self) -> String { + // "group_remote_id".into() + todo!() + } + + fn convo_type(&self) -> storage::ConversationKind { + ConversationKind::GroupV1 + } +} + +impl + GroupConvo for GroupV1Convo +{ + fn add_member( + &mut self, + ctx: &mut ClientCtx, + members: &[&str], + ) -> Result<(), ChatError> { + // add_members returns: + // commit — the Commit message Alice broadcasts to all members + // welcome — the Welcome message sent privately to each new joiner + // _group_info — used for external joins; ignore for now + let ctx_ref = self.ctx.borrow(); + let provider = ctx_ref.provider(); + + if members.len() > 50 { + // This is a temporary limit that originates from the the De-MLS epoch time. + return Err(ChatError::Protocol( + "Cannot add more than 50 Members at a time".into(), + )); + } + + // Get the Keypacakages and transpose any errors. + // The account_id is kept so invites can be addressed properly + let keypkgs = members + .iter() + // .map(|ident| self.key_package_for_account(ctx, ident)) + .map(|ident| self.key_package_for_account(ctx, ident)) + .collect::, ChatError>>()?; + + let (commit, welcome, _group_info) = self + .mls_group + .add_members(&*provider, ctx_ref.ident(), keypkgs.iter().as_slice()) + .unwrap(); + + self.mls_group.merge_pending_commit(&*provider).unwrap(); + + // TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users + for account_id in members { + ctx_ref + .init() + .invite_to_group_v1(ctx, account_id, &welcome)?; + } + + let encrypted_payload = EncryptedPayload { + encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext { + payload: commit.to_bytes()?.into(), + })), + }; + + let addr_enc_payload = AddressedEncryptedPayload { + delivery_address: self.ctrl_delivery_address(), + data: encrypted_payload, + }; + // Prepare commit message + // TODO: (P1) Make GroupConvos agnostic to framing so its less error prone and more + let env = addr_enc_payload.into_envelope(self.convo_id.clone()); + + ctx.ds() + .publish(env) + .map_err(|e| ChatError::Generic(format!("Publish: {e}"))) + } +} + +use prost::Oneof; + +#[derive(Clone, PartialEq, Message)] +pub struct GroupV1Frame { + #[prost(string, tag = "1")] + pub sender: String, + + #[prost(uint64, tag = "2")] + pub timestamp: u64, + + // oneof field — optional, holds one variant + #[prost(oneof = "FrameType", tags = "3, 4, 5")] + pub payload: Option, +} + +#[derive(Clone, PartialEq, Oneof)] +pub enum FrameType { + #[prost(bytes, tag = "3")] + Welcome(Vec), +} + +#[cfg(test)] +mod tests { + use crypto::PrivateKey; + + use super::*; + + #[test] + fn test_mls() {} +} diff --git a/core/conversations/src/ctx.rs b/core/conversations/src/ctx.rs new file mode 100644 index 0000000..f7e3d10 --- /dev/null +++ b/core/conversations/src/ctx.rs @@ -0,0 +1,36 @@ +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +use storage::ChatStore; + +use crate::{DeliveryService, RegistrationService}; + +pub struct ClientCtx { + ds: DS, + contact_registry: RS, + convo_store: Rc>, // TODO: (P2) Remove Rc/Refcell +} + +impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx { + pub fn new(ds: DS, contact_registry: RS, convo_store: Rc>) -> Self { + Self { + ds, + contact_registry, + convo_store, + } + } + + pub fn ds(&'a mut self) -> &'a mut DS { + &mut self.ds + } + + pub fn contact_registry(&'a mut self) -> &'a mut RS { + &mut self.contact_registry + } + + pub fn store(&'a self) -> RefMut { + self.convo_store.borrow_mut() + } +} diff --git a/core/conversations/src/errors.rs b/core/conversations/src/errors.rs index 664cdd3..5b2bfe4 100644 --- a/core/conversations/src/errors.rs +++ b/core/conversations/src/errors.rs @@ -1,3 +1,4 @@ +use openmls::{framing::errors::MlsMessageError, prelude::tls_codec}; pub use thiserror::Error; use storage::StorageError; @@ -26,6 +27,23 @@ pub enum ChatError { UnsupportedConvoType(String), #[error("storage error: {0}")] Storage(#[from] StorageError), + #[error("mls error: {0}")] + MlsMessageError(#[from] MlsMessageError), + #[error("TlsCodec: {0}")] + TlsCodec(#[from] tls_codec::Error), + #[error("generic: {0}")] + Generic(String), + #[error("KeyPackage: {0}")] + KeyPackage(#[from] openmls::prelude::KeyPackageVerifyError), + #[error("Delivery: {0}")] + Delivery(String), +} + +impl ChatError { + // This is a stopgap until there is a proper error system in place + pub fn generic(e: impl ToString) -> Self { + Self::Generic(e.to_string()) + } } #[derive(Error, Debug)] diff --git a/core/conversations/src/external_traits.rs b/core/conversations/src/external_traits.rs new file mode 100644 index 0000000..8f6f3fa --- /dev/null +++ b/core/conversations/src/external_traits.rs @@ -0,0 +1,35 @@ +use std::{cell::RefCell, fmt::Debug, fmt::Display, rc::Rc}; + +use crate::types::AddressedEnvelope; + +pub struct Service { + inner: Rc>, +} + +impl Service { + pub fn new(t: T) -> Self { + Self { + inner: Rc::new(RefCell::new(t)), + } + } + + fn with(&self, f: F) -> R + where + F: FnOnce(&T) -> R, + { + let inner = self.inner.borrow(); + f(&inner) + } +} + +pub trait DeliveryService { + type Error: Display; + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>; + fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error>; +} + +pub trait RegistrationService: Debug { + type Error: Display; + fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error>; + fn retreive(&self, identity: &str) -> Result>, Self::Error>; +} diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs new file mode 100644 index 0000000..b94bffa --- /dev/null +++ b/core/conversations/src/inbox_v2.rs @@ -0,0 +1,383 @@ +use std::any::Any; +use std::cell::{Ref, RefCell}; +use std::collections::HashMap; +use std::rc::Rc; + +use chat_proto::logoschat::envelope::EnvelopeV1; +use crypto::Ed25519SigningKey; +use crypto::Ed25519VerifyingKey; +use crypto::PublicKey; +use openmls::prelude::tls_codec::Serialize; +use openmls::{prelude::*, treesync::RatchetTree}; +use openmls_libcrux_crypto::Provider as LibcruxProvider; +use openmls_traits::signatures::Signer; +use openmls_traits::storage::StorageProvider; +use prost::{Message, Oneof}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use storage::ChatStore; +use storage::ConversationMeta; +use storage::ConversationStore; + +use crate::AddressedEnvelope; +use crate::ChatError; +use crate::DeliveryService; +use crate::RegistrationService; +use crate::conversation::GroupConvo; +use crate::conversation::group_v1::{MlsCtx, MlsInitializer}; +use crate::conversation::{GroupV1Convo, IdentityProvider}; +use crate::ctx::ClientCtx; +use crate::types::AddressedEncryptedPayload; +use crate::utils::hash_size::Testing; +use crate::utils::{blake2b_hex, hash_size, hex_trunc}; + +static ACCOUNT_COUNTER: AtomicUsize = AtomicUsize::new(0); + +const ACCOUNT_NAMES: &[&str] = &["Saro", "Raya", "Pax"]; + +#[derive(Clone)] +pub struct LogosAccount { + id: String, + signing_key: Ed25519SigningKey, + // x25519_key: crypto::PrivateKey, +} + +impl LogosAccount { + pub fn new() -> Self { + let idx = ACCOUNT_COUNTER.fetch_add(1, Ordering::Relaxed); + + let id = if idx < ACCOUNT_NAMES.len() { + ACCOUNT_NAMES[idx % ACCOUNT_NAMES.len()].to_string() + } else { + use rand_core::{OsRng, RngCore}; + const CHARSET: &[u8] = + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let i: String = (0..8) + .map(|_| { + let idx = (OsRng.next_u32() as usize) % CHARSET.len(); + CHARSET[idx] as char + }) + .collect(); + i + }; + Self { + id, + signing_key: Ed25519SigningKey::generate(), + // x25519_key: crypto::PrivateKey::random(), + } + } +} + +impl Signer for LogosAccount { + fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { + Ok(self.signing_key.sign(payload).as_ref().to_vec()) + } + + fn signature_scheme(&self) -> SignatureScheme { + SignatureScheme::ED25519 + } +} + +impl IdentityProvider for LogosAccount { + fn friendly_name(&self) -> String { + self.id.clone() + } + + fn public_key(&self) -> Ed25519VerifyingKey { + self.signing_key.verifying_key() + } +} + +#[derive(Clone)] +pub struct MlsContext { + pub ident_provider: LogosAccount, + pub initializer: Init, + provider: Rc>, +} + +impl MlsCtx for MlsContext { + type IDENT = LogosAccount; + type INIT = Init; + + fn ident(&self) -> &LogosAccount { + &self.ident_provider + } + + fn provider(&self) -> Ref<'_, LibcruxProvider> { + self.provider.borrow() + } + + fn init(&self) -> &Init { + &self.initializer + } + + // Build an MLS Credential from the supplied IdentityProvider + fn get_credential(&self) -> CredentialWithKey { + CredentialWithKey { + credential: BasicCredential::new(self.ident_provider.friendly_name().into()).into(), + signature_key: self.ident_provider.public_key().as_ref().into(), + } + } +} + +pub trait GroupInitializer { + fn on_new_group_convo(&self, convo: impl GroupConvo) -> Result<(), ChatError>; +} + +#[derive(Clone)] +pub struct InboxV2 { + pub account: LogosAccount, // TODO: (!) don't expose account + mls_provider: Rc>, + convo_map: HashMap>, +} + +impl<'a> InboxV2 { + pub fn new() -> Self { + let account = LogosAccount::new(); + let mls_provider = Rc::new(RefCell::new(LibcruxProvider::new().unwrap())); + Self { + account, + mls_provider, + convo_map: HashMap::new(), + } + } + + pub fn register( + &mut self, + ctx: &mut ClientCtx, + ) -> Result<(), ChatError> { + let keypackage = self.create_keypackage()?; + + let bytes = keypackage.tls_serialize_detached()?; + + ctx.contact_registry() + .register(self.account.friendly_name(), bytes) + .map_err(ChatError::generic)?; //TODO: (P1) create an address scheme instead of using names + Ok(()) + } + + pub fn delivery_address(&self) -> String { + Self::delivery_address_for_account_id(&self.account.id) + } + + pub fn id(&self) -> String { + Self::conversation_id_for_account_id(&self.account.id) + } + + pub fn create_group_v1( + &self, + ctx: &mut ClientCtx, + ) -> Result>, ChatError> { + let convo = GroupV1Convo::new(self.assemble_ctx(), ctx.ds()); + Ok(convo) + } + + pub fn handle_frame( + &self, + ctx: &mut ClientCtx, + payload_bytes: &[u8], + ) -> Result<(), ChatError> { + let inbox_frame = InboxV2Frame::decode(payload_bytes)?; + + let Some(payload) = inbox_frame.payload else { + return Err(ChatError::BadParsing("InboxV2Payload missing")); + }; + + match payload { + InviteType::GroupV1(group_v1_heavy_invite) => { + self.handle_heavy_invite(ctx, group_v1_heavy_invite) + } + } + } + + fn assemble_ctx(&self) -> MlsContext { + MlsContext { + ident_provider: self.account.clone(), + initializer: self.clone(), + provider: self.mls_provider.clone(), + } + } + + fn persist_convo( + &self, + ctx: &'a ClientCtx, + convo: impl GroupConvo, + ) -> Result<(), ChatError> { + // TODO: (P2) Remove remote_convo_id this is an implementation detail specific to PrivateV1 + // TODO: (P3) Implement From for ConversationMeta + let meta = ConversationMeta { + local_convo_id: convo.id().to_string(), + remote_convo_id: "0".into(), + kind: storage::ConversationKind::GroupV1, + }; + ctx.store().save_conversation(&meta)?; + // TODO: (P1) Persist state + Ok(()) + } + + fn handle_heavy_invite( + &self, + ctx: &mut ClientCtx, + invite: GroupV1HeavyInvite, + ) -> Result<(), ChatError> { + let (msg_in, rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?; + + let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else { + return Err(ChatError::ProtocolExpectation( + "something else", + "Welcome".into(), + )); + }; + + let mls_ctx = Rc::new(RefCell::new(self.assemble_ctx())); + + let convo = GroupV1Convo::new_from_welcome(mls_ctx, ctx.ds(), welcome); + self.persist_convo(ctx, convo) + } + + fn create_keypackage(&self) -> Result { + let mls_ctx = self.assemble_ctx(); + + let capabilities = Capabilities::builder() + .ciphersuites(vec![ + Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, + ]) + .extensions(vec![ExtensionType::ApplicationId]) + .build(); + let a = KeyPackage::builder() + .leaf_node_capabilities(capabilities) + .build( + Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, + &*mls_ctx.provider(), + &self.account, + mls_ctx.get_credential(), + ) + .expect("Failed to build KeyPackage"); + + Ok(a.key_package().clone()) + } + + fn delivery_address_for_account_id(account_id: &str) -> String { + blake2b_hex::(&["InboxV2|", "delivery_address|", account_id]) + } + + fn conversation_id_for_account_id(account_id: &str) -> String { + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id]) + } + + fn dbg_mls_store(ctx: &MlsContext, prefix: impl AsRef) { + let pa = ctx.provider.borrow(); + let data = &*pa.storage().values.read().unwrap(); + + println!(":::MlsProviderStore::: -- {}", prefix.as_ref()); + for key in data.keys() { + let val = match data.get(key) { + Some(x) => format!("{} ({})", hex_trunc(x), blake2b_hex::(&[x])), + None => "None".into(), + }; + + println!(". {:?}: {:?}", hex_trunc(key), val) + } + } + + pub fn load_mls_convo( + &self, + ctx: &mut ClientCtx, + convo_id: String, + ) -> Result>, ChatError> { + let mls_ctx = self.assemble_ctx(); + + let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?; + let group_id = GroupId::from_slice(&group_id_bytes); + let convo = + GroupV1Convo::load(Rc::new(RefCell::new(mls_ctx)), ctx.ds(), convo_id, group_id)?; + + Ok(convo) + } +} + +impl MlsInitializer for InboxV2 { + fn invite_to_group_v1( + &self, + ctx: &mut ClientCtx, + account_id: &str, + welcome: &MlsMessageOut, + ) -> Result<(), ChatError> { + let invite = GroupV1HeavyInvite { + welcome_bytes: welcome.to_bytes()?, + }; + + let frame = InboxV2Frame { + payload: Some(InviteType::GroupV1(invite)), + }; + + let envelope = EnvelopeV1 { + conversation_hint: Self::conversation_id_for_account_id(account_id), + salt: 0, + payload: frame.encode_to_vec().into(), + }; + + let outbound_msg = AddressedEnvelope { + delivery_address: Self::delivery_address_for_account_id(account_id), + data: envelope.encode_to_vec(), + }; + + ctx.ds().publish(outbound_msg).map_err(ChatError::generic)?; + Ok(()) + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct InboxV2Frame { + #[prost(oneof = "InviteType", tags = "1")] + pub payload: Option, +} + +#[derive(Clone, PartialEq, Oneof)] +pub enum InviteType { + #[prost(message, tag = "1")] + GroupV1(GroupV1HeavyInvite), +} + +#[derive(Clone, PartialEq, Message)] +pub struct GroupV1HeavyInvite { + #[prost(bytes, tag = "1")] + pub welcome_bytes: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use openmls_traits::signatures::Signer; + + struct Account { + name: String, + signing_key: crypto::Ed25519SigningKey, + } + + impl Signer for Account { + fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { + Ok(self.signing_key.sign(payload).as_ref().to_vec()) + } + + fn signature_scheme(&self) -> SignatureScheme { + SignatureScheme::ED25519 + } + } + + impl IdentityProvider for Account { + fn friendly_name(&self) -> String { + self.name.clone() + } + + fn public_key(&self) -> Ed25519VerifyingKey { + todo!() + } + } + + #[test] + fn dev() { + // let inbox = InboxV2::new(...); + // let group = inbox.create_group_v1().unwrap(); + // let bytes = group.send("hello".as_bytes()); + } +} diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 8de610b..a979083 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -2,15 +2,22 @@ mod account; mod context; mod conversation; mod crypto; +mod ctx; mod errors; +mod external_traits; mod inbox; +mod inbox_v2; mod proto; mod types; mod utils; pub use account::LogosAccount; +#[cfg(test)] +mod test_utils; + pub use context::{Context, ConversationIdOwned, Introduction}; pub use errors::ChatError; +pub use external_traits::{DeliveryService, RegistrationService}; pub use sqlite::ChatStorage; pub use sqlite::StorageConfig; pub use types::{AddressedEnvelope, ContentData}; diff --git a/core/conversations/src/test_utils.rs b/core/conversations/src/test_utils.rs new file mode 100644 index 0000000..7384230 --- /dev/null +++ b/core/conversations/src/test_utils.rs @@ -0,0 +1,310 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet, VecDeque}, + fmt::Debug, + io::Cursor, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use storage::{ChatStore, ConversationMeta, ConversationStore, IdentityStore}; +use storage::{EphemeralKeyStore, RatchetStore}; + +use crate::{ + AddressedEnvelope, DeliveryService, RegistrationService, + utils::{blake2b_hex, hash_size::Testing, hex_trunc}, +}; + +type Callback = Rc)>; + +type Filter = Box) -> bool>; + +#[derive(Debug)] +struct BroadcasterShared { + /// Per-address message queue; all published messages are appended here. + messages: VecDeque, + base_index: usize, +} + +impl BroadcasterShared { + pub fn read(&self, cursor: usize) -> Option<&T> { + self.messages.get(cursor + self.base_index) + } + + pub fn tail(&self) -> usize { + self.messages.len() + self.base_index + } +} + +#[derive(Clone, Debug)] +pub struct LocalBroadcaster { + shared: Rc>>, + cursor: usize, + subscriptions: HashSet, + outbound_msgs: Vec, +} + +impl LocalBroadcaster { + pub fn new() -> Self { + let shared = Rc::new(RefCell::new(BroadcasterShared { + messages: VecDeque::new(), + base_index: 0, + })); + + let cursor = shared.borrow().tail(); + Self { + shared, + cursor, + subscriptions: HashSet::new(), + outbound_msgs: Vec::new(), + } + } + + /// Returns a new consumer that shares the same message store but has its + /// own independent cursor — it starts from the beginning of each address + /// queue regardless of what any other consumer has already processed. + pub fn new_consumer(&self) -> Self { + let mut inner = self.shared.clone(); + let cursor = inner.borrow().tail(); + Self { + shared: inner, + cursor, + subscriptions: HashSet::new(), + outbound_msgs: Vec::new(), + } + } + + /// Pulls all messages this consumer has not yet seen on `address`, + /// applying any registered filter. Advances the cursor so the same + /// messages are not returned again. + pub fn poll(&mut self) -> Option> { + loop { + let next = self.cursor; + match self.shared.borrow().read(next) { + None => return None, + Some(ae) => { + self.cursor = next + 1; + if self.subscriptions.contains(ae.delivery_address.as_str()) + && self.is_inbound(ae) + { + return Some(ae.data.clone()); + } + } + } + } + } + + pub fn clear(&mut self) { + self.cursor = self.shared.borrow().tail(); + } + + fn msg_id(msg: &AddressedEnvelope) -> String { + blake2b_hex::(&[msg.data.as_slice()]) + } + + fn is_inbound(&self, msg: &AddressedEnvelope) -> bool { + let mid = Self::msg_id(msg); + !self.outbound_msgs.contains(&mid) + } +} + +impl DeliveryService for LocalBroadcaster { + type Error = String; + + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> { + self.outbound_msgs.push(Self::msg_id(&envelope)); + self.shared.borrow_mut().messages.push_back(envelope); + + Ok(()) + } + + fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error> { + // Strict temporal ordering of subscriptions is not enforced. + // Subscruptions are evaluated on polling, not when the message is published + self.subscriptions.insert(delivery_address); + Ok(()) + } +} + +/// A Contact Registry used for Tests. +/// This implementation stores bundle bytes and then returns them when +/// retreived +/// + +#[derive(Clone)] +pub struct EphemeralRegistry { + registry: Arc>>>, +} + +impl EphemeralRegistry { + pub fn new() -> Self { + Self { + registry: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +impl Debug for EphemeralRegistry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let registry = self.registry.lock().unwrap(); + let truncated: Vec<(&String, String)> = registry + .iter() + .map(|(k, v)| { + let hex = if v.len() <= 8 { + hex::encode(v) + } else { + format!( + "{}..{}", + hex::encode(&v[..4]), + hex::encode(&v[v.len() - 4..]) + ) + }; + (k, hex) + }) + .collect(); + f.debug_struct("EphemeralRegistry") + .field("registry", &truncated) + .finish() + } +} + +impl RegistrationService for EphemeralRegistry { + type Error = String; + + fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error> { + self.registry.lock().unwrap().insert(identity, key_bundle); + Ok(()) + } + + fn retreive(&self, identity: &str) -> Result>, Self::Error> { + Ok(self.registry.lock().unwrap().get(identity).cloned()) + } +} + +pub struct MemStore { + convos: HashMap, + state: HashMap>, +} + +impl MemStore { + pub fn new() -> Self { + Self { + convos: HashMap::new(), + state: HashMap::new(), + } + } +} + +impl ConversationStore for MemStore { + fn save_conversation( + &mut self, + meta: &storage::ConversationMeta, + ) -> Result<(), storage::StorageError> { + self.convos + .insert(meta.local_convo_id.clone(), meta.clone()); + Ok(()) + } + + fn load_conversation( + &self, + local_convo_id: &str, + ) -> Result, storage::StorageError> { + let a = self.convos.get(local_convo_id).cloned(); + Ok(a) + } + + fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_conversations(&self) -> Result, storage::StorageError> { + Ok(self.convos.values().cloned().collect()) + } + + fn has_conversation(&self, local_convo_id: &str) -> Result { + Ok(self.convos.contains_key(local_convo_id)) + } +} + +impl IdentityStore for MemStore { + fn load_identity(&self) -> Result, storage::StorageError> { + // todo!() + Ok(None) + } + + fn save_identity(&mut self, identity: &crypto::Identity) -> Result<(), storage::StorageError> { + // todo!() + Ok(()) + } +} + +impl EphemeralKeyStore for MemStore { + fn save_ephemeral_key( + &mut self, + public_key_hex: &str, + private_key: &crypto::PrivateKey, + ) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_ephemeral_key( + &self, + public_key_hex: &str, + ) -> Result, storage::StorageError> { + todo!() + } + + fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), storage::StorageError> { + todo!() + } +} + +impl RatchetStore for MemStore { + fn save_ratchet_state( + &mut self, + conversation_id: &str, + state: &storage::RatchetStateRecord, + skipped_keys: &[storage::SkippedKeyRecord], + ) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_ratchet_state( + &self, + conversation_id: &str, + ) -> Result { + todo!() + } + + fn load_skipped_keys( + &self, + conversation_id: &str, + ) -> Result, storage::StorageError> { + todo!() + } + + fn has_ratchet_state(&self, conversation_id: &str) -> Result { + todo!() + } + + fn delete_ratchet_state(&mut self, conversation_id: &str) -> Result<(), storage::StorageError> { + todo!() + } + + fn cleanup_old_skipped_keys( + &mut self, + max_age_secs: i64, + ) -> Result { + todo!() + } +} + +// impl GroupMlsStorageV1 for MemStore { +// fn save_state(&self, convo_id: &str, state: &[u8]) { +// self.state.insert(convo_id, state) +// } + +// fn load_state(&self, convo_id: &str) -> Vec { +// self.state.get(convo_id).unwrap().clone() +// } +// } diff --git a/core/conversations/src/types.rs b/core/conversations/src/types.rs index e8ecc70..b60ab84 100644 --- a/core/conversations/src/types.rs +++ b/core/conversations/src/types.rs @@ -6,13 +6,51 @@ use crate::proto::{self, Message}; // This struct represents Outbound data. // It wraps an encoded payload with a delivery address, so it can be handled by the delivery service. +#[derive(Clone)] pub struct AddressedEnvelope { pub delivery_address: String, pub data: Vec, } +impl AddressedEnvelope { + pub fn new(delivery_address: String, convo_id: String, data: &[u8]) -> Self { + let envelope = proto::EnvelopeV1 { + // TODO: conversation_id should be obscured + conversation_hint: convo_id, + salt: 0, + payload: proto::Bytes::copy_from_slice(data), + }; + + AddressedEnvelope { + delivery_address, + data: envelope.encode_to_vec(), + } + } +} + +impl Debug for AddressedEnvelope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let data = &self.data; + let hex = if data.len() <= 8 { + hex::encode(data) + } else { + format!( + "{}..{}", + hex::encode(&data[..4]), + hex::encode(&data[data.len() - 4..]) + ) + }; + + f.debug_struct("AddressedEnvelope") + .field("addr", &self.delivery_address) + .field("data", &hex) + .finish() + } +} + // This struct represents the result of processed inbound data. // It wraps content payload with a conversation_id +#[derive(Debug)] pub struct ContentData { pub conversation_id: String, pub data: Vec, @@ -30,17 +68,11 @@ pub(crate) struct AddressedEncryptedPayload { impl AddressedEncryptedPayload { // Wrap in an envelope and prepare for transmission pub fn into_envelope(self, convo_id: String) -> AddressedEnvelope { - let envelope = proto::EnvelopeV1 { - // TODO: conversation_id should be obscured - conversation_hint: convo_id, - salt: 0, - payload: proto::Bytes::copy_from_slice(self.data.encode_to_vec().as_slice()), - }; - - AddressedEnvelope { - delivery_address: self.delivery_address, - data: envelope.encode_to_vec(), - } + AddressedEnvelope::new( + self.delivery_address, + convo_id, + self.data.encode_to_vec().as_slice(), + ) } } diff --git a/core/conversations/src/utils.rs b/core/conversations/src/utils.rs index 306e898..4649c3d 100644 --- a/core/conversations/src/utils.rs +++ b/core/conversations/src/utils.rs @@ -6,3 +6,66 @@ pub fn timestamp_millis() -> i64 { .unwrap() .as_millis() as i64 } + +/// Track hash sizes in use across the crate. +pub mod hash_size { + use blake2::digest::{ + consts::U64, + generic_array::ArrayLength, + typenum::{IsLessOrEqual, NonZero}, + }; + + pub trait HashLen + where + >::Output: NonZero, + { + type Size: ArrayLength + IsLessOrEqual; + } + + /// This macro generates HashLen for the given typenum::length + macro_rules! hash_sizes { + ($($(#[$attr:meta])* $name:ident => $size:ty),* $(,)?) => { + $( + $(#[$attr])* + pub struct $name; + impl HashLen for $name { type Size = $size; } + )* + }; + } + + use blake2::digest::consts::{U4, U8, U18}; + hash_sizes! { + /// Generic hash size for tests and debug + Testing => U4, + /// Account ID hash length + AccountId => U8, + ConversationId => U18, + } +} + +use blake2::{Blake2b, Digest}; +/// This establishes an easy to use wrapper for hashes in this crate. +/// The output is formatted string of hex characters +pub fn blake2b_hex(components: &[impl AsRef<[u8]>]) -> String { + //A + let mut hash = Blake2b::::new(); + + for c in components { + hash.update(c); + } + + let output = hash.finalize(); + hex::encode(output) +} + +pub fn hex_trunc(data: &[u8]) -> String { + if data.len() <= 8 { + hex::encode(data) + } else { + format!( + "{}..{}", + hex::encode(&data[..4]), + hex::encode(&data[data.len() - 4..]) + ) + } +} diff --git a/core/crypto/src/lib.rs b/core/crypto/src/lib.rs index 1759091..754a37c 100644 --- a/core/crypto/src/lib.rs +++ b/core/crypto/src/lib.rs @@ -4,6 +4,8 @@ mod signatures; mod x3dh; mod xeddsa_sign; +use thiserror::Error; + pub use identity::Identity; pub use keys::{PrivateKey, PublicKey, SymmetricKey32}; pub use signatures::{Ed25519SigningKey, Ed25519VerifyingKey}; diff --git a/core/sqlite/src/lib.rs b/core/sqlite/src/lib.rs index 8c57bb3..8382598 100644 --- a/core/sqlite/src/lib.rs +++ b/core/sqlite/src/lib.rs @@ -10,8 +10,8 @@ use std::collections::HashSet; use crypto::{Identity, PrivateKey}; use rusqlite::{Transaction, params}; use storage::{ - ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore, - RatchetStateRecord, RatchetStore, SkippedKeyRecord, StorageError, + ChatStore, ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, + IdentityStore, RatchetStateRecord, RatchetStore, SkippedKeyRecord, StorageError, }; use zeroize::Zeroize; @@ -532,6 +532,16 @@ fn blob_to_array( .map_err(|_| invalid_blob_length(field, N, actual)) } +// impl GroupMlsStorageV1 for ChatStorage { +// fn save_state(&self, convo_id: &str, state: &[u8]) { +// todo!() +// } + +// fn load_state(&self, convo_id: &str) -> Vec { +// todo!() +// } +// } + #[cfg(test)] mod tests { use storage::{ diff --git a/core/storage/src/store.rs b/core/storage/src/store.rs index a24ad25..d53b16c 100644 --- a/core/storage/src/store.rs +++ b/core/storage/src/store.rs @@ -27,6 +27,7 @@ pub trait EphemeralKeyStore { pub enum ConversationKind { PrivateV1, Unknown(String), + GroupV1, } impl ConversationKind { @@ -34,6 +35,7 @@ impl ConversationKind { match self { Self::PrivateV1 => "private_v1", Self::Unknown(value) => value.as_str(), + Self::GroupV1 => "group_v1", } } } @@ -42,6 +44,7 @@ impl From<&str> for ConversationKind { fn from(value: &str) -> Self { match value { "private_v1" => Self::PrivateV1, + "group_v1" => Self::GroupV1, other => Self::Unknown(other.to_string()), } } @@ -120,6 +123,8 @@ pub trait RatchetStore { fn cleanup_old_skipped_keys(&mut self, max_age_secs: i64) -> Result; } +// TODO: (P2) this should be defined in the ConversationType + pub trait ChatStore: IdentityStore + EphemeralKeyStore + ConversationStore + RatchetStore {} impl ChatStore for T where T: IdentityStore + EphemeralKeyStore + ConversationStore + RatchetStore diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c95a0f1..50e6099 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,9 +1,9 @@ use libchat::{ AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, - Introduction, StorageConfig, + DeliveryService, Introduction, StorageConfig, }; -use crate::{delivery::DeliveryService, errors::ClientError}; +use crate::errors::ClientError; pub struct ChatClient { ctx: Context, diff --git a/crates/client/src/delivery.rs b/crates/client/src/delivery.rs deleted file mode 100644 index 853de0d..0000000 --- a/crates/client/src/delivery.rs +++ /dev/null @@ -1,6 +0,0 @@ -use libchat::AddressedEnvelope; - -pub trait DeliveryService { - type Error: std::fmt::Debug; - fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>; -} diff --git a/crates/client/src/delivery_in_process.rs b/crates/client/src/delivery_in_process.rs index ae9d03a..6cceb25 100644 --- a/crates/client/src/delivery_in_process.rs +++ b/crates/client/src/delivery_in_process.rs @@ -1,4 +1,4 @@ -use crate::{AddressedEnvelope, delivery::DeliveryService}; +use crate::{AddressedEnvelope, DeliveryService}; use std::collections::HashMap; use std::convert::Infallible; use std::sync::{Arc, RwLock}; diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index cfd9074..a0cac6f 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,12 +1,12 @@ mod client; -mod delivery; mod delivery_in_process; mod errors; pub use client::ChatClient; -pub use delivery::DeliveryService; pub use delivery_in_process::{Cursor, InProcessDelivery, MessageBus}; pub use errors::ClientError; // Re-export types callers need to interact with ChatClient -pub use libchat::{AddressedEnvelope, ContentData, ConversationIdOwned, StorageConfig}; +pub use libchat::{ + AddressedEnvelope, ContentData, ConversationIdOwned, DeliveryService, StorageConfig, +}; diff --git a/extensions/delivery/Cargo.toml b/extensions/delivery/Cargo.toml new file mode 100644 index 0000000..f55ccf8 --- /dev/null +++ b/extensions/delivery/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "delivery" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/extensions/delivery/src/lib.rs b/extensions/delivery/src/lib.rs new file mode 100644 index 0000000..2b14bd0 --- /dev/null +++ b/extensions/delivery/src/lib.rs @@ -0,0 +1,5 @@ +mod local_bcast; + +use local_bcast::LocalBroadcast; + +pub use LocalBroadcast; diff --git a/extensions/delivery/src/local_bcast.rs b/extensions/delivery/src/local_bcast.rs new file mode 100644 index 0000000..20b1335 --- /dev/null +++ b/extensions/delivery/src/local_bcast.rs @@ -0,0 +1,58 @@ +use libchat::DeliveryService; + +type Callback = Box)>; + +#[derive(Clone)] +struct LocalBroadcaster { + subscribers: Arc>>>, +} + +impl LocalBroadcaster { + pub fn new() -> Self { + Self { + subscribers: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +impl DeliveryService for LocalBroadcaster { + type Error = String; + + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> { + let callbacks = self + .subscribers + .lock() + .unwrap() + .remove(&envelope.delivery_address) + .unwrap_or_default(); + + for cb in callbacks { + cb(envelope.delivery_address.clone(), &envelope.data); + } + + Ok(()) + } + + fn subscribe(&mut self, delivery_address: String, cb: F) -> Result<(), Self::Error> + where + F: FnOnce(String, &Vec) + 'static, + { + self.subscribers + .lock() + .unwrap() + .entry(delivery_address) + .or_default() + .push(Box::new(cb)); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn local_bcast() { + let ds = LocalBroadcast::new(); + } +} From 1d1a3a170e48e9c0b2370a014879b0e97b587072 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:19:41 -0700 Subject: [PATCH 02/39] Clean warnings --- core/conversations/src/context.rs | 37 +++------- core/conversations/src/conversation.rs | 2 +- .../src/conversation/group_v1.rs | 58 +-------------- core/conversations/src/ctx.rs | 4 +- core/conversations/src/external_traits.rs | 22 +----- core/conversations/src/inbox_v2.rs | 72 +------------------ core/conversations/src/test_utils.rs | 48 ++++++------- core/conversations/src/types.rs | 2 +- core/conversations/src/utils.rs | 2 + core/crypto/src/lib.rs | 2 - core/sqlite/src/lib.rs | 4 +- 11 files changed, 46 insertions(+), 207 deletions(-) diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index 0266aa1..a97b487 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; -use crate::conversation::{Convo, GroupConvo, GroupV1Convo, IdentityProvider}; -use crate::ctx::{self, ClientCtx}; +use crate::conversation::{Convo, GroupConvo, IdentityProvider}; +use crate::ctx::ClientCtx; use crate::account::LogosAccount; use crate::{DeliveryService, RegistrationService}; @@ -10,7 +10,7 @@ use crate::{ conversation::{Conversation, ConversationId, Id, PrivateV1Convo}, errors::ChatError, inbox::Inbox, - inbox_v2::{GroupInitializer, InboxV2}, + inbox_v2::InboxV2, proto::{EncryptedPayload, EnvelopeV1, Message}, types::{AddressedEnvelope, ContentData}, }; @@ -189,12 +189,10 @@ impl Cont // Decode bytes and send to protocol for processing. pub fn handle_payload(&mut self, payload: &[u8]) -> Result, ChatError> { let env = EnvelopeV1::decode(payload)?; - let e2 = env.clone(); // TODO: Impl Conversation hinting let convo_id = env.conversation_hint; - let a = self.pq_inbox.id(); match convo_id { c if c == self.inbox.id() => self.dispatch_to_inbox(&env.payload), c if c == self.pq_inbox.id() => self.dispatch_to_inbox2(&env.payload), @@ -283,6 +281,7 @@ impl Cont } } + #[allow(unused)] // Temporary until GroupIntegration is completed fn load_group_convo( &mut self, convo_id: ConversationId, @@ -309,23 +308,9 @@ impl Cont } } -impl GroupInitializer - for Context -{ - fn on_new_group_convo( - &self, - convo: impl crate::conversation::GroupConvo, - ) -> Result<(), ChatError> { - todo!() - } -} - #[cfg(test)] mod tests { - use std::{ - any::Any, - ops::{Deref, DerefMut}, - }; + use std::ops::{Deref, DerefMut}; use sqlite::{ChatStorage, StorageConfig}; use storage::{ConversationStore, IdentityStore}; @@ -447,14 +432,14 @@ mod tests { .create_group_convo(&[raya_id.as_ref()]) .unwrap(); - let CONVO_ID = s_convo.id(); + let convo_id = s_convo.id(); // Raya can read this message because // 1) It was sent after add_members was committed, and // 2) LocalBroadcaster provides historical messages. clients[SARO] - .convo(CONVO_ID) + .convo(convo_id) .send_content( &mut clients[SARO].client_ctx, b"ok who broke the group chat again", @@ -465,7 +450,7 @@ mod tests { process(&mut clients); clients[RAYA] - .convo(CONVO_ID) + .convo(convo_id) .send_content( &mut clients[RAYA].client_ctx, b"it was literally working five minutes ago", @@ -481,7 +466,7 @@ mod tests { let pax_id = clients[PAX].account_id(); clients[SARO] - .convo(CONVO_ID) + .convo(convo_id) .add_member(&mut clients[SARO].client_ctx, &[pax_id.as_ref()]) .unwrap(); @@ -489,7 +474,7 @@ mod tests { process(&mut clients); clients[PAX] - .convo(CONVO_ID) + .convo(convo_id) .send_content( &mut clients[PAX].client_ctx, b"ngl the key rotation is cooked", @@ -501,7 +486,7 @@ mod tests { process(&mut clients); clients[SARO] - .convo(CONVO_ID) + .convo(convo_id) .send_content( &mut clients[SARO].client_ctx, b"bro we literally just added you to the group ", diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index c172e97..6c6685f 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use storage::{ChatStore, ConversationKind, ConversationStore, RatchetStore}; pub use crate::errors::ChatError; -pub use group_v1::{GroupV1Convo, IdentityProvider, LogosMlsProvider}; +pub use group_v1::{GroupV1Convo, IdentityProvider}; pub use privatev1::PrivateV1Convo; pub type ConversationId<'a> = &'a str; diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 9bd35a6..85c37e4 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -3,17 +3,14 @@ use std::rc::Rc; use blake2::{Blake2b, Digest, digest::consts::U6}; use crypto::Ed25519VerifyingKey; +use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::*; -use openmls::{prelude::tls_codec::Deserialize, treesync::RatchetTree}; use openmls_libcrux_crypto::Provider as LibcruxProvider; -use openmls::prelude::MlsMessageBodyIn; use openmls_traits::signatures::Signer as OpenMlsSigner; -use openmls_traits::storage::StorageProvider; -use prost::Message; use crate::{ - AddressedEnvelope, DeliveryService, RegistrationService, + DeliveryService, RegistrationService, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, ctx::ClientCtx, types::{AddressedEncryptedPayload, ContentData}, @@ -49,13 +46,6 @@ pub trait MlsCtx { fn get_credential(&self) -> CredentialWithKey; } -pub trait LogosMlsProvider: OpenMlsProvider {} - -pub trait GroupMlsStorageV1 { - fn save_state(&self, state: &[u8]); - fn load_state(&self) -> Vec; -} - pub struct GroupV1Convo { ctx: Rc>, pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility @@ -150,12 +140,7 @@ impl GroupV1Convo { return Err(ChatError::NoConvo("mls group not found".into())); }; - // println!( - // "\n>>> {}. {:?}", - // ctx.borrow().ident().friendly_name(), - // mls_group - // ); - Self::subscribe(ds, &convo_id); + Self::subscribe(ds, &convo_id)?; Ok(GroupV1Convo { ctx, @@ -173,10 +158,6 @@ impl GroupV1Convo { Ok(()) } - pub fn ratchet_tree(&self) -> RatchetTree { - self.mls_group.export_ratchet_tree() - } - fn mls_create_config() -> MlsGroupCreateConfig { MlsGroupCreateConfig::builder() .ciphersuite(Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519) @@ -234,8 +215,6 @@ impl GroupV1Convo { )?; //TODO: P3 - Hardcoded Protocol Version Ok(keypkg) } - - fn save_state(&self, store: &CS) {} } impl Id for GroupV1Convo { @@ -400,34 +379,3 @@ impl .map_err(|e| ChatError::Generic(format!("Publish: {e}"))) } } - -use prost::Oneof; - -#[derive(Clone, PartialEq, Message)] -pub struct GroupV1Frame { - #[prost(string, tag = "1")] - pub sender: String, - - #[prost(uint64, tag = "2")] - pub timestamp: u64, - - // oneof field — optional, holds one variant - #[prost(oneof = "FrameType", tags = "3, 4, 5")] - pub payload: Option, -} - -#[derive(Clone, PartialEq, Oneof)] -pub enum FrameType { - #[prost(bytes, tag = "3")] - Welcome(Vec), -} - -#[cfg(test)] -mod tests { - use crypto::PrivateKey; - - use super::*; - - #[test] - fn test_mls() {} -} diff --git a/core/conversations/src/ctx.rs b/core/conversations/src/ctx.rs index f7e3d10..8264ff0 100644 --- a/core/conversations/src/ctx.rs +++ b/core/conversations/src/ctx.rs @@ -1,5 +1,5 @@ use std::{ - cell::{Ref, RefCell, RefMut}, + cell::{RefCell, RefMut}, rc::Rc, }; @@ -30,7 +30,7 @@ impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx< &mut self.contact_registry } - pub fn store(&'a self) -> RefMut { + pub fn store(&'a self) -> RefMut<'a, CS> { self.convo_store.borrow_mut() } } diff --git a/core/conversations/src/external_traits.rs b/core/conversations/src/external_traits.rs index 8f6f3fa..c5422c9 100644 --- a/core/conversations/src/external_traits.rs +++ b/core/conversations/src/external_traits.rs @@ -1,27 +1,7 @@ -use std::{cell::RefCell, fmt::Debug, fmt::Display, rc::Rc}; +use std::{fmt::Debug, fmt::Display}; use crate::types::AddressedEnvelope; -pub struct Service { - inner: Rc>, -} - -impl Service { - pub fn new(t: T) -> Self { - Self { - inner: Rc::new(RefCell::new(t)), - } - } - - fn with(&self, f: F) -> R - where - F: FnOnce(&T) -> R, - { - let inner = self.inner.borrow(); - f(&inner) - } -} - pub trait DeliveryService { type Error: Display; fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>; diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index b94bffa..2f492bc 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -1,22 +1,17 @@ -use std::any::Any; use std::cell::{Ref, RefCell}; -use std::collections::HashMap; use std::rc::Rc; use chat_proto::logoschat::envelope::EnvelopeV1; use crypto::Ed25519SigningKey; use crypto::Ed25519VerifyingKey; -use crypto::PublicKey; use openmls::prelude::tls_codec::Serialize; -use openmls::{prelude::*, treesync::RatchetTree}; +use openmls::prelude::*; use openmls_libcrux_crypto::Provider as LibcruxProvider; use openmls_traits::signatures::Signer; -use openmls_traits::storage::StorageProvider; use prost::{Message, Oneof}; use std::sync::atomic::{AtomicUsize, Ordering}; use storage::ChatStore; use storage::ConversationMeta; -use storage::ConversationStore; use crate::AddressedEnvelope; use crate::ChatError; @@ -26,9 +21,7 @@ use crate::conversation::GroupConvo; use crate::conversation::group_v1::{MlsCtx, MlsInitializer}; use crate::conversation::{GroupV1Convo, IdentityProvider}; use crate::ctx::ClientCtx; -use crate::types::AddressedEncryptedPayload; -use crate::utils::hash_size::Testing; -use crate::utils::{blake2b_hex, hash_size, hex_trunc}; +use crate::utils::{blake2b_hex, hash_size}; static ACCOUNT_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -119,15 +112,10 @@ impl MlsCtx for MlsContext { } } -pub trait GroupInitializer { - fn on_new_group_convo(&self, convo: impl GroupConvo) -> Result<(), ChatError>; -} - #[derive(Clone)] pub struct InboxV2 { pub account: LogosAccount, // TODO: (!) don't expose account mls_provider: Rc>, - convo_map: HashMap>, } impl<'a> InboxV2 { @@ -137,7 +125,6 @@ impl<'a> InboxV2 { Self { account, mls_provider, - convo_map: HashMap::new(), } } @@ -219,7 +206,7 @@ impl<'a> InboxV2 { ctx: &mut ClientCtx, invite: GroupV1HeavyInvite, ) -> Result<(), ChatError> { - let (msg_in, rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?; + let (msg_in, _rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?; let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else { return Err(ChatError::ProtocolExpectation( @@ -264,21 +251,6 @@ impl<'a> InboxV2 { blake2b_hex::(&["InboxV2|", "conversation_id|", account_id]) } - fn dbg_mls_store(ctx: &MlsContext, prefix: impl AsRef) { - let pa = ctx.provider.borrow(); - let data = &*pa.storage().values.read().unwrap(); - - println!(":::MlsProviderStore::: -- {}", prefix.as_ref()); - for key in data.keys() { - let val = match data.get(key) { - Some(x) => format!("{} ({})", hex_trunc(x), blake2b_hex::(&[x])), - None => "None".into(), - }; - - println!(". {:?}: {:?}", hex_trunc(key), val) - } - } - pub fn load_mls_convo( &self, ctx: &mut ClientCtx, @@ -343,41 +315,3 @@ pub struct GroupV1HeavyInvite { #[prost(bytes, tag = "1")] pub welcome_bytes: Vec, } - -#[cfg(test)] -mod tests { - use super::*; - use openmls_traits::signatures::Signer; - - struct Account { - name: String, - signing_key: crypto::Ed25519SigningKey, - } - - impl Signer for Account { - fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { - Ok(self.signing_key.sign(payload).as_ref().to_vec()) - } - - fn signature_scheme(&self) -> SignatureScheme { - SignatureScheme::ED25519 - } - } - - impl IdentityProvider for Account { - fn friendly_name(&self) -> String { - self.name.clone() - } - - fn public_key(&self) -> Ed25519VerifyingKey { - todo!() - } - } - - #[test] - fn dev() { - // let inbox = InboxV2::new(...); - // let group = inbox.create_group_v1().unwrap(); - // let bytes = group.send("hello".as_bytes()); - } -} diff --git a/core/conversations/src/test_utils.rs b/core/conversations/src/test_utils.rs index 7384230..2f23da6 100644 --- a/core/conversations/src/test_utils.rs +++ b/core/conversations/src/test_utils.rs @@ -2,23 +2,18 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, fmt::Debug, - io::Cursor, rc::Rc, sync::{Arc, Mutex}, }; -use storage::{ChatStore, ConversationMeta, ConversationStore, IdentityStore}; +use storage::{ConversationMeta, ConversationStore, IdentityStore}; use storage::{EphemeralKeyStore, RatchetStore}; use crate::{ AddressedEnvelope, DeliveryService, RegistrationService, - utils::{blake2b_hex, hash_size::Testing, hex_trunc}, + utils::{blake2b_hex, hash_size::Testing}, }; -type Callback = Rc)>; - -type Filter = Box) -> bool>; - #[derive(Debug)] struct BroadcasterShared { /// Per-address message queue; all published messages are appended here. @@ -64,7 +59,7 @@ impl LocalBroadcaster { /// own independent cursor — it starts from the beginning of each address /// queue regardless of what any other consumer has already processed. pub fn new_consumer(&self) -> Self { - let mut inner = self.shared.clone(); + let inner = self.shared.clone(); let cursor = inner.borrow().tail(); Self { shared: inner, @@ -94,10 +89,6 @@ impl LocalBroadcaster { } } - pub fn clear(&mut self) { - self.cursor = self.shared.borrow().tail(); - } - fn msg_id(msg: &AddressedEnvelope) -> String { blake2b_hex::(&[msg.data.as_slice()]) } @@ -183,14 +174,12 @@ impl RegistrationService for EphemeralRegistry { pub struct MemStore { convos: HashMap, - state: HashMap>, } impl MemStore { pub fn new() -> Self { Self { convos: HashMap::new(), - state: HashMap::new(), } } } @@ -213,7 +202,7 @@ impl ConversationStore for MemStore { Ok(a) } - fn remove_conversation(&mut self, local_convo_id: &str) -> Result<(), storage::StorageError> { + fn remove_conversation(&mut self, _local_convo_id: &str) -> Result<(), storage::StorageError> { todo!() } @@ -232,7 +221,7 @@ impl IdentityStore for MemStore { Ok(None) } - fn save_identity(&mut self, identity: &crypto::Identity) -> Result<(), storage::StorageError> { + fn save_identity(&mut self, _identity: &crypto::Identity) -> Result<(), storage::StorageError> { // todo!() Ok(()) } @@ -241,20 +230,20 @@ impl IdentityStore for MemStore { impl EphemeralKeyStore for MemStore { fn save_ephemeral_key( &mut self, - public_key_hex: &str, - private_key: &crypto::PrivateKey, + _public_key_hex: &str, + _private_key: &crypto::PrivateKey, ) -> Result<(), storage::StorageError> { todo!() } fn load_ephemeral_key( &self, - public_key_hex: &str, + _public_key_hex: &str, ) -> Result, storage::StorageError> { todo!() } - fn remove_ephemeral_key(&mut self, public_key_hex: &str) -> Result<(), storage::StorageError> { + fn remove_ephemeral_key(&mut self, _public_key_hex: &str) -> Result<(), storage::StorageError> { todo!() } } @@ -262,38 +251,41 @@ impl EphemeralKeyStore for MemStore { impl RatchetStore for MemStore { fn save_ratchet_state( &mut self, - conversation_id: &str, - state: &storage::RatchetStateRecord, - skipped_keys: &[storage::SkippedKeyRecord], + _conversation_id: &str, + _state: &storage::RatchetStateRecord, + _skipped_keys: &[storage::SkippedKeyRecord], ) -> Result<(), storage::StorageError> { todo!() } fn load_ratchet_state( &self, - conversation_id: &str, + _conversation_id: &str, ) -> Result { todo!() } fn load_skipped_keys( &self, - conversation_id: &str, + _conversation_id: &str, ) -> Result, storage::StorageError> { todo!() } - fn has_ratchet_state(&self, conversation_id: &str) -> Result { + fn has_ratchet_state(&self, _conversation_id: &str) -> Result { todo!() } - fn delete_ratchet_state(&mut self, conversation_id: &str) -> Result<(), storage::StorageError> { + fn delete_ratchet_state( + &mut self, + _conversation_id: &str, + ) -> Result<(), storage::StorageError> { todo!() } fn cleanup_old_skipped_keys( &mut self, - max_age_secs: i64, + _max_age_secs: i64, ) -> Result { todo!() } diff --git a/core/conversations/src/types.rs b/core/conversations/src/types.rs index b60ab84..b5b174b 100644 --- a/core/conversations/src/types.rs +++ b/core/conversations/src/types.rs @@ -60,7 +60,7 @@ pub struct ContentData { // Internal type Definitions // Used by Conversations to attach addresses to outbound encrypted payloads -pub(crate) struct AddressedEncryptedPayload { +pub struct AddressedEncryptedPayload { pub delivery_address: String, pub data: proto::EncryptedPayload, } diff --git a/core/conversations/src/utils.rs b/core/conversations/src/utils.rs index 4649c3d..3b51997 100644 --- a/core/conversations/src/utils.rs +++ b/core/conversations/src/utils.rs @@ -58,6 +58,8 @@ pub fn blake2b_hex(components: &[impl AsRef<[u8]>]) -> hex::encode(output) } +/// Shorten byte slices for testing and logging +#[allow(unused)] pub fn hex_trunc(data: &[u8]) -> String { if data.len() <= 8 { hex::encode(data) diff --git a/core/crypto/src/lib.rs b/core/crypto/src/lib.rs index 754a37c..1759091 100644 --- a/core/crypto/src/lib.rs +++ b/core/crypto/src/lib.rs @@ -4,8 +4,6 @@ mod signatures; mod x3dh; mod xeddsa_sign; -use thiserror::Error; - pub use identity::Identity; pub use keys::{PrivateKey, PublicKey, SymmetricKey32}; pub use signatures::{Ed25519SigningKey, Ed25519VerifyingKey}; diff --git a/core/sqlite/src/lib.rs b/core/sqlite/src/lib.rs index 8382598..bb7ea3c 100644 --- a/core/sqlite/src/lib.rs +++ b/core/sqlite/src/lib.rs @@ -10,8 +10,8 @@ use std::collections::HashSet; use crypto::{Identity, PrivateKey}; use rusqlite::{Transaction, params}; use storage::{ - ChatStore, ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, - IdentityStore, RatchetStateRecord, RatchetStore, SkippedKeyRecord, StorageError, + ConversationKind, ConversationMeta, ConversationStore, EphemeralKeyStore, IdentityStore, + RatchetStateRecord, RatchetStore, SkippedKeyRecord, StorageError, }; use zeroize::Zeroize; From d9904ad3b04003a3818bf740db69c64811e89254 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:47:35 -0700 Subject: [PATCH 03/39] Remove dead test --- core/conversations/src/context.rs | 34 ------------------------------- 1 file changed, 34 deletions(-) diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index a97b487..895ef64 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -640,38 +640,4 @@ mod tests { .expect("bob should receive"); assert_eq!(content.data, b"alice reply"); } - - #[test] - fn bcast_test() { - let mut a = LocalBroadcaster::new(); - let mut b = a.new_consumer(); - - a.subscribe("a".into()).unwrap(); - b.subscribe("b".into()).unwrap(); - - { - let e = AddressedEnvelope { - delivery_address: "a".into(), - data: (1..4).collect(), - }; - a.publish(e.clone()).unwrap(); - - let result = a.poll(); - assert!(result.unwrap() == e.data); - assert!(a.poll().is_none()); - } - - { - let e = AddressedEnvelope { - delivery_address: "b".into(), - data: (4..10).collect(), - }; - a.publish(e.clone()).unwrap(); - - dbg!(&b); - let result = b.poll(); - assert!(result.unwrap() == e.data); - assert!(b.poll().is_none()); - } - } } From aa380adb3776957f68e3e134d7e5bd2801b8d4ec Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:12:20 -0700 Subject: [PATCH 04/39] Re-use components in integration tests --- Cargo.lock | 26 +- Cargo.toml | 22 +- core/conversations/Cargo.toml | 1 + core/conversations/src/context.rs | 368 ++---------------- .../src/conversation/privatev1.rs | 3 +- core/conversations/src/lib.rs | 3 +- core/integration_tests_core/Cargo.toml | 15 + core/integration_tests_core/src/lib.rs | 23 ++ .../tests/mls_integration.rs | 177 +++++++++ .../tests/private_integration.rs | 165 ++++++++ crates/client/src/client.rs | 6 +- extensions/components/Cargo.toml | 11 + extensions/components/src/contact_registry.rs | 62 +++ extensions/components/src/delivery.rs | 3 + .../src/delivery/local_broadcaster.rs | 116 ++++++ extensions/components/src/lib.rs | 7 + extensions/components/src/storage.rs | 3 + .../components/src/storage/in_memory_store.rs | 130 +++++++ extensions/delivery/Cargo.toml | 6 - extensions/delivery/src/lib.rs | 5 - extensions/delivery/src/local_bcast.rs | 58 --- 21 files changed, 784 insertions(+), 426 deletions(-) create mode 100644 core/integration_tests_core/Cargo.toml create mode 100644 core/integration_tests_core/src/lib.rs create mode 100644 core/integration_tests_core/tests/mls_integration.rs create mode 100644 core/integration_tests_core/tests/private_integration.rs create mode 100644 extensions/components/Cargo.toml create mode 100644 extensions/components/src/contact_registry.rs create mode 100644 extensions/components/src/delivery.rs create mode 100644 extensions/components/src/delivery/local_broadcaster.rs create mode 100644 extensions/components/src/lib.rs create mode 100644 extensions/components/src/storage.rs create mode 100644 extensions/components/src/storage/in_memory_store.rs delete mode 100644 extensions/delivery/Cargo.toml delete mode 100644 extensions/delivery/src/lib.rs delete mode 100644 extensions/delivery/src/local_bcast.rs diff --git a/Cargo.lock b/Cargo.lock index 1e397a6..edbb5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "components" +version = "0.1.0" +dependencies = [ + "crypto", + "hex", + "libchat", + "storage", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -633,10 +643,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "delivery" -version = "0.1.0" - [[package]] name = "der" version = "0.7.10" @@ -1317,6 +1323,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "integration_tests_core" +version = "0.1.0" +dependencies = [ + "chat-sqlite", + "components", + "libchat", + "storage", + "tempfile", +] + [[package]] name = "inventory" version = "0.3.24" @@ -1402,6 +1419,7 @@ dependencies = [ "blake2", "chat-proto", "chat-sqlite", + "components", "crypto", "double-ratchets", "hex", diff --git a/Cargo.toml b/Cargo.toml index 448b133..9fb2e90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,18 +8,28 @@ members = [ "core/crypto", "core/double-ratchets", "core/storage", + "core/integration_tests_core", "crates/client", "crates/client-ffi", - "bin/chat-cli", "extensions/delivery", + "bin/chat-cli", + "extensions/components", ] -# default-members = [ "core/*", "crates/*"] -default-members = [ "core/*"] +default-members = [ + "core/sqlite", + "core/conversations", + "core/crypto", + "core/double-ratchets", + "core/storage", + "core/integration_tests_core", +] [workspace.dependencies] -blake2 = "0.10" -libchat = { path = "core/conversations" } -storage = { path = "core/storage" } + blake2 = "0.10" + crypto = { path = "core/crypto" } + libchat = { path = "core/conversations" } + sqlite = { path = "core/sqlite"} + storage = { path = "core/storage" } # Panicking across FFI boundaries is UB; abort is the correct strategy for a # C FFI library. diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index f5492c4..5ee70d1 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -24,4 +24,5 @@ openmls = { version = "0.8.1", features = ["libcrux-provider"] } openmls_traits = "0.5.0" [dev-dependencies] +components = { package = "components", path = "../../extensions/components" } tempfile = "3" diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index 895ef64..ae968e6 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -1,3 +1,4 @@ +use std::cell::Ref; use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; @@ -7,7 +8,7 @@ use crate::ctx::ClientCtx; use crate::account::LogosAccount; use crate::{DeliveryService, RegistrationService}; use crate::{ - conversation::{Conversation, ConversationId, Id, PrivateV1Convo}, + conversation::{Conversation, Id, PrivateV1Convo}, errors::ChatError, inbox::Inbox, inbox_v2::InboxV2, @@ -17,13 +18,13 @@ use crate::{ use crypto::{Identity, PublicKey}; use storage::{ChatStore, ConversationKind}; -pub use crate::conversation::ConversationIdOwned; +pub use crate::conversation::{ConversationId, ConversationIdOwned}; pub use crate::inbox::Introduction; // 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, + identity: Rc, client_ctx: ClientCtx, inbox: Inbox, pq_inbox: InboxV2, @@ -66,7 +67,7 @@ impl Cont .map_err(ChatError::generic)?; Ok(Self { - _identity: identity, + identity: identity, client_ctx: ctx, inbox, pq_inbox, @@ -104,7 +105,7 @@ impl Cont .map_err(ChatError::generic)?; Ok(Self { - _identity: identity, + identity, client_ctx: ctx, pq_inbox, inbox, @@ -114,17 +115,29 @@ impl Cont }) } + pub fn store(&self) -> Ref<'_, CS> { + self.store.borrow() + } + + pub fn client_ctx(&mut self) -> &mut ClientCtx { + &mut self.client_ctx + } + + pub fn identity(&self) -> &Identity { + &self.identity + } + /// Returns the unique identifier associated with the account pub fn account_id(&self) -> String { self.pq_inbox.account.friendly_name() } pub fn installation_name(&self) -> &str { - self._identity.get_name() + self.identity.get_name() } pub fn installation_key(&self) -> PublicKey { - self._identity.public_key() + self.identity.public_key() } pub fn create_private_convo( @@ -253,6 +266,13 @@ impl Cont Ok(intro.into()) } + pub fn get_convo( + &mut self, + convo_id: ConversationId, + ) -> Result>, ChatError> { + self.load_group_convo(convo_id) + } + /// Loads a conversation from DB by constructing it from metadata. fn load_convo(&mut self, convo_id: ConversationId) -> Result, ChatError> { let record = self @@ -307,337 +327,3 @@ impl Cont } } } - -#[cfg(test)] -mod tests { - use std::ops::{Deref, DerefMut}; - - use sqlite::{ChatStorage, StorageConfig}; - use storage::{ConversationStore, IdentityStore}; - use tempfile::tempdir; - - use crate::{ - test_utils::{EphemeralRegistry, LocalBroadcaster, MemStore}, - utils::hex_trunc, - }; - - use super::*; - - type TestContext = Context; - - fn send_and_verify( - sender: &mut TestContext, - receiver: &mut TestContext, - convo_id: ConversationId, - content: &[u8], - ) { - let payloads = sender.send_content(convo_id, content).unwrap(); - let payload = payloads.first().unwrap(); - let received = receiver - .handle_payload(&payload.data) - .unwrap() - .expect("expected content"); - assert_eq!(content, received.data.as_slice()); - assert!(!received.is_new_convo); // Check that `is_new_convo` is FALSE - } - - // Simple client Functionality for testing - struct Client { - inner: Context, - on_content: Option>, - } - - impl Client { - fn init( - ctx: Context, - cb: Option, - ) -> Self { - Client { - inner: ctx, - on_content: cb.map(|f| Box::new(f) as Box), - } - } - - fn process_messages(&mut self) { - while let Some(data) = self.client_ctx.ds().poll() { - let res = self.handle_payload(&data).unwrap(); - if let Some(cb) = &self.on_content { - match res { - Some(content_data) => cb(content_data), - None => continue, - } - } - } - } - - fn convo( - &mut self, - convo_id: &str, - ) -> Box> { - // TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync - self.load_group_convo(convo_id).unwrap() - } - } - - impl Deref for Client { - type Target = Context; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl DerefMut for Client { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - - // Higher order function to handle printing - fn pretty_print(prefix: impl Into) -> Box { - let prefix = prefix.into(); - return Box::new(move |c: ContentData| { - let cid = hex_trunc(c.conversation_id.as_bytes()); - let content = String::from_utf8(c.data).unwrap(); - println!("{} ({}) {}", prefix, cid, content) - }); - } - - fn process(clients: &mut Vec) { - for client in clients { - client.process_messages(); - } - } - - #[test] - fn create_group() { - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - - let saro_ctx = - Context::new_with_name("saro", ds.new_consumer(), rs.clone(), MemStore::new()).unwrap(); - let raya_ctx = - Context::new_with_name("raya", ds.clone(), rs.clone(), MemStore::new()).unwrap(); - - let mut clients = vec![ - Client::init(saro_ctx, Some(pretty_print(" Saro "))), - Client::init(raya_ctx, Some(pretty_print(" Raya "))), - ]; - - const SARO: usize = 0; - const RAYA: usize = 1; - - let raya_id = clients[RAYA].account_id(); - let s_convo = clients[SARO] - .create_group_convo(&[raya_id.as_ref()]) - .unwrap(); - - let convo_id = s_convo.id(); - - // Raya can read this message because - // 1) It was sent after add_members was committed, and - // 2) LocalBroadcaster provides historical messages. - - clients[SARO] - .convo(convo_id) - .send_content( - &mut clients[SARO].client_ctx, - b"ok who broke the group chat again", - ) - .unwrap(); - - // clients[SARO].process_messages(); - process(&mut clients); - - clients[RAYA] - .convo(convo_id) - .send_content( - &mut clients[RAYA].client_ctx, - b"it was literally working five minutes ago", - ) - .unwrap(); - - // clients[SARO].process_messages(); - process(&mut clients); - - let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); - clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax")))); - const PAX: usize = 2; - - let pax_id = clients[PAX].account_id(); - clients[SARO] - .convo(convo_id) - .add_member(&mut clients[SARO].client_ctx, &[pax_id.as_ref()]) - .unwrap(); - - // clients[SARO].process_messages(); - process(&mut clients); - - clients[PAX] - .convo(convo_id) - .send_content( - &mut clients[PAX].client_ctx, - b"ngl the key rotation is cooked", - ) - .unwrap(); - - // clients[SARO].process_messages(); - - process(&mut clients); - - clients[SARO] - .convo(convo_id) - .send_content( - &mut clients[SARO].client_ctx, - b"bro we literally just added you to the group ", - ) - .unwrap(); - - process(&mut clients); - // process(&mut clients); - } - - #[test] - fn ctx_integration() { - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - - let mut saro = - Context::new_with_name("saro", ds.clone(), rs.clone(), ChatStorage::in_memory()) - .unwrap(); - let mut raya = Context::new_with_name("raya", ds, rs, ChatStorage::in_memory()).unwrap(); - - // Raya creates intro bundle and sends to Saro - let bundle = raya.create_intro_bundle().unwrap(); - let intro = Introduction::try_from(bundle.as_slice()).unwrap(); - - // Saro initiates conversation with Raya - let mut content = vec![10]; - let (saro_convo_id, payloads) = saro.create_private_convo(&intro, &content).unwrap(); - - // Raya receives initial message - let payload = payloads.first().unwrap(); - let initial_content = raya - .handle_payload(&payload.data) - .unwrap() - .expect("expected initial content"); - - let raya_convo_id = initial_content.conversation_id; - assert_eq!(content, initial_content.data); - assert!(initial_content.is_new_convo); - - // Exchange messages back and forth - for _ in 0..10 { - content.push(content.last().unwrap() + 1); - send_and_verify(&mut raya, &mut saro, &raya_convo_id, &content); - - content.push(content.last().unwrap() + 1); - send_and_verify(&mut saro, &mut raya, &saro_convo_id, &content); - } - } - - #[test] - fn identity_persistence() { - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap(); - let ctx1 = Context::new_with_name("alice", ds, rs, store1).unwrap(); - let pubkey1 = ctx1._identity.public_key(); - let name1 = ctx1.installation_name().to_string(); - - // For persistence tests with file-based storage, we'd need a shared db. - // With in-memory, we just verify the identity was created. - assert_eq!(name1, "alice"); - assert!(!pubkey1.as_bytes().iter().all(|&b| b == 0)); - } - - #[test] - fn open_persists_new_identity() { - let dir = tempdir().unwrap(); - let db_path = dir.path().join("chat.sqlite"); - let db_path = db_path.to_string_lossy().into_owned(); - - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap(); - let ctx = Context::new_from_store("alice", ds, rs, store).unwrap(); - let pubkey = ctx._identity.public_key(); - drop(ctx); - - let store = ChatStorage::new(StorageConfig::File(db_path)).unwrap(); - let persisted = store.load_identity().unwrap().unwrap(); - - assert_eq!(persisted.get_name(), "alice"); - assert_eq!(persisted.public_key(), pubkey); - } - - #[test] - fn conversation_metadata_persistence() { - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - let mut alice = - Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()) - .unwrap(); - let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); - - let bundle = alice.create_intro_bundle().unwrap(); - let intro = Introduction::try_from(bundle.as_slice()).unwrap(); - let (_, payloads) = bob.create_private_convo(&intro, b"hi").unwrap(); - - let payload = payloads.first().unwrap(); - let content = alice.handle_payload(&payload.data).unwrap().unwrap(); - assert!(content.is_new_convo); - - let convos = alice.store.borrow().load_conversations().unwrap(); - assert_eq!(convos.len(), 1); - assert_eq!(convos[0].kind.as_str(), "private_v1"); - } - - #[test] - fn conversation_full_flow() { - let ds = LocalBroadcaster::new(); - let rs = EphemeralRegistry::new(); - let mut alice = - Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()) - .unwrap(); - let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); - - let bundle = alice.create_intro_bundle().unwrap(); - let intro = Introduction::try_from(bundle.as_slice()).unwrap(); - let (bob_convo_id, payloads) = bob.create_private_convo(&intro, b"hello").unwrap(); - - let payload = payloads.first().unwrap(); - let content = alice.handle_payload(&payload.data).unwrap().unwrap(); - let alice_convo_id = content.conversation_id; - - let payloads = alice.send_content(&alice_convo_id, b"reply 1").unwrap(); - let payload = payloads.first().unwrap(); - bob.handle_payload(&payload.data).unwrap().unwrap(); - - let payloads = bob.send_content(&bob_convo_id, b"reply 2").unwrap(); - let payload = payloads.first().unwrap(); - alice.handle_payload(&payload.data).unwrap().unwrap(); - - // Verify conversation list - let convo_ids = alice.list_conversations().unwrap(); - assert_eq!(convo_ids.len(), 1); - - // Continue exchanging messages - let payloads = bob.send_content(&bob_convo_id, b"more messages").unwrap(); - let payload = payloads.first().unwrap(); - let content = alice - .handle_payload(&payload.data) - .expect("should decrypt") - .expect("should have content"); - assert_eq!(content.data, b"more messages"); - - // Alice can also send back - let payloads = alice.send_content(&alice_convo_id, b"alice reply").unwrap(); - let payload = payloads.first().unwrap(); - let content = bob - .handle_payload(&payload.data) - .unwrap() - .expect("bob should receive"); - assert_eq!(content.data, b"alice reply"); - } -} diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index b7736d8..cb83396 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -13,8 +13,7 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc, sync::Arc}; use storage::{ConversationKind, ConversationMeta, ConversationStore}; use crate::{ - context::ConversationIdOwned, - conversation::{ChatError, ConversationId, Convo, Id}, + conversation::{ChatError, ConversationId, ConversationIdOwned, Convo, Id}, errors::EncryptionError, proto, types::{AddressedEncryptedPayload, ContentData}, diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index a979083..63417a4 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -15,7 +15,8 @@ pub use account::LogosAccount; #[cfg(test)] mod test_utils; -pub use context::{Context, ConversationIdOwned, Introduction}; +pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; +pub use conversation::GroupConvo; pub use errors::ChatError; pub use external_traits::{DeliveryService, RegistrationService}; pub use sqlite::ChatStorage; diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml new file mode 100644 index 0000000..34ff420 --- /dev/null +++ b/core/integration_tests_core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "integration_tests_core" +version = "0.1.0" +edition = "2024" + +# [[test]] +# name = "integration_tests_core" + +[dev-dependencies] +libchat = { workspace = true } +storage = { workspace = true } +sqlite = {package = "chat-sqlite", path ="../sqlite"} + +components = { path = "../../extensions/components" } +tempfile = "3" diff --git a/core/integration_tests_core/src/lib.rs b/core/integration_tests_core/src/lib.rs new file mode 100644 index 0000000..7d18d35 --- /dev/null +++ b/core/integration_tests_core/src/lib.rs @@ -0,0 +1,23 @@ +// use std::ops::{Deref, DerefMut}; + +// use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; +// use libchat::{ +// AddressedEnvelope, ChatStorage, ContentData, Context, ConversationIdOwned, Introduction, +// StorageConfig, +// }; + +// fn send_and_verify( +// sender: &mut Context, +// receiver: &mut Context, +// convo_id: &str, +// content: &[u8], +// ) { +// let payloads = sender.send_content(convo_id, content).unwrap(); +// let payload = payloads.first().unwrap(); +// let received = receiver +// .handle_payload(&payload.data) +// .unwrap() +// .expect("expected content"); +// assert_eq!(content, received.data.as_slice()); +// assert!(!received.is_new_convo); +// } diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs new file mode 100644 index 0000000..ff81b30 --- /dev/null +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -0,0 +1,177 @@ +use std::ops::{Deref, DerefMut}; + +use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; +use libchat::{ChatStorage, ContentData, Context, ConversationId, GroupConvo}; + +type TestContext = Context; + +fn send_and_verify( + sender: &mut TestContext, + receiver: &mut TestContext, + convo_id: ConversationId, + content: &[u8], +) { + let payloads = sender.send_content(convo_id, content).unwrap(); + let payload = payloads.first().unwrap(); + let received = receiver + .handle_payload(&payload.data) + .unwrap() + .expect("expected content"); + assert_eq!(content, received.data.as_slice()); + assert!(!received.is_new_convo); // Check that `is_new_convo` is FALSE +} + +// Simple client Functionality for testing +struct Client { + inner: Context, + on_content: Option>, +} + +impl Client { + fn init( + ctx: Context, + cb: Option, + ) -> Self { + Client { + inner: ctx, + on_content: cb.map(|f| Box::new(f) as Box), + } + } + + fn process_messages(&mut self) { + while let Some(data) = self.client_ctx().ds().poll() { + let res = self.handle_payload(&data).unwrap(); + if let Some(cb) = &self.on_content { + match res { + Some(content_data) => cb(content_data), + None => continue, + } + } + } + } + + fn convo( + &mut self, + convo_id: &str, + ) -> Box> { + // TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync + self.get_convo(convo_id).unwrap() + } +} + +impl Deref for Client { + type Target = Context; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +// Higher order function to handle printing +fn pretty_print(prefix: impl Into) -> Box { + let prefix = prefix.into(); + return Box::new(move |c: ContentData| { + let cid = c.conversation_id.as_bytes(); + let content = String::from_utf8(c.data).unwrap(); + println!("{} ({:?}) {}", prefix, cid, content) + }); +} + +fn process(clients: &mut Vec) { + for client in clients { + client.process_messages(); + } +} + +#[test] +fn create_group() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + + let saro_ctx = + Context::new_with_name("saro", ds.new_consumer(), rs.clone(), MemStore::new()).unwrap(); + let raya_ctx = Context::new_with_name("raya", ds.clone(), rs.clone(), MemStore::new()).unwrap(); + + let mut clients = vec![ + Client::init(saro_ctx, Some(pretty_print(" Saro "))), + Client::init(raya_ctx, Some(pretty_print(" Raya "))), + ]; + + const SARO: usize = 0; + const RAYA: usize = 1; + + let raya_id = clients[RAYA].account_id(); + let s_convo = clients[SARO] + .create_group_convo(&[raya_id.as_ref()]) + .unwrap(); + + let convo_id = s_convo.id(); + + // Raya can read this message because + // 1) It was sent after add_members was committed, and + // 2) LocalBroadcaster provides historical messages. + + clients[SARO] + .convo(convo_id) + .send_content( + &mut clients[SARO].client_ctx(), + b"ok who broke the group chat again", + ) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + clients[RAYA] + .convo(convo_id) + .send_content( + &mut clients[RAYA].client_ctx(), + b"it was literally working five minutes ago", + ) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); + clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax")))); + const PAX: usize = 2; + + let pax_id = clients[PAX].account_id(); + clients[SARO] + .convo(convo_id) + .add_member(&mut clients[SARO].client_ctx(), &[pax_id.as_ref()]) + .unwrap(); + + // clients[SARO].process_messages(); + process(&mut clients); + + clients[PAX] + .convo(convo_id) + .send_content( + &mut clients[PAX].client_ctx(), + b"ngl the key rotation is cooked", + ) + .unwrap(); + + // clients[SARO].process_messages(); + + process(&mut clients); + + clients[SARO] + .convo(convo_id) + .send_content( + &mut clients[SARO].client_ctx(), + b"bro we literally just added you to the group ", + ) + .unwrap(); + + process(&mut clients); + // process(&mut clients); +} diff --git a/core/integration_tests_core/tests/private_integration.rs b/core/integration_tests_core/tests/private_integration.rs new file mode 100644 index 0000000..b9eec0b --- /dev/null +++ b/core/integration_tests_core/tests/private_integration.rs @@ -0,0 +1,165 @@ +use std::ops::{Deref, DerefMut}; + +use libchat::{AddressedEnvelope, Context, Introduction}; +use sqlite::{ChatStorage, StorageConfig}; +use storage::{ConversationStore, IdentityStore}; +use tempfile::tempdir; + +use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; + +fn send_and_verify( + sender: &mut Context, + receiver: &mut Context, + convo_id: &str, + content: &[u8], +) { + let payloads = sender.send_content(convo_id, content).unwrap(); + let payload = payloads.first().unwrap(); + let received = receiver + .handle_payload(&payload.data) + .unwrap() + .expect("expected content"); + assert_eq!(content, received.data.as_slice()); + assert!(!received.is_new_convo); +} + +#[test] +fn ctx_integration() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + + let mut saro = + Context::new_with_name("saro", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); + let mut raya = Context::new_with_name("raya", ds, rs, ChatStorage::in_memory()).unwrap(); + + // Raya creates intro bundle and sends to Saro + let bundle = raya.create_intro_bundle().unwrap(); + let intro = Introduction::try_from(bundle.as_slice()).unwrap(); + + // Saro initiates conversation with Raya + let mut content = vec![10]; + let (saro_convo_id, payloads) = saro.create_private_convo(&intro, &content).unwrap(); + + // Raya receives initial message + let payload = payloads.first().unwrap(); + let initial_content = raya + .handle_payload(&payload.data) + .unwrap() + .expect("expected initial content"); + + let raya_convo_id = initial_content.conversation_id; + assert_eq!(content, initial_content.data); + assert!(initial_content.is_new_convo); + + // Exchange messages back and forth + for _ in 0..10 { + content.push(content.last().unwrap() + 1); + send_and_verify(&mut raya, &mut saro, &raya_convo_id, &content); + + content.push(content.last().unwrap() + 1); + send_and_verify(&mut saro, &mut raya, &saro_convo_id, &content); + } +} + +#[test] +fn identity_persistence() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap(); + let ctx1 = Context::new_with_name("alice", ds, rs, store1).unwrap(); + let pubkey1 = ctx1.identity().public_key(); + let name1 = ctx1.installation_name().to_string(); + + // For persistence tests with file-based storage, we'd need a shared db. + // With in-memory, we just verify the identity was created. + assert_eq!(name1, "alice"); + assert!(!pubkey1.as_bytes().iter().all(|&b| b == 0)); +} + +#[test] +fn open_persists_new_identity() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("chat.sqlite"); + let db_path = db_path.to_string_lossy().into_owned(); + + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap(); + let ctx = Context::new_from_store("alice", ds, rs, store).unwrap(); + let pubkey = ctx.identity().public_key(); + drop(ctx); + + let store = ChatStorage::new(StorageConfig::File(db_path)).unwrap(); + let persisted = store.load_identity().unwrap().unwrap(); + + assert_eq!(persisted.get_name(), "alice"); + assert_eq!(persisted.public_key(), pubkey); +} + +#[test] +fn conversation_metadata_persistence() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let mut alice = + Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); + let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); + + let bundle = alice.create_intro_bundle().unwrap(); + let intro = Introduction::try_from(bundle.as_slice()).unwrap(); + let (_, payloads) = bob.create_private_convo(&intro, b"hi").unwrap(); + + let payload = payloads.first().unwrap(); + let content = alice.handle_payload(&payload.data).unwrap().unwrap(); + assert!(content.is_new_convo); + + let convos = alice.store().load_conversations().unwrap(); + assert_eq!(convos.len(), 1); + assert_eq!(convos[0].kind.as_str(), "private_v1"); +} + +#[test] +fn conversation_full_flow() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + let mut alice = + Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); + let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); + + let bundle = alice.create_intro_bundle().unwrap(); + let intro = Introduction::try_from(bundle.as_slice()).unwrap(); + let (bob_convo_id, payloads) = bob.create_private_convo(&intro, b"hello").unwrap(); + + let payload = payloads.first().unwrap(); + let content = alice.handle_payload(&payload.data).unwrap().unwrap(); + let alice_convo_id = content.conversation_id; + + let payloads = alice.send_content(&alice_convo_id, b"reply 1").unwrap(); + let payload = payloads.first().unwrap(); + bob.handle_payload(&payload.data).unwrap().unwrap(); + + let payloads = bob.send_content(&bob_convo_id, b"reply 2").unwrap(); + let payload = payloads.first().unwrap(); + alice.handle_payload(&payload.data).unwrap().unwrap(); + + // Verify conversation list + let convo_ids = alice.list_conversations().unwrap(); + assert_eq!(convo_ids.len(), 1); + + // Continue exchanging messages + let payloads = bob.send_content(&bob_convo_id, b"more messages").unwrap(); + let payload = payloads.first().unwrap(); + let content = alice + .handle_payload(&payload.data) + .expect("should decrypt") + .expect("should have content"); + assert_eq!(content.data, b"more messages"); + + // Alice can also send back + let payloads = alice.send_content(&alice_convo_id, b"alice reply").unwrap(); + let payload = payloads.first().unwrap(); + let content = bob + .handle_payload(&payload.data) + .unwrap() + .expect("bob should receive"); + assert_eq!(content.data, b"alice reply"); +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 50e6099..a51b397 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,16 +1,16 @@ use libchat::{ - AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, + AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, RegistrationService DeliveryService, Introduction, StorageConfig, }; use crate::errors::ClientError; pub struct ChatClient { - ctx: Context, + ctx: Context, delivery: D, } -impl ChatClient { +impl ChatClient { /// Create an in-memory, ephemeral client. Identity is lost on drop. pub fn new(name: impl Into, delivery: D) -> Self { let store = ChatStorage::in_memory(); diff --git a/extensions/components/Cargo.toml b/extensions/components/Cargo.toml new file mode 100644 index 0000000..0be31bc --- /dev/null +++ b/extensions/components/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "components" +version = "0.1.0" +edition = "2024" + +[dependencies] +libchat = { workspace = true } +storage = { workspace = true } +crypto = { workspace = true } # Needed because Storage traits require "Identity" struct + +hex = "0.4.3" \ No newline at end of file diff --git a/extensions/components/src/contact_registry.rs b/extensions/components/src/contact_registry.rs new file mode 100644 index 0000000..02f2f26 --- /dev/null +++ b/extensions/components/src/contact_registry.rs @@ -0,0 +1,62 @@ +use std::{ + collections::HashMap, + fmt::Debug, + sync::{Arc, Mutex}, +}; + +use libchat::RegistrationService; + +/// A Contact Registry used for Tests. +/// This implementation stores bundle bytes and then returns them when +/// retreived +/// + +#[derive(Clone)] +pub struct EphemeralRegistry { + registry: Arc>>>, +} + +impl EphemeralRegistry { + pub fn new() -> Self { + Self { + registry: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +impl Debug for EphemeralRegistry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let registry = self.registry.lock().unwrap(); + let truncated: Vec<(&String, String)> = registry + .iter() + .map(|(k, v)| { + let hex = if v.len() <= 8 { + hex::encode(v) + } else { + format!( + "{}..{}", + hex::encode(&v[..4]), + hex::encode(&v[v.len() - 4..]) + ) + }; + (k, hex) + }) + .collect(); + f.debug_struct("EphemeralRegistry") + .field("registry", &truncated) + .finish() + } +} + +impl RegistrationService for EphemeralRegistry { + type Error = String; + + fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error> { + self.registry.lock().unwrap().insert(identity, key_bundle); + Ok(()) + } + + fn retreive(&self, identity: &str) -> Result>, Self::Error> { + Ok(self.registry.lock().unwrap().get(identity).cloned()) + } +} diff --git a/extensions/components/src/delivery.rs b/extensions/components/src/delivery.rs new file mode 100644 index 0000000..627ac55 --- /dev/null +++ b/extensions/components/src/delivery.rs @@ -0,0 +1,3 @@ +mod local_broadcaster; + +pub use local_broadcaster::LocalBroadcaster; diff --git a/extensions/components/src/delivery/local_broadcaster.rs b/extensions/components/src/delivery/local_broadcaster.rs new file mode 100644 index 0000000..9828b26 --- /dev/null +++ b/extensions/components/src/delivery/local_broadcaster.rs @@ -0,0 +1,116 @@ +use std::{ + cell::RefCell, + collections::{HashSet, VecDeque}, + hash::{DefaultHasher, Hash, Hasher}, + rc::Rc, +}; + +use libchat::{AddressedEnvelope, DeliveryService}; + +#[derive(Debug)] +struct BroadcasterShared { + /// Per-address message queue; all published messages are appended here. + messages: VecDeque, + base_index: usize, +} + +impl BroadcasterShared { + pub fn read(&self, cursor: usize) -> Option<&T> { + self.messages.get(cursor + self.base_index) + } + + pub fn tail(&self) -> usize { + self.messages.len() + self.base_index + } +} + +#[derive(Clone, Debug)] +pub struct LocalBroadcaster { + shared: Rc>>, + cursor: usize, + subscriptions: HashSet, + outbound_msgs: Vec, +} + +/// This is Lightweight DeliveryService which can be used for tests +/// and local examples. Messages are not delivered until `poll` is called +/// which allows for more fine grain test cases. +impl LocalBroadcaster { + pub fn new() -> Self { + let shared = Rc::new(RefCell::new(BroadcasterShared { + messages: VecDeque::new(), + base_index: 0, + })); + + let cursor = shared.borrow().tail(); + Self { + shared, + cursor, + subscriptions: HashSet::new(), + outbound_msgs: Vec::new(), + } + } + + /// Returns a new consumer that shares the same message store but has its + /// own independent cursor — it starts from the beginning of each address + /// queue regardless of what any other consumer has already processed. + pub fn new_consumer(&self) -> Self { + let inner = self.shared.clone(); + let cursor = inner.borrow().tail(); + Self { + shared: inner, + cursor, + subscriptions: HashSet::new(), + outbound_msgs: Vec::new(), + } + } + + /// Pulls all messages this consumer has not yet seen on `address`, + /// applying any registered filter. Advances the cursor so the same + /// messages are not returned again. + pub fn poll(&mut self) -> Option> { + loop { + let next = self.cursor; + match self.shared.borrow().read(next) { + None => return None, + Some(ae) => { + self.cursor = next + 1; + if self.subscriptions.contains(ae.delivery_address.as_str()) + && self.is_inbound(ae) + { + return Some(ae.data.clone()); + } + } + } + } + } + + fn msg_id(msg: &AddressedEnvelope) -> u64 { + let mut hasher = DefaultHasher::new(); + msg.data.as_slice().hash(&mut hasher); + hasher.finish() + } + + fn is_inbound(&self, msg: &AddressedEnvelope) -> bool { + let mid = Self::msg_id(msg); + !self.outbound_msgs.contains(&mid) + } +} + +impl DeliveryService for LocalBroadcaster { + type Error = String; + + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> { + self.outbound_msgs.push(Self::msg_id(&envelope)); + self.shared.borrow_mut().messages.push_back(envelope); + + Ok(()) + } + + fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error> { + // Strict temporal ordering of subscriptions is not enforced. + // Subscriptions are evaluated on polling, not when the message is published + self.subscriptions.insert(delivery_address); + Ok(()) + } +} diff --git a/extensions/components/src/lib.rs b/extensions/components/src/lib.rs new file mode 100644 index 0000000..d55c0f7 --- /dev/null +++ b/extensions/components/src/lib.rs @@ -0,0 +1,7 @@ +mod contact_registry; +mod delivery; +mod storage; + +pub use contact_registry::EphemeralRegistry; +pub use delivery::*; +pub use storage::*; diff --git a/extensions/components/src/storage.rs b/extensions/components/src/storage.rs new file mode 100644 index 0000000..36bbcbe --- /dev/null +++ b/extensions/components/src/storage.rs @@ -0,0 +1,3 @@ +mod in_memory_store; + +pub use in_memory_store::MemStore; diff --git a/extensions/components/src/storage/in_memory_store.rs b/extensions/components/src/storage/in_memory_store.rs new file mode 100644 index 0000000..fcbc8b1 --- /dev/null +++ b/extensions/components/src/storage/in_memory_store.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use storage::{ + // TODO: (P4) Importable crates need to be prefixed with a project name to avoid conflicts + ConversationMeta, + ConversationStore, + EphemeralKeyStore, + IdentityStore, + RatchetStore, +}; + +/// An Test focused StorageService which holds data in a hashmap +pub struct MemStore { + convos: HashMap, +} + +impl MemStore { + pub fn new() -> Self { + Self { + convos: HashMap::new(), + } + } +} + +impl ConversationStore for MemStore { + fn save_conversation( + &mut self, + meta: &storage::ConversationMeta, + ) -> Result<(), storage::StorageError> { + self.convos + .insert(meta.local_convo_id.clone(), meta.clone()); + Ok(()) + } + + fn load_conversation( + &self, + local_convo_id: &str, + ) -> Result, storage::StorageError> { + let a = self.convos.get(local_convo_id).cloned(); + Ok(a) + } + + fn remove_conversation(&mut self, _local_convo_id: &str) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_conversations(&self) -> Result, storage::StorageError> { + Ok(self.convos.values().cloned().collect()) + } + + fn has_conversation(&self, local_convo_id: &str) -> Result { + Ok(self.convos.contains_key(local_convo_id)) + } +} + +impl IdentityStore for MemStore { + fn load_identity(&self) -> Result, storage::StorageError> { + // todo!() + Ok(None) + } + + fn save_identity(&mut self, _identity: &crypto::Identity) -> Result<(), storage::StorageError> { + // todo!() + Ok(()) + } +} + +impl EphemeralKeyStore for MemStore { + fn save_ephemeral_key( + &mut self, + _public_key_hex: &str, + _private_key: &crypto::PrivateKey, + ) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_ephemeral_key( + &self, + _public_key_hex: &str, + ) -> Result, storage::StorageError> { + todo!() + } + + fn remove_ephemeral_key(&mut self, _public_key_hex: &str) -> Result<(), storage::StorageError> { + todo!() + } +} + +impl RatchetStore for MemStore { + fn save_ratchet_state( + &mut self, + _conversation_id: &str, + _state: &storage::RatchetStateRecord, + _skipped_keys: &[storage::SkippedKeyRecord], + ) -> Result<(), storage::StorageError> { + todo!() + } + + fn load_ratchet_state( + &self, + _conversation_id: &str, + ) -> Result { + todo!() + } + + fn load_skipped_keys( + &self, + _conversation_id: &str, + ) -> Result, storage::StorageError> { + todo!() + } + + fn has_ratchet_state(&self, _conversation_id: &str) -> Result { + todo!() + } + + fn delete_ratchet_state( + &mut self, + _conversation_id: &str, + ) -> Result<(), storage::StorageError> { + todo!() + } + + fn cleanup_old_skipped_keys( + &mut self, + _max_age_secs: i64, + ) -> Result { + todo!() + } +} diff --git a/extensions/delivery/Cargo.toml b/extensions/delivery/Cargo.toml deleted file mode 100644 index f55ccf8..0000000 --- a/extensions/delivery/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "delivery" -version = "0.1.0" -edition = "2024" - -[dependencies] diff --git a/extensions/delivery/src/lib.rs b/extensions/delivery/src/lib.rs deleted file mode 100644 index 2b14bd0..0000000 --- a/extensions/delivery/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod local_bcast; - -use local_bcast::LocalBroadcast; - -pub use LocalBroadcast; diff --git a/extensions/delivery/src/local_bcast.rs b/extensions/delivery/src/local_bcast.rs deleted file mode 100644 index 20b1335..0000000 --- a/extensions/delivery/src/local_bcast.rs +++ /dev/null @@ -1,58 +0,0 @@ -use libchat::DeliveryService; - -type Callback = Box)>; - -#[derive(Clone)] -struct LocalBroadcaster { - subscribers: Arc>>>, -} - -impl LocalBroadcaster { - pub fn new() -> Self { - Self { - subscribers: Arc::new(Mutex::new(HashMap::new())), - } - } -} - -impl DeliveryService for LocalBroadcaster { - type Error = String; - - fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> { - let callbacks = self - .subscribers - .lock() - .unwrap() - .remove(&envelope.delivery_address) - .unwrap_or_default(); - - for cb in callbacks { - cb(envelope.delivery_address.clone(), &envelope.data); - } - - Ok(()) - } - - fn subscribe(&mut self, delivery_address: String, cb: F) -> Result<(), Self::Error> - where - F: FnOnce(String, &Vec) + 'static, - { - self.subscribers - .lock() - .unwrap() - .entry(delivery_address) - .or_default() - .push(Box::new(cb)); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - #[test] - fn local_bcast() { - let ds = LocalBroadcast::new(); - } -} From d988ecad55660387e11ead3d91d96e40e12c405c Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:37:23 -0700 Subject: [PATCH 05/39] Remove deadcode --- core/sqlite/src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/sqlite/src/lib.rs b/core/sqlite/src/lib.rs index bb7ea3c..8c57bb3 100644 --- a/core/sqlite/src/lib.rs +++ b/core/sqlite/src/lib.rs @@ -532,16 +532,6 @@ fn blob_to_array( .map_err(|_| invalid_blob_length(field, N, actual)) } -// impl GroupMlsStorageV1 for ChatStorage { -// fn save_state(&self, convo_id: &str, state: &[u8]) { -// todo!() -// } - -// fn load_state(&self, convo_id: &str) -> Vec { -// todo!() -// } -// } - #[cfg(test)] mod tests { use storage::{ From 246ab8dcf999c8cf576a675a59207106da1725b2 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:51:14 -0700 Subject: [PATCH 06/39] undo import fixes --- core/conversations/src/conversation/privatev1.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index cb83396..b7736d8 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -13,7 +13,8 @@ use std::{cell::RefCell, fmt::Debug, rc::Rc, sync::Arc}; use storage::{ConversationKind, ConversationMeta, ConversationStore}; use crate::{ - conversation::{ChatError, ConversationId, ConversationIdOwned, Convo, Id}, + context::ConversationIdOwned, + conversation::{ChatError, ConversationId, Convo, Id}, errors::EncryptionError, proto, types::{AddressedEncryptedPayload, ContentData}, From def297f132fada31ee06b956d265c1e9078c4b9e Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:28:47 -0700 Subject: [PATCH 07/39] tidy --- core/conversations/src/conversation/group_v1.rs | 7 ++----- core/conversations/src/inbox_v2.rs | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 85c37e4..fa0662b 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -2,12 +2,13 @@ use std::cell::{Ref, RefCell}; use std::rc::Rc; use blake2::{Blake2b, Digest, digest::consts::U6}; +use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; use crypto::Ed25519VerifyingKey; use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::*; use openmls_libcrux_crypto::Provider as LibcruxProvider; - use openmls_traits::signatures::Signer as OpenMlsSigner; +use storage::{ChatStore, ConversationKind}; use crate::{ DeliveryService, RegistrationService, @@ -15,13 +16,10 @@ use crate::{ ctx::ClientCtx, types::{AddressedEncryptedPayload, ContentData}, }; -use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; -use storage::{ChatStore, ConversationKind}; pub trait IdentityProvider: OpenMlsSigner { fn friendly_name(&self) -> String; fn public_key(&self) -> Ed25519VerifyingKey; - // fn installation_key() -> u8; } pub trait MlsInitializer { @@ -30,7 +28,6 @@ pub trait MlsInitializer { ctx: &mut ClientCtx, account_id: &str, welcome: &MlsMessageOut, - // ratchet_tree: RatchetTree, // Embedded ) -> Result<(), ChatError>; } diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 2f492bc..8bcbf5f 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -24,9 +24,7 @@ use crate::ctx::ClientCtx; use crate::utils::{blake2b_hex, hash_size}; static ACCOUNT_COUNTER: AtomicUsize = AtomicUsize::new(0); - const ACCOUNT_NAMES: &[&str] = &["Saro", "Raya", "Pax"]; - #[derive(Clone)] pub struct LogosAccount { id: String, From 3bf8ecb904f17205f1b701b672852e2f3a1b5c01 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:21:15 -0700 Subject: [PATCH 08/39] Update Accounts + service_traits --- core/conversations/src/account.rs | 17 +- core/conversations/src/context.rs | 17 +- core/conversations/src/conversation.rs | 8 +- .../src/conversation/group_v1.rs | 85 +++--- core/conversations/src/ctx.rs | 15 +- core/conversations/src/external_traits.rs | 15 -- core/conversations/src/inbox_v2.rs | 242 ++++++------------ core/conversations/src/lib.rs | 7 +- core/conversations/src/service_traits.rs | 38 +++ core/conversations/src/test_utils.rs | 22 +- core/conversations/src/types.rs | 2 +- .../tests/mls_integration.rs | 14 +- extensions/components/src/contact_registry.rs | 17 +- .../src/delivery/local_broadcaster.rs | 4 +- 14 files changed, 240 insertions(+), 263 deletions(-) delete mode 100644 core/conversations/src/external_traits.rs create mode 100644 core/conversations/src/service_traits.rs diff --git a/core/conversations/src/account.rs b/core/conversations/src/account.rs index 06e2914..3ca7dfc 100644 --- a/core/conversations/src/account.rs +++ b/core/conversations/src/account.rs @@ -1,14 +1,15 @@ -use crypto::Ed25519SigningKey; +use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; use openmls::prelude::SignatureScheme; use openmls_traits::signatures::Signer; -use crate::types::AccountId; +use crate::{conversation::IdentityProvider, types::AccountId}; /// Logos Account represents a single account across /// multiple installations and services. pub struct LogosAccount { id: AccountId, signing_key: Ed25519SigningKey, + verifying_key: Ed25519VerifyingKey, } impl LogosAccount { @@ -17,9 +18,11 @@ impl LogosAccount { /// TODO: (P1) Remove once implementation is ready. pub fn new_test(explicit_id: impl Into) -> Self { let signing_key = Ed25519SigningKey::generate(); + let verifying_key = signing_key.verifying_key() Self { id: AccountId::new(explicit_id.into()), signing_key, + verifying_key } } @@ -38,3 +41,13 @@ impl Signer for LogosAccount { SignatureScheme::ED25519 } } + +impl IdentityProvider for LogosAccount { + fn friendly_name(&self) -> String { + self.id.to_string() + } + + fn public_key(&self) -> &Ed25519VerifyingKey { + &self.verifying_key + } +} diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index ae968e6..e3d6906 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -2,6 +2,7 @@ use std::cell::Ref; use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; +use crate::account::LogosAccount; use crate::conversation::{Convo, GroupConvo, IdentityProvider}; use crate::ctx::ClientCtx; @@ -13,7 +14,7 @@ use crate::{ inbox::Inbox, inbox_v2::InboxV2, proto::{EncryptedPayload, EnvelopeV1, Message}, - types::{AddressedEnvelope, ContentData}, + types::{AccountId, AddressedEnvelope, ContentData}, }; use crypto::{Identity, PublicKey}; use storage::{ChatStore, ConversationKind}; @@ -59,11 +60,11 @@ impl Cont let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&store), Rc::clone(&identity)); - let pq_inbox = InboxV2::new(); + let pq_inbox = InboxV2::new_with_account(LogosAccount::new_test(name)); // Subscribe ctx.ds() - .subscribe(pq_inbox.delivery_address()) + .subscribe(&pq_inbox.delivery_address()) .map_err(ChatError::generic)?; Ok(Self { @@ -97,11 +98,11 @@ impl Cont let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&chat_store), Rc::clone(&identity)); - let mut pq_inbox = InboxV2::new(); + let mut pq_inbox = InboxV2::new_with_account(LogosAccount::new_test(name)); pq_inbox.register(&mut ctx)?; ctx.ds() - .subscribe(pq_inbox.delivery_address()) + .subscribe(&pq_inbox.delivery_address()) .map_err(ChatError::generic)?; Ok(Self { @@ -128,8 +129,8 @@ impl Cont } /// Returns the unique identifier associated with the account - pub fn account_id(&self) -> String { - self.pq_inbox.account.friendly_name() + pub fn account_id(&self) -> &AccountId { + self.pq_inbox.account_id() } pub fn installation_name(&self) -> &str { @@ -162,7 +163,7 @@ impl Cont pub fn create_group_convo( &mut self, - participants: &[&str], + participants: &[&AccountId], ) -> Result>, ChatError> { let mut convo = self.pq_inbox.create_group_v1(&mut self.client_ctx)?; self.client_ctx diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 6c6685f..4a36bc3 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -2,9 +2,9 @@ pub mod group_v1; mod privatev1; use crate::{ - DeliveryService, RegistrationService, + DeliveryService, service_traits::KeyPackageProvider, ctx::ClientCtx, - types::{AddressedEncryptedPayload, ContentData}, + types::{AccountId, AddressedEncryptedPayload, ContentData}, }; use chat_proto::logoschat::encryption::EncryptedPayload; use std::fmt::Debug; @@ -42,11 +42,11 @@ pub trait Convo: Id + Debug { fn convo_type(&self) -> ConversationKind; } -pub trait GroupConvo: Convo { +pub trait GroupConvo: Convo { fn add_member( &mut self, ctx: &mut ClientCtx, - members: &[&str], + members: &[&AccountId], ) -> Result<(), ChatError>; // Default implementation which dispatches envelopes to the DeliveryService diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index fa0662b..12a71da 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -1,4 +1,4 @@ -use std::cell::{Ref, RefCell}; +use std::cell::RefCell; use std::rc::Rc; use blake2::{Blake2b, Digest, digest::consts::U6}; @@ -10,8 +10,9 @@ use openmls_libcrux_crypto::Provider as LibcruxProvider; use openmls_traits::signatures::Signer as OpenMlsSigner; use storage::{ChatStore, ConversationKind}; +use crate::types::AccountId; use crate::{ - DeliveryService, RegistrationService, + DeliveryService, service_traits::KeyPackageProvider, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, ctx::ClientCtx, types::{AddressedEncryptedPayload, ContentData}, @@ -19,37 +20,38 @@ use crate::{ pub trait IdentityProvider: OpenMlsSigner { fn friendly_name(&self) -> String; - fn public_key(&self) -> Ed25519VerifyingKey; + fn public_key(&self) -> &Ed25519VerifyingKey; } -pub trait MlsInitializer { - fn invite_to_group_v1( +pub trait MlsContext { + type IDENT: IdentityProvider; + + fn ident(&self) -> &Self::IDENT; + fn provider(&self) -> &LibcruxProvider; + + // Build an MLS Credential from the supplied IdentityProvider + fn get_credential(&self) -> CredentialWithKey { + CredentialWithKey { + credential: BasicCredential::new(self.ident().friendly_name().into()).into(), + signature_key: self.ident().public_key().as_ref().into(), + } + } + + fn invite_user( &self, ctx: &mut ClientCtx, - account_id: &str, + account_id: &AccountId, welcome: &MlsMessageOut, ) -> Result<(), ChatError>; } -pub trait MlsCtx { - type IDENT: IdentityProvider; - type INIT: MlsInitializer; - - fn ident(&self) -> &Self::IDENT; - fn provider(&self) -> Ref<'_, LibcruxProvider>; - fn init(&self) -> &Self::INIT; - - // Build an MLS Credential from the supplied IdentityProvider - fn get_credential(&self) -> CredentialWithKey; -} - -pub struct GroupV1Convo { +pub struct GroupV1Convo { ctx: Rc>, pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility convo_id: String, } -impl std::fmt::Debug for GroupV1Convo { +impl std::fmt::Debug for GroupV1Convo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GroupV1Convo") .field("name", &self.ctx.borrow().ident().friendly_name()) @@ -59,14 +61,13 @@ impl std::fmt::Debug for GroupV1Convo { } } -impl GroupV1Convo { - pub fn new(ctx: Ctx, ds: &mut DS) -> Self { +impl GroupV1Convo { + pub fn new(ctx: Rc>, ds: &mut DS) -> Self { let config = Self::mls_create_config(); - let ctx = Rc::new(RefCell::new(ctx)); let mls_group = { let ctx_ref = ctx.borrow(); MlsGroup::new( - &*ctx_ref.provider(), + ctx_ref.provider(), ctx_ref.ident(), &config, ctx_ref.get_credential(), @@ -99,11 +100,11 @@ impl GroupV1Convo { let ctx_borrow = ctx.borrow(); let provider = ctx_borrow.provider(); - StagedWelcome::build_from_welcome(&*provider, &Self::mls_join_config(), welcome) + StagedWelcome::build_from_welcome(provider, &Self::mls_join_config(), welcome) .unwrap() .build() .unwrap() - .into_group(&*provider) + .into_group(provider) .unwrap() }; @@ -147,9 +148,9 @@ impl GroupV1Convo { } fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { - ds.subscribe(Self::delivery_address_from_id(&convo_id)) + ds.subscribe(&Self::delivery_address_from_id(&convo_id)) .map_err(ChatError::generic)?; - ds.subscribe(Self::ctrl_delivery_address_from_id(&convo_id)) + ds.subscribe(&Self::ctrl_delivery_address_from_id(&convo_id)) .map_err(ChatError::generic)?; Ok(()) @@ -190,14 +191,14 @@ impl GroupV1Convo { Self::ctrl_delivery_address_from_id(&self.convo_id) } - fn key_package_for_account( + fn key_package_for_account( &self, ctx: &mut ClientCtx, - ident: &str, + ident: &AccountId, ) -> Result { let retrieved_bytes = ctx .contact_registry() - .retreive(ident) + .retrieve(ident) .map_err(|e| ChatError::Generic(e.to_string()))?; // dbg!(ctx.contact_registry()); @@ -214,13 +215,13 @@ impl GroupV1Convo { } } -impl Id for GroupV1Convo { +impl Id for GroupV1Convo { fn id(&self) -> ConversationId<'_> { &self.convo_id } } -impl Convo for GroupV1Convo { +impl Convo for GroupV1Convo { fn send_message( &mut self, content: &[u8], @@ -229,7 +230,7 @@ impl Convo for GroupV1Convo { let provider = ctx_ref.provider(); let mls_message_out = self .mls_group - .create_message(&*provider, ctx_ref.ident(), content) + .create_message(provider, ctx_ref.ident(), content) .unwrap(); let a = AddressedEncryptedPayload { @@ -281,7 +282,7 @@ impl Convo for GroupV1Convo { let processed = self .mls_group - .process_message(&*provider, protocol_message) + .process_message(provider, protocol_message) .map_err(ChatError::generic)?; match processed.into_content() { @@ -292,7 +293,7 @@ impl Convo for GroupV1Convo { })), ProcessedMessageContent::StagedCommitMessage(commit) => { self.mls_group - .merge_staged_commit(&*provider, *commit) + .merge_staged_commit(provider, *commit) .map_err(ChatError::generic)?; Ok(None) } @@ -313,13 +314,13 @@ impl Convo for GroupV1Convo { } } -impl +impl GroupConvo for GroupV1Convo { fn add_member( &mut self, ctx: &mut ClientCtx, - members: &[&str], + members: &[&AccountId], ) -> Result<(), ChatError> { // add_members returns: // commit — the Commit message Alice broadcasts to all members @@ -345,16 +346,14 @@ impl let (commit, welcome, _group_info) = self .mls_group - .add_members(&*provider, ctx_ref.ident(), keypkgs.iter().as_slice()) + .add_members(provider, ctx_ref.ident(), keypkgs.iter().as_slice()) .unwrap(); - self.mls_group.merge_pending_commit(&*provider).unwrap(); + self.mls_group.merge_pending_commit(provider).unwrap(); // TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users for account_id in members { - ctx_ref - .init() - .invite_to_group_v1(ctx, account_id, &welcome)?; + ctx_ref.invite_user(ctx, account_id, &welcome)?; } let encrypted_payload = EncryptedPayload { diff --git a/core/conversations/src/ctx.rs b/core/conversations/src/ctx.rs index 8264ff0..d540a9f 100644 --- a/core/conversations/src/ctx.rs +++ b/core/conversations/src/ctx.rs @@ -6,14 +6,15 @@ use std::{ use storage::ChatStore; use crate::{DeliveryService, RegistrationService}; +use crate::service_traits::KeyPackageProvider; -pub struct ClientCtx { +pub struct ClientCtx { ds: DS, contact_registry: RS, convo_store: Rc>, // TODO: (P2) Remove Rc/Refcell } -impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx { +impl<'a, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore> ClientCtx { pub fn new(ds: DS, contact_registry: RS, convo_store: Rc>) -> Self { Self { ds, @@ -26,11 +27,17 @@ impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx< &mut self.ds } - pub fn contact_registry(&'a mut self) -> &'a mut RS { - &mut self.contact_registry + pub fn contact_registry(&'a self) -> &'a RS { + &self.contact_registry } pub fn store(&'a self) -> RefMut<'a, CS> { self.convo_store.borrow_mut() } } + +impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx { + pub fn contact_registry_mut(&'a mut self) -> &'a mut RS { + &mut self.contact_registry + } +} diff --git a/core/conversations/src/external_traits.rs b/core/conversations/src/external_traits.rs deleted file mode 100644 index c5422c9..0000000 --- a/core/conversations/src/external_traits.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::{fmt::Debug, fmt::Display}; - -use crate::types::AddressedEnvelope; - -pub trait DeliveryService { - type Error: Display; - fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>; - fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error>; -} - -pub trait RegistrationService: Debug { - type Error: Display; - fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error>; - fn retreive(&self, identity: &str) -> Result>, Self::Error>; -} diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 8bcbf5f..f072726 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -1,131 +1,108 @@ -use std::cell::{Ref, RefCell}; +use std::cell::RefCell; use std::rc::Rc; use chat_proto::logoschat::envelope::EnvelopeV1; -use crypto::Ed25519SigningKey; -use crypto::Ed25519VerifyingKey; use openmls::prelude::tls_codec::Serialize; use openmls::prelude::*; use openmls_libcrux_crypto::Provider as LibcruxProvider; -use openmls_traits::signatures::Signer; use prost::{Message, Oneof}; -use std::sync::atomic::{AtomicUsize, Ordering}; use storage::ChatStore; use storage::ConversationMeta; use crate::AddressedEnvelope; use crate::ChatError; use crate::DeliveryService; -use crate::RegistrationService; +use crate::account::LogosAccount; use crate::conversation::GroupConvo; -use crate::conversation::group_v1::{MlsCtx, MlsInitializer}; +use crate::conversation::group_v1::MlsContext; use crate::conversation::{GroupV1Convo, IdentityProvider}; use crate::ctx::ClientCtx; +use crate::types::AccountId; use crate::utils::{blake2b_hex, hash_size}; - -static ACCOUNT_COUNTER: AtomicUsize = AtomicUsize::new(0); -const ACCOUNT_NAMES: &[&str] = &["Saro", "Raya", "Pax"]; -#[derive(Clone)] -pub struct LogosAccount { - id: String, - signing_key: Ed25519SigningKey, - // x25519_key: crypto::PrivateKey, +use crate::RegistrationService; +use crate::service_traits::KeyPackageProvider; +pub struct PqMlsContext { + ident_provider: LogosAccount, + provider: LibcruxProvider, } -impl LogosAccount { - pub fn new() -> Self { - let idx = ACCOUNT_COUNTER.fetch_add(1, Ordering::Relaxed); - - let id = if idx < ACCOUNT_NAMES.len() { - ACCOUNT_NAMES[idx % ACCOUNT_NAMES.len()].to_string() - } else { - use rand_core::{OsRng, RngCore}; - const CHARSET: &[u8] = - b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let i: String = (0..8) - .map(|_| { - let idx = (OsRng.next_u32() as usize) % CHARSET.len(); - CHARSET[idx] as char - }) - .collect(); - i - }; - Self { - id, - signing_key: Ed25519SigningKey::generate(), - // x25519_key: crypto::PrivateKey::random(), - } - } -} - -impl Signer for LogosAccount { - fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { - Ok(self.signing_key.sign(payload).as_ref().to_vec()) - } - - fn signature_scheme(&self) -> SignatureScheme { - SignatureScheme::ED25519 - } -} - -impl IdentityProvider for LogosAccount { - fn friendly_name(&self) -> String { - self.id.clone() - } - - fn public_key(&self) -> Ed25519VerifyingKey { - self.signing_key.verifying_key() - } -} - -#[derive(Clone)] -pub struct MlsContext { - pub ident_provider: LogosAccount, - pub initializer: Init, - provider: Rc>, -} - -impl MlsCtx for MlsContext { +impl MlsContext for PqMlsContext { type IDENT = LogosAccount; - type INIT = Init; fn ident(&self) -> &LogosAccount { &self.ident_provider } - fn provider(&self) -> Ref<'_, LibcruxProvider> { - self.provider.borrow() + fn provider(&self) -> &LibcruxProvider { + &self.provider } - fn init(&self) -> &Init { - &self.initializer - } + fn invite_user( + &self, + ctx: &mut ClientCtx, + account_id: &AccountId, + welcome: &MlsMessageOut, + ) -> Result<(), ChatError> { + let invite = GroupV1HeavyInvite { + welcome_bytes: welcome.to_bytes()?, + }; - // Build an MLS Credential from the supplied IdentityProvider - fn get_credential(&self) -> CredentialWithKey { - CredentialWithKey { - credential: BasicCredential::new(self.ident_provider.friendly_name().into()).into(), - signature_key: self.ident_provider.public_key().as_ref().into(), - } + let frame = InboxV2Frame { + payload: Some(InviteType::GroupV1(invite)), + }; + + let envelope = EnvelopeV1 { + conversation_hint: ProtocolParams::conversation_id_for_account_id(&account_id), + salt: 0, + payload: frame.encode_to_vec().into(), + }; + + let outbound_msg = AddressedEnvelope { + delivery_address: ProtocolParams::delivery_address_for_account_id(&account_id), + data: envelope.encode_to_vec(), + }; + + ctx.ds().publish(outbound_msg).map_err(ChatError::generic)?; + Ok(()) } } -#[derive(Clone)] +struct InboxProtocolParams {} + +impl InboxProtocolParams { + fn delivery_address_for_account_id(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) + } + + fn conversation_id_for_account_id(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) + } +} + +type ProtocolParams = InboxProtocolParams; + pub struct InboxV2 { - pub account: LogosAccount, // TODO: (!) don't expose account - mls_provider: Rc>, + account_id: AccountId, + ctx: Rc>, } impl<'a> InboxV2 { - pub fn new() -> Self { - let account = LogosAccount::new(); - let mls_provider = Rc::new(RefCell::new(LibcruxProvider::new().unwrap())); + pub fn new_with_account(account: LogosAccount) -> Self { + let account_id = account.account_id().clone(); + let provider = LibcruxProvider::new().unwrap(); Self { - account, - mls_provider, + account_id, + ctx: Rc::new(RefCell::new(PqMlsContext { + ident_provider: account, + provider, + })), } } + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + pub fn register( &mut self, ctx: &mut ClientCtx, @@ -134,29 +111,29 @@ impl<'a> InboxV2 { let bytes = keypackage.tls_serialize_detached()?; - ctx.contact_registry() - .register(self.account.friendly_name(), bytes) + ctx.contact_registry_mut() + .register(&self.ctx.borrow().ident_provider.friendly_name(), bytes) .map_err(ChatError::generic)?; //TODO: (P1) create an address scheme instead of using names Ok(()) } pub fn delivery_address(&self) -> String { - Self::delivery_address_for_account_id(&self.account.id) + ProtocolParams::delivery_address_for_account_id(&self.account_id) } pub fn id(&self) -> String { - Self::conversation_id_for_account_id(&self.account.id) + ProtocolParams::conversation_id_for_account_id(&self.account_id) } - pub fn create_group_v1( + pub fn create_group_v1( &self, ctx: &mut ClientCtx, - ) -> Result>, ChatError> { + ) -> Result, ChatError> { let convo = GroupV1Convo::new(self.assemble_ctx(), ctx.ds()); Ok(convo) } - pub fn handle_frame( + pub fn handle_frame( &self, ctx: &mut ClientCtx, payload_bytes: &[u8], @@ -174,15 +151,11 @@ impl<'a> InboxV2 { } } - fn assemble_ctx(&self) -> MlsContext { - MlsContext { - ident_provider: self.account.clone(), - initializer: self.clone(), - provider: self.mls_provider.clone(), - } + fn assemble_ctx(&self) -> Rc> { + self.ctx.clone() } - fn persist_convo( + fn persist_convo( &self, ctx: &'a ClientCtx, convo: impl GroupConvo, @@ -199,7 +172,7 @@ impl<'a> InboxV2 { Ok(()) } - fn handle_heavy_invite( + fn handle_heavy_invite( &self, ctx: &mut ClientCtx, invite: GroupV1HeavyInvite, @@ -213,15 +186,12 @@ impl<'a> InboxV2 { )); }; - let mls_ctx = Rc::new(RefCell::new(self.assemble_ctx())); - - let convo = GroupV1Convo::new_from_welcome(mls_ctx, ctx.ds(), welcome); + let convo = GroupV1Convo::new_from_welcome(self.assemble_ctx(), ctx.ds(), welcome); self.persist_convo(ctx, convo) } fn create_keypackage(&self) -> Result { - let mls_ctx = self.assemble_ctx(); - + let ctx_borrow = self.ctx.borrow(); let capabilities = Capabilities::builder() .ciphersuites(vec![ Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, @@ -232,70 +202,28 @@ impl<'a> InboxV2 { .leaf_node_capabilities(capabilities) .build( Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, - &*mls_ctx.provider(), - &self.account, - mls_ctx.get_credential(), + ctx_borrow.provider(), + ctx_borrow.ident(), + ctx_borrow.get_credential(), ) .expect("Failed to build KeyPackage"); Ok(a.key_package().clone()) } - fn delivery_address_for_account_id(account_id: &str) -> String { - blake2b_hex::(&["InboxV2|", "delivery_address|", account_id]) - } - - fn conversation_id_for_account_id(account_id: &str) -> String { - blake2b_hex::(&["InboxV2|", "conversation_id|", account_id]) - } - - pub fn load_mls_convo( + pub fn load_mls_convo( &self, ctx: &mut ClientCtx, convo_id: String, - ) -> Result>, ChatError> { - let mls_ctx = self.assemble_ctx(); - + ) -> Result, ChatError> { let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?; let group_id = GroupId::from_slice(&group_id_bytes); - let convo = - GroupV1Convo::load(Rc::new(RefCell::new(mls_ctx)), ctx.ds(), convo_id, group_id)?; + let convo = GroupV1Convo::load(self.assemble_ctx(), ctx.ds(), convo_id, group_id)?; Ok(convo) } } -impl MlsInitializer for InboxV2 { - fn invite_to_group_v1( - &self, - ctx: &mut ClientCtx, - account_id: &str, - welcome: &MlsMessageOut, - ) -> Result<(), ChatError> { - let invite = GroupV1HeavyInvite { - welcome_bytes: welcome.to_bytes()?, - }; - - let frame = InboxV2Frame { - payload: Some(InviteType::GroupV1(invite)), - }; - - let envelope = EnvelopeV1 { - conversation_hint: Self::conversation_id_for_account_id(account_id), - salt: 0, - payload: frame.encode_to_vec().into(), - }; - - let outbound_msg = AddressedEnvelope { - delivery_address: Self::delivery_address_for_account_id(account_id), - data: envelope.encode_to_vec(), - }; - - ctx.ds().publish(outbound_msg).map_err(ChatError::generic)?; - Ok(()) - } -} - #[derive(Clone, PartialEq, Message)] pub struct InboxV2Frame { #[prost(oneof = "InviteType", tags = "1")] diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 63417a4..516edeb 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -4,7 +4,7 @@ mod conversation; mod crypto; mod ctx; mod errors; -mod external_traits; +mod service_traits; mod inbox; mod inbox_v2; mod proto; @@ -18,7 +18,8 @@ mod test_utils; pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use conversation::GroupConvo; pub use errors::ChatError; -pub use external_traits::{DeliveryService, RegistrationService}; +pub use service_traits::{DeliveryService, RegistrationService}; pub use sqlite::ChatStorage; pub use sqlite::StorageConfig; -pub use types::{AddressedEnvelope, ContentData}; +pub use types::{AccountId, AddressedEnvelope, ContentData}; +pub use utils::hex_trunc; diff --git a/core/conversations/src/service_traits.rs b/core/conversations/src/service_traits.rs new file mode 100644 index 0000000..028a1c6 --- /dev/null +++ b/core/conversations/src/service_traits.rs @@ -0,0 +1,38 @@ +use std::{fmt::Debug, fmt::Display}; + +use crate::types::{AccountId, AddressedEnvelope}; + +/// A Delivery service is responsible for payload transport. +/// This interface allows Conversations to send payloads on the wire as well as +/// register interest in delivery_addresses. Client implementations are responsible +/// for providing the inbound payloads to Context::handle_payload. +pub trait DeliveryService: Debug { + type Error: Display; + fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error>; + fn subscribe(&mut self, delivery_address: &str) -> Result<(), Self::Error>; +} + +/// Manages key bundle storage for MLS group creation/addition while contacts are +/// offline. +/// +/// Implement this to provide a contact registry — ach participant publishes their key package +/// on registration; others fetch it to initiate a conversation. +pub trait RegistrationService: Debug { + type Error: Display; + fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error>; + fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error>; +} + +/// Read-only view of a contact registry. Not part of the public API. +/// Satisfied automatically by any `RegistrationService` implementation. +pub trait KeyPackageProvider: Debug { + type Error: Display; + fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error>; +} + +impl KeyPackageProvider for T { + type Error = T::Error; + fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error> { + RegistrationService::retrieve(self, identity) + } +} diff --git a/core/conversations/src/test_utils.rs b/core/conversations/src/test_utils.rs index 2f23da6..cceb668 100644 --- a/core/conversations/src/test_utils.rs +++ b/core/conversations/src/test_utils.rs @@ -10,7 +10,7 @@ use storage::{ConversationMeta, ConversationStore, IdentityStore}; use storage::{EphemeralKeyStore, RatchetStore}; use crate::{ - AddressedEnvelope, DeliveryService, RegistrationService, + AccountId, AddressedEnvelope, DeliveryService, RegistrationService, KeyPackageProvider, utils::{blake2b_hex, hash_size::Testing}, }; @@ -109,17 +109,17 @@ impl DeliveryService for LocalBroadcaster { Ok(()) } - fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error> { + fn subscribe(&mut self, delivery_address: &str) -> Result<(), Self::Error> { // Strict temporal ordering of subscriptions is not enforced. // Subscruptions are evaluated on polling, not when the message is published - self.subscriptions.insert(delivery_address); + self.subscriptions.insert(delivery_address.to_string()); Ok(()) } } /// A Contact Registry used for Tests. /// This implementation stores bundle bytes and then returns them when -/// retreived +/// retrieved /// #[derive(Clone)] @@ -159,16 +159,18 @@ impl Debug for EphemeralRegistry { } } -impl RegistrationService for EphemeralRegistry { +impl KeyPackageProvider for EphemeralRegistry { type Error = String; - fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error> { - self.registry.lock().unwrap().insert(identity, key_bundle); - Ok(()) + fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error> { + Ok(self.registry.lock().unwrap().get(identity.as_str()).cloned()) } +} - fn retreive(&self, identity: &str) -> Result>, Self::Error> { - Ok(self.registry.lock().unwrap().get(identity).cloned()) +impl RegistrationService for EphemeralRegistry { + fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { + self.registry.lock().unwrap().insert(identity.to_string(), key_bundle); + Ok(()) } } diff --git a/core/conversations/src/types.rs b/core/conversations/src/types.rs index b5b174b..9564234 100644 --- a/core/conversations/src/types.rs +++ b/core/conversations/src/types.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::fmt::{self, Debug}; use crate::proto::{self, Message}; diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs index ff81b30..1769725 100644 --- a/core/integration_tests_core/tests/mls_integration.rs +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; -use libchat::{ChatStorage, ContentData, Context, ConversationId, GroupConvo}; +use libchat::{ChatStorage, ContentData, Context, ConversationId, GroupConvo, hex_trunc}; type TestContext = Context; @@ -77,7 +77,7 @@ impl DerefMut for Client { fn pretty_print(prefix: impl Into) -> Box { let prefix = prefix.into(); return Box::new(move |c: ContentData| { - let cid = c.conversation_id.as_bytes(); + let cid = hex_trunc(c.conversation_id.as_bytes()); let content = String::from_utf8(c.data).unwrap(); println!("{} ({:?}) {}", prefix, cid, content) }); @@ -106,10 +106,8 @@ fn create_group() { const SARO: usize = 0; const RAYA: usize = 1; - let raya_id = clients[RAYA].account_id(); - let s_convo = clients[SARO] - .create_group_convo(&[raya_id.as_ref()]) - .unwrap(); + let raya_id = clients[RAYA].account_id().clone(); + let s_convo = clients[SARO].create_group_convo(&[&raya_id]).unwrap(); let convo_id = s_convo.id(); @@ -143,10 +141,10 @@ fn create_group() { clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax")))); const PAX: usize = 2; - let pax_id = clients[PAX].account_id(); + let pax_id = clients[PAX].account_id().clone(); clients[SARO] .convo(convo_id) - .add_member(&mut clients[SARO].client_ctx(), &[pax_id.as_ref()]) + .add_member(&mut clients[SARO].client_ctx(), &[&pax_id]) .unwrap(); // clients[SARO].process_messages(); diff --git a/extensions/components/src/contact_registry.rs b/extensions/components/src/contact_registry.rs index 02f2f26..bd67775 100644 --- a/extensions/components/src/contact_registry.rs +++ b/extensions/components/src/contact_registry.rs @@ -4,11 +4,11 @@ use std::{ sync::{Arc, Mutex}, }; -use libchat::RegistrationService; +use libchat::{AccountId, RegistrationService}; /// A Contact Registry used for Tests. /// This implementation stores bundle bytes and then returns them when -/// retreived +/// retrieved /// #[derive(Clone)] @@ -51,12 +51,17 @@ impl Debug for EphemeralRegistry { impl RegistrationService for EphemeralRegistry { type Error = String; - fn register(&mut self, identity: String, key_bundle: Vec) -> Result<(), Self::Error> { - self.registry.lock().unwrap().insert(identity, key_bundle); + fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { + self.registry.lock().unwrap().insert(identity.to_string(), key_bundle); Ok(()) } - fn retreive(&self, identity: &str) -> Result>, Self::Error> { - Ok(self.registry.lock().unwrap().get(identity).cloned()) + fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error> { + Ok(self + .registry + .lock() + .unwrap() + .get(identity.as_str()) + .cloned()) } } diff --git a/extensions/components/src/delivery/local_broadcaster.rs b/extensions/components/src/delivery/local_broadcaster.rs index 9828b26..a366ad2 100644 --- a/extensions/components/src/delivery/local_broadcaster.rs +++ b/extensions/components/src/delivery/local_broadcaster.rs @@ -107,10 +107,10 @@ impl DeliveryService for LocalBroadcaster { Ok(()) } - fn subscribe(&mut self, delivery_address: String) -> Result<(), Self::Error> { + fn subscribe(&mut self, delivery_address: &str) -> Result<(), Self::Error> { // Strict temporal ordering of subscriptions is not enforced. // Subscriptions are evaluated on polling, not when the message is published - self.subscriptions.insert(delivery_address); + self.subscriptions.insert(delivery_address.to_string()); Ok(()) } } From d8e15dc8d6c31330014adab01e7b62ddb3eeaf84 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:59:20 -0700 Subject: [PATCH 09/39] Remove ClientCtx --- Cargo.lock | 1 + core/conversations/Cargo.toml | 1 + core/conversations/src/account.rs | 4 +- core/conversations/src/context.rs | 102 +++++++++------ core/conversations/src/conversation.rs | 31 ++--- .../src/conversation/group_v1.rs | 122 ++++++++++++------ core/conversations/src/ctx.rs | 43 ------ core/conversations/src/inbox_v2.rs | 110 ++++++++-------- core/conversations/src/lib.rs | 3 +- core/conversations/src/test_utils.rs | 35 +++-- core/conversations/src/utils.rs | 3 +- .../tests/mls_integration.rs | 62 +++------ .../tests/private_integration.rs | 6 +- 13 files changed, 245 insertions(+), 278 deletions(-) delete mode 100644 core/conversations/src/ctx.rs diff --git a/Cargo.lock b/Cargo.lock index edbb5ac..d5c8689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1424,6 +1424,7 @@ dependencies = [ "double-ratchets", "hex", "openmls", + "openmls_libcrux_crypto 0.3.1", "openmls_traits 0.5.0", "prost", "rand_core 0.6.4", diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 5ee70d1..512cfa6 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -21,6 +21,7 @@ thiserror = "2.0.17" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } storage = { path = "../storage" } openmls = { version = "0.8.1", features = ["libcrux-provider"] } +openmls_libcrux_crypto = "0.3.1" openmls_traits = "0.5.0" [dev-dependencies] diff --git a/core/conversations/src/account.rs b/core/conversations/src/account.rs index 3ca7dfc..161710f 100644 --- a/core/conversations/src/account.rs +++ b/core/conversations/src/account.rs @@ -18,11 +18,11 @@ impl LogosAccount { /// TODO: (P1) Remove once implementation is ready. pub fn new_test(explicit_id: impl Into) -> Self { let signing_key = Ed25519SigningKey::generate(); - let verifying_key = signing_key.verifying_key() + let verifying_key = signing_key.verifying_key(); Self { id: AccountId::new(explicit_id.into()), signing_key, - verifying_key + verifying_key, } } diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index e3d6906..fef0bf4 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -1,12 +1,10 @@ -use std::cell::Ref; +use std::cell::{Ref, RefMut}; use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; use crate::account::LogosAccount; -use crate::conversation::{Convo, GroupConvo, IdentityProvider}; -use crate::ctx::ClientCtx; +use crate::conversation::{Convo, GroupConvo}; -use crate::account::LogosAccount; use crate::{DeliveryService, RegistrationService}; use crate::{ conversation::{Conversation, Id, PrivateV1Convo}, @@ -26,13 +24,18 @@ pub use crate::inbox::Introduction; // Ctx manages lifetimes of objects to process and generate payloads. pub struct Context { identity: Rc, - client_ctx: ClientCtx, - inbox: Inbox, - pq_inbox: InboxV2, + ds: Rc>, store: Rc>, + inbox: Inbox, + pq_inbox: InboxV2, } -impl Context { +impl Context +where + DS: DeliveryService + 'static, + RS: RegistrationService + 'static, + CS: ChatStore + 'static, +{ /// Opens or creates a Context with the given storage configuration. /// /// If an identity exists in storage, it will be restored. @@ -40,13 +43,15 @@ impl Cont pub fn new_from_store( name: impl Into, delivery: DS, - contact_reg: RS, + registration: RS, store: CS, ) -> Result { let name = name.into(); + // Services for sharing with Converastions/Inboxes + let ds = Rc::new(RefCell::new(delivery)); + let contact_registry = Rc::new(RefCell::new(registration)); let store = Rc::new(RefCell::new(store)); - let mut ctx = ClientCtx::new(delivery, contact_reg, store.clone()); // Load or create identity let identity = if let Some(identity) = store.borrow().load_identity()? { @@ -60,20 +65,24 @@ impl Cont let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&store), Rc::clone(&identity)); - let pq_inbox = InboxV2::new_with_account(LogosAccount::new_test(name)); + let pq_inbox = InboxV2::new( + LogosAccount::new_test(name), + ds.clone(), + contact_registry.clone(), + store.clone(), + ); // Subscribe - ctx.ds() + ds.borrow_mut() .subscribe(&pq_inbox.delivery_address()) .map_err(ChatError::generic)?; Ok(Self { identity: identity, - client_ctx: ctx, + ds, + store, inbox, pq_inbox, - store, - account: LogosAccount::new_test(name.as_str()), }) } @@ -83,47 +92,55 @@ impl Cont pub fn new_with_name( name: impl Into, delivery: DS, - contact_reg: RS, + registration: RS, chat_store: CS, ) -> Result { let name = name.into(); let identity = Identity::new(&name); - let chat_store = Rc::new(RefCell::new(chat_store)); - let mut ctx = ClientCtx::new(delivery, contact_reg, chat_store.clone()); - chat_store + // Services for sharing with Converastions/Inboxes + let ds = Rc::new(RefCell::new(delivery)); + let contact_registry = Rc::new(RefCell::new(registration)); + let store = Rc::new(RefCell::new(chat_store)); + + store .borrow_mut() .save_identity(&identity) .expect("in-memory storage should not fail"); let identity = Rc::new(identity); - let inbox = Inbox::new(Rc::clone(&chat_store), Rc::clone(&identity)); - let mut pq_inbox = InboxV2::new_with_account(LogosAccount::new_test(name)); - pq_inbox.register(&mut ctx)?; + let inbox = Inbox::new(store.clone(), Rc::clone(&identity)); + let mut pq_inbox = InboxV2::new( + LogosAccount::new_test(name), + ds.clone(), + contact_registry.clone(), + store.clone(), + ); - ctx.ds() + // TODO: (!) This seems weird here + pq_inbox.register()?; + + ds.borrow_mut() .subscribe(&pq_inbox.delivery_address()) .map_err(ChatError::generic)?; Ok(Self { identity, - client_ctx: ctx, + ds, + store, pq_inbox, inbox, - - store: chat_store, - account: LogosAccount::new_test(name.as_str()), }) } + pub fn ds(&self) -> RefMut<'_, DS> { + self.ds.borrow_mut() + } + pub fn store(&self) -> Ref<'_, CS> { self.store.borrow() } - pub fn client_ctx(&mut self) -> &mut ClientCtx { - &mut self.client_ctx - } - pub fn identity(&self) -> &Identity { &self.identity } @@ -164,16 +181,17 @@ impl Cont pub fn create_group_convo( &mut self, participants: &[&AccountId], - ) -> Result>, ChatError> { - let mut convo = self.pq_inbox.create_group_v1(&mut self.client_ctx)?; - self.client_ctx - .store() + ) -> Result>, ChatError> { + // TODO: (!) Perform this in InboxV2? + let mut convo = self.pq_inbox.create_group_v1()?; + self.store + .borrow_mut() .save_conversation(&storage::ConversationMeta { local_convo_id: convo.id().to_string(), remote_convo_id: "0".into(), kind: ConversationKind::GroupV1, })?; - convo.add_member(&mut self.client_ctx, participants)?; + convo.add_member(participants)?; Ok(Box::new(convo)) } @@ -246,7 +264,7 @@ impl Cont // Dispatch encrypted payload to Inbox, and register the created Conversation fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result, ChatError> { - self.pq_inbox.handle_frame(&mut self.client_ctx, payload)?; + self.pq_inbox.handle_frame(payload)?; Ok(None) } @@ -270,7 +288,7 @@ impl Cont pub fn get_convo( &mut self, convo_id: ConversationId, - ) -> Result>, ChatError> { + ) -> Result>, ChatError> { self.load_group_convo(convo_id) } @@ -292,8 +310,7 @@ impl Cont Ok(Box::new(private_convo)) } ConversationKind::GroupV1 => Ok(Box::new( - self.pq_inbox - .load_mls_convo(&mut self.client_ctx, record.local_convo_id)?, + self.pq_inbox.load_mls_convo(record.local_convo_id)?, )), ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( "unsupported conversation type: {}", @@ -306,7 +323,7 @@ impl Cont fn load_group_convo( &mut self, convo_id: ConversationId, - ) -> Result>, ChatError> { + ) -> Result>, ChatError> { let record = self .store .borrow() @@ -318,8 +335,7 @@ impl Cont Err(ChatError::NoConvo("This is not a group convo".into())) } ConversationKind::GroupV1 => Ok(Box::new( - self.pq_inbox - .load_mls_convo(&mut self.client_ctx, record.local_convo_id)?, + self.pq_inbox.load_mls_convo(record.local_convo_id)?, )), ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( "unsupported conversation type: {}", diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 4a36bc3..702ca93 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -2,14 +2,14 @@ pub mod group_v1; mod privatev1; use crate::{ - DeliveryService, service_traits::KeyPackageProvider, - ctx::ClientCtx, + DeliveryService, + service_traits::KeyPackageProvider, types::{AccountId, AddressedEncryptedPayload, ContentData}, }; use chat_proto::logoschat::encryption::EncryptedPayload; use std::fmt::Debug; use std::sync::Arc; -use storage::{ChatStore, ConversationKind, ConversationStore, RatchetStore}; +use storage::{ConversationKind, ConversationStore, RatchetStore}; pub use crate::errors::ChatError; pub use group_v1::{GroupV1Convo, IdentityProvider}; @@ -42,27 +42,12 @@ pub trait Convo: Id + Debug { fn convo_type(&self) -> ConversationKind; } -pub trait GroupConvo: Convo { - fn add_member( - &mut self, - ctx: &mut ClientCtx, - members: &[&AccountId], - ) -> Result<(), ChatError>; +pub trait GroupConvo: Convo { + fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError>; - // Default implementation which dispatches envelopes to the DeliveryService - fn send_content( - &mut self, - ctx: &mut ClientCtx, - content: &[u8], - ) -> Result<(), ChatError> { - let payloads = self.send_message(content)?; - for payload in payloads { - ctx.ds() - .publish(payload.into_envelope(self.id().into())) - .map_err(|e| ChatError::Delivery(e.to_string()))?; - } - Ok(()) - } + // This is intended to replace `send_message`. The trait change is that it automatically + // sends the payload directly. + fn send_content(&mut self, content: &[u8]) -> Result<(), ChatError>; } pub enum Conversation { diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 12a71da..f54c56d 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -8,13 +8,13 @@ use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::*; use openmls_libcrux_crypto::Provider as LibcruxProvider; use openmls_traits::signatures::Signer as OpenMlsSigner; -use storage::{ChatStore, ConversationKind}; +use storage::ConversationKind; use crate::types::AccountId; use crate::{ - DeliveryService, service_traits::KeyPackageProvider, + DeliveryService, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, - ctx::ClientCtx, + service_traits::KeyPackageProvider, types::{AddressedEncryptedPayload, ContentData}, }; @@ -37,21 +37,28 @@ pub trait MlsContext { } } - fn invite_user( + fn invite_user( &self, - ctx: &mut ClientCtx, + ds: &mut DS, account_id: &AccountId, welcome: &MlsMessageOut, ) -> Result<(), ChatError>; } -pub struct GroupV1Convo { - ctx: Rc>, +pub struct GroupV1Convo { + ctx: Rc>, + ds: Rc>, + keypkg_provider: Rc>, pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility convo_id: String, } -impl std::fmt::Debug for GroupV1Convo { +impl std::fmt::Debug for GroupV1Convo +where + MlsCtx: MlsContext, + DS: DeliveryService, + KP: KeyPackageProvider, +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GroupV1Convo") .field("name", &self.ctx.borrow().ident().friendly_name()) @@ -61,8 +68,17 @@ impl std::fmt::Debug for GroupV1Convo { } } -impl GroupV1Convo { - pub fn new(ctx: Rc>, ds: &mut DS) -> Self { +impl GroupV1Convo +where + MlsCtx: MlsContext, + DS: DeliveryService, + KP: KeyPackageProvider, +{ + pub fn new( + ctx: Rc>, + ds: Rc>, + keypkg_provider: Rc>, + ) -> Self { let config = Self::mls_create_config(); let mls_group = { let ctx_ref = ctx.borrow(); @@ -75,7 +91,7 @@ impl GroupV1Convo { .unwrap() }; let convo_id = hex::encode(mls_group.group_id().as_slice()); - Self::subscribe(ds, &convo_id); + Self::subscribe(&mut ds.borrow_mut(), &convo_id); println!( "@ Create Convo: {}. {}. d:{} dc:{}", @@ -86,14 +102,17 @@ impl GroupV1Convo { ); Self { ctx, + ds, + keypkg_provider, mls_group, convo_id, } } - pub fn new_from_welcome( - ctx: Rc>, - ds: &mut DS, + pub fn new_from_welcome( + ctx: Rc>, + ds: Rc>, + keypkg_provider: Rc>, welcome: Welcome, ) -> Self { let mls_group = { @@ -109,7 +128,7 @@ impl GroupV1Convo { }; let convo_id = hex::encode(mls_group.group_id().as_slice()); - Self::subscribe(ds, &convo_id); + Self::subscribe(&mut *ds.borrow_mut(), &convo_id); println!( "@ Welcome Convo: I:{}. {}. d:{} dc:{}", @@ -121,14 +140,17 @@ impl GroupV1Convo { GroupV1Convo { ctx, + ds, + keypkg_provider, mls_group, convo_id, } } - pub fn load( - ctx: Rc>, - ds: &mut DS, + pub fn load( + ctx: Rc>, + ds: Rc>, + keypkg_provider: Rc>, convo_id: String, group_id: GroupId, ) -> Result { @@ -138,16 +160,18 @@ impl GroupV1Convo { return Err(ChatError::NoConvo("mls group not found".into())); }; - Self::subscribe(ds, &convo_id)?; + Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; Ok(GroupV1Convo { ctx, + ds, + keypkg_provider, mls_group, convo_id, }) } - fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { + fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { ds.subscribe(&Self::delivery_address_from_id(&convo_id)) .map_err(ChatError::generic)?; ds.subscribe(&Self::ctrl_delivery_address_from_id(&convo_id)) @@ -191,15 +215,12 @@ impl GroupV1Convo { Self::ctrl_delivery_address_from_id(&self.convo_id) } - fn key_package_for_account( - &self, - ctx: &mut ClientCtx, - ident: &AccountId, - ) -> Result { - let retrieved_bytes = ctx - .contact_registry() + fn key_package_for_account(&self, ident: &AccountId) -> Result { + let retrieved_bytes = self + .keypkg_provider + .borrow() .retrieve(ident) - .map_err(|e| ChatError::Generic(e.to_string()))?; + .map_err(|e: KP::Error| ChatError::Generic(e.to_string()))?; // dbg!(ctx.contact_registry()); let Some(keypkg_bytes) = retrieved_bytes else { @@ -215,13 +236,23 @@ impl GroupV1Convo { } } -impl Id for GroupV1Convo { +impl Id for GroupV1Convo +where + MlsCtx: MlsContext, + DS: DeliveryService, + KP: KeyPackageProvider, +{ fn id(&self) -> ConversationId<'_> { &self.convo_id } } -impl Convo for GroupV1Convo { +impl Convo for GroupV1Convo +where + MlsCtx: MlsContext, + DS: DeliveryService, + KP: KeyPackageProvider, +{ fn send_message( &mut self, content: &[u8], @@ -314,14 +345,13 @@ impl Convo for GroupV1Convo { } } -impl - GroupConvo for GroupV1Convo +impl GroupConvo for GroupV1Convo +where + MlsCtx: MlsContext, + DS: DeliveryService, + KP: KeyPackageProvider, { - fn add_member( - &mut self, - ctx: &mut ClientCtx, - members: &[&AccountId], - ) -> Result<(), ChatError> { + fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> { // add_members returns: // commit — the Commit message Alice broadcasts to all members // welcome — the Welcome message sent privately to each new joiner @@ -341,7 +371,7 @@ impl, ChatError>>()?; let (commit, welcome, _group_info) = self @@ -353,7 +383,7 @@ impl Result<(), ChatError> { + let payloads = self.send_message(content)?; + for payload in payloads { + self.ds + .borrow_mut() + .publish(payload.into_envelope(self.id().into())) + .map_err(|e| ChatError::Delivery(e.to_string()))?; + } + Ok(()) + } } diff --git a/core/conversations/src/ctx.rs b/core/conversations/src/ctx.rs deleted file mode 100644 index d540a9f..0000000 --- a/core/conversations/src/ctx.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::{ - cell::{RefCell, RefMut}, - rc::Rc, -}; - -use storage::ChatStore; - -use crate::{DeliveryService, RegistrationService}; -use crate::service_traits::KeyPackageProvider; - -pub struct ClientCtx { - ds: DS, - contact_registry: RS, - convo_store: Rc>, // TODO: (P2) Remove Rc/Refcell -} - -impl<'a, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore> ClientCtx { - pub fn new(ds: DS, contact_registry: RS, convo_store: Rc>) -> Self { - Self { - ds, - contact_registry, - convo_store, - } - } - - pub fn ds(&'a mut self) -> &'a mut DS { - &mut self.ds - } - - pub fn contact_registry(&'a self) -> &'a RS { - &self.contact_registry - } - - pub fn store(&'a self) -> RefMut<'a, CS> { - self.convo_store.borrow_mut() - } -} - -impl<'a, DS: DeliveryService, RS: RegistrationService, CS: ChatStore> ClientCtx { - pub fn contact_registry_mut(&'a mut self) -> &'a mut RS { - &mut self.contact_registry - } -} diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index f072726..4aea37a 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -12,15 +12,13 @@ use storage::ConversationMeta; use crate::AddressedEnvelope; use crate::ChatError; use crate::DeliveryService; +use crate::RegistrationService; use crate::account::LogosAccount; use crate::conversation::GroupConvo; use crate::conversation::group_v1::MlsContext; use crate::conversation::{GroupV1Convo, IdentityProvider}; -use crate::ctx::ClientCtx; use crate::types::AccountId; use crate::utils::{blake2b_hex, hash_size}; -use crate::RegistrationService; -use crate::service_traits::KeyPackageProvider; pub struct PqMlsContext { ident_provider: LogosAccount, provider: LibcruxProvider, @@ -37,9 +35,9 @@ impl MlsContext for PqMlsContext { &self.provider } - fn invite_user( + fn invite_user( &self, - ctx: &mut ClientCtx, + ds: &mut DS, account_id: &AccountId, welcome: &MlsMessageOut, ) -> Result<(), ChatError> { @@ -62,7 +60,7 @@ impl MlsContext for PqMlsContext { data: envelope.encode_to_vec(), }; - ctx.ds().publish(outbound_msg).map_err(ChatError::generic)?; + ds.publish(outbound_msg).map_err(ChatError::generic)?; Ok(()) } } @@ -81,17 +79,33 @@ impl InboxProtocolParams { type ProtocolParams = InboxProtocolParams; -pub struct InboxV2 { +pub struct InboxV2 { account_id: AccountId, + ds: Rc>, + reg_service: Rc>, + store: Rc>, ctx: Rc>, } -impl<'a> InboxV2 { - pub fn new_with_account(account: LogosAccount) -> Self { +impl<'a, DS, CS, RS> InboxV2 +where + DS: DeliveryService, + RS: RegistrationService, + CS: ChatStore, +{ + pub fn new( + account: LogosAccount, + ds: Rc>, + reg_service: Rc>, + store: Rc>, + ) -> Self { let account_id = account.account_id().clone(); let provider = LibcruxProvider::new().unwrap(); Self { account_id, + ds, + reg_service, + store, ctx: Rc::new(RefCell::new(PqMlsContext { ident_provider: account, provider, @@ -103,18 +117,19 @@ impl<'a> InboxV2 { &self.account_id } - pub fn register( - &mut self, - ctx: &mut ClientCtx, - ) -> Result<(), ChatError> { - let keypackage = self.create_keypackage()?; + /// Submit MlsKeypackage to registration service + pub fn register(&mut self) -> Result<(), ChatError> { + let keypackage_bytes = self.create_keypackage()?.tls_serialize_detached()?; - let bytes = keypackage.tls_serialize_detached()?; - - ctx.contact_registry_mut() - .register(&self.ctx.borrow().ident_provider.friendly_name(), bytes) - .map_err(ChatError::generic)?; //TODO: (P1) create an address scheme instead of using names - Ok(()) + // TODO: (P3) Each keypackage can only be used once either enable... + // "LastResort" package or publish multiple + self.reg_service + .borrow_mut() + .register( + &self.ctx.borrow().ident_provider.friendly_name(), + keypackage_bytes, + ) + .map_err(ChatError::generic) } pub fn delivery_address(&self) -> String { @@ -125,19 +140,12 @@ impl<'a> InboxV2 { ProtocolParams::conversation_id_for_account_id(&self.account_id) } - pub fn create_group_v1( - &self, - ctx: &mut ClientCtx, - ) -> Result, ChatError> { - let convo = GroupV1Convo::new(self.assemble_ctx(), ctx.ds()); + pub fn create_group_v1(&self) -> Result, ChatError> { + let convo = GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone()); Ok(convo) } - pub fn handle_frame( - &self, - ctx: &mut ClientCtx, - payload_bytes: &[u8], - ) -> Result<(), ChatError> { + pub fn handle_frame(&self, payload_bytes: &[u8]) -> Result<(), ChatError> { let inbox_frame = InboxV2Frame::decode(payload_bytes)?; let Some(payload) = inbox_frame.payload else { @@ -146,20 +154,12 @@ impl<'a> InboxV2 { match payload { InviteType::GroupV1(group_v1_heavy_invite) => { - self.handle_heavy_invite(ctx, group_v1_heavy_invite) + self.handle_heavy_invite(group_v1_heavy_invite) } } } - fn assemble_ctx(&self) -> Rc> { - self.ctx.clone() - } - - fn persist_convo( - &self, - ctx: &'a ClientCtx, - convo: impl GroupConvo, - ) -> Result<(), ChatError> { + fn persist_convo(&self, convo: impl GroupConvo) -> Result<(), ChatError> { // TODO: (P2) Remove remote_convo_id this is an implementation detail specific to PrivateV1 // TODO: (P3) Implement From for ConversationMeta let meta = ConversationMeta { @@ -167,16 +167,12 @@ impl<'a> InboxV2 { remote_convo_id: "0".into(), kind: storage::ConversationKind::GroupV1, }; - ctx.store().save_conversation(&meta)?; + self.store.borrow_mut().save_conversation(&meta)?; // TODO: (P1) Persist state Ok(()) } - fn handle_heavy_invite( - &self, - ctx: &mut ClientCtx, - invite: GroupV1HeavyInvite, - ) -> Result<(), ChatError> { + fn handle_heavy_invite(&self, invite: GroupV1HeavyInvite) -> Result<(), ChatError> { let (msg_in, _rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?; let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else { @@ -186,8 +182,13 @@ impl<'a> InboxV2 { )); }; - let convo = GroupV1Convo::new_from_welcome(self.assemble_ctx(), ctx.ds(), welcome); - self.persist_convo(ctx, convo) + let convo = GroupV1Convo::new_from_welcome( + self.ctx.clone(), + self.ds.clone(), + self.reg_service.clone(), + welcome, + ); + self.persist_convo(convo) } fn create_keypackage(&self) -> Result { @@ -211,14 +212,19 @@ impl<'a> InboxV2 { Ok(a.key_package().clone()) } - pub fn load_mls_convo( + pub fn load_mls_convo( &self, - ctx: &mut ClientCtx, convo_id: String, - ) -> Result, ChatError> { + ) -> Result, ChatError> { let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?; let group_id = GroupId::from_slice(&group_id_bytes); - let convo = GroupV1Convo::load(self.assemble_ctx(), ctx.ds(), convo_id, group_id)?; + let convo = GroupV1Convo::load( + self.ctx.clone(), + self.ds.clone(), + self.reg_service.clone(), + convo_id, + group_id, + )?; Ok(convo) } diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 516edeb..9e53aa5 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -2,12 +2,11 @@ mod account; mod context; mod conversation; mod crypto; -mod ctx; mod errors; -mod service_traits; mod inbox; mod inbox_v2; mod proto; +mod service_traits; mod types; mod utils; diff --git a/core/conversations/src/test_utils.rs b/core/conversations/src/test_utils.rs index cceb668..9999735 100644 --- a/core/conversations/src/test_utils.rs +++ b/core/conversations/src/test_utils.rs @@ -10,7 +10,7 @@ use storage::{ConversationMeta, ConversationStore, IdentityStore}; use storage::{EphemeralKeyStore, RatchetStore}; use crate::{ - AccountId, AddressedEnvelope, DeliveryService, RegistrationService, KeyPackageProvider, + AccountId, AddressedEnvelope, DeliveryService, RegistrationService, utils::{blake2b_hex, hash_size::Testing}, }; @@ -159,18 +159,23 @@ impl Debug for EphemeralRegistry { } } -impl KeyPackageProvider for EphemeralRegistry { +impl RegistrationService for EphemeralRegistry { type Error = String; + fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { + self.registry + .lock() + .unwrap() + .insert(identity.to_string(), key_bundle); + Ok(()) + } fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error> { - Ok(self.registry.lock().unwrap().get(identity.as_str()).cloned()) - } -} - -impl RegistrationService for EphemeralRegistry { - fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { - self.registry.lock().unwrap().insert(identity.to_string(), key_bundle); - Ok(()) + Ok(self + .registry + .lock() + .unwrap() + .get(identity.as_str()) + .cloned()) } } @@ -292,13 +297,3 @@ impl RatchetStore for MemStore { todo!() } } - -// impl GroupMlsStorageV1 for MemStore { -// fn save_state(&self, convo_id: &str, state: &[u8]) { -// self.state.insert(convo_id, state) -// } - -// fn load_state(&self, convo_id: &str) -> Vec { -// self.state.get(convo_id).unwrap().clone() -// } -// } diff --git a/core/conversations/src/utils.rs b/core/conversations/src/utils.rs index 3b51997..3c81443 100644 --- a/core/conversations/src/utils.rs +++ b/core/conversations/src/utils.rs @@ -33,13 +33,12 @@ pub mod hash_size { }; } - use blake2::digest::consts::{U4, U8, U18}; + use blake2::digest::consts::{U4, U8}; hash_sizes! { /// Generic hash size for tests and debug Testing => U4, /// Account ID hash length AccountId => U8, - ConversationId => U18, } } diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs index 1769725..f0bc16a 100644 --- a/core/integration_tests_core/tests/mls_integration.rs +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -1,25 +1,7 @@ use std::ops::{Deref, DerefMut}; use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; -use libchat::{ChatStorage, ContentData, Context, ConversationId, GroupConvo, hex_trunc}; - -type TestContext = Context; - -fn send_and_verify( - sender: &mut TestContext, - receiver: &mut TestContext, - convo_id: ConversationId, - content: &[u8], -) { - let payloads = sender.send_content(convo_id, content).unwrap(); - let payload = payloads.first().unwrap(); - let received = receiver - .handle_payload(&payload.data) - .unwrap() - .expect("expected content"); - assert_eq!(content, received.data.as_slice()); - assert!(!received.is_new_convo); // Check that `is_new_convo` is FALSE -} +use libchat::{ContentData, Context, GroupConvo, hex_trunc}; // Simple client Functionality for testing struct Client { @@ -39,12 +21,16 @@ impl Client { } fn process_messages(&mut self) { - while let Some(data) = self.client_ctx().ds().poll() { + let messages: Vec<_> = { + let mut ds = self.ds(); + std::iter::from_fn(|| ds.poll()).collect() + }; + + for data in messages { let res = self.handle_payload(&data).unwrap(); if let Some(cb) = &self.on_content { - match res { - Some(content_data) => cb(content_data), - None => continue, + if let Some(content_data) = res { + cb(content_data); } } } @@ -53,7 +39,7 @@ impl Client { fn convo( &mut self, convo_id: &str, - ) -> Box> { + ) -> Box> { // TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync self.get_convo(convo_id).unwrap() } @@ -117,24 +103,16 @@ fn create_group() { clients[SARO] .convo(convo_id) - .send_content( - &mut clients[SARO].client_ctx(), - b"ok who broke the group chat again", - ) + .send_content(b"ok who broke the group chat again") .unwrap(); - // clients[SARO].process_messages(); process(&mut clients); clients[RAYA] .convo(convo_id) - .send_content( - &mut clients[RAYA].client_ctx(), - b"it was literally working five minutes ago", - ) + .send_content(b"it was literally working five minutes ago") .unwrap(); - // clients[SARO].process_messages(); process(&mut clients); let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); @@ -144,32 +122,22 @@ fn create_group() { let pax_id = clients[PAX].account_id().clone(); clients[SARO] .convo(convo_id) - .add_member(&mut clients[SARO].client_ctx(), &[&pax_id]) + .add_member(&[&pax_id]) .unwrap(); - // clients[SARO].process_messages(); process(&mut clients); clients[PAX] .convo(convo_id) - .send_content( - &mut clients[PAX].client_ctx(), - b"ngl the key rotation is cooked", - ) + .send_content(b"ngl the key rotation is cooked") .unwrap(); - // clients[SARO].process_messages(); - process(&mut clients); clients[SARO] .convo(convo_id) - .send_content( - &mut clients[SARO].client_ctx(), - b"bro we literally just added you to the group ", - ) + .send_content(b"bro we literally just added you to the group ") .unwrap(); process(&mut clients); - // process(&mut clients); } diff --git a/core/integration_tests_core/tests/private_integration.rs b/core/integration_tests_core/tests/private_integration.rs index b9eec0b..90d2a9b 100644 --- a/core/integration_tests_core/tests/private_integration.rs +++ b/core/integration_tests_core/tests/private_integration.rs @@ -1,11 +1,9 @@ -use std::ops::{Deref, DerefMut}; - -use libchat::{AddressedEnvelope, Context, Introduction}; +use libchat::{Context, Introduction}; use sqlite::{ChatStorage, StorageConfig}; use storage::{ConversationStore, IdentityStore}; use tempfile::tempdir; -use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; +use components::{EphemeralRegistry, LocalBroadcaster}; fn send_and_verify( sender: &mut Context, From 60e728576aba1e55353de381ef9f1341ed3fd5ad Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:00:40 -0700 Subject: [PATCH 10/39] Remove duplicate test_utils --- core/conversations/src/lib.rs | 3 - core/conversations/src/test_utils.rs | 299 --------------------------- 2 files changed, 302 deletions(-) delete mode 100644 core/conversations/src/test_utils.rs diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 9e53aa5..4f855f6 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -11,9 +11,6 @@ mod types; mod utils; pub use account::LogosAccount; -#[cfg(test)] -mod test_utils; - pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use conversation::GroupConvo; pub use errors::ChatError; diff --git a/core/conversations/src/test_utils.rs b/core/conversations/src/test_utils.rs deleted file mode 100644 index 9999735..0000000 --- a/core/conversations/src/test_utils.rs +++ /dev/null @@ -1,299 +0,0 @@ -use std::{ - cell::RefCell, - collections::{HashMap, HashSet, VecDeque}, - fmt::Debug, - rc::Rc, - sync::{Arc, Mutex}, -}; - -use storage::{ConversationMeta, ConversationStore, IdentityStore}; -use storage::{EphemeralKeyStore, RatchetStore}; - -use crate::{ - AccountId, AddressedEnvelope, DeliveryService, RegistrationService, - utils::{blake2b_hex, hash_size::Testing}, -}; - -#[derive(Debug)] -struct BroadcasterShared { - /// Per-address message queue; all published messages are appended here. - messages: VecDeque, - base_index: usize, -} - -impl BroadcasterShared { - pub fn read(&self, cursor: usize) -> Option<&T> { - self.messages.get(cursor + self.base_index) - } - - pub fn tail(&self) -> usize { - self.messages.len() + self.base_index - } -} - -#[derive(Clone, Debug)] -pub struct LocalBroadcaster { - shared: Rc>>, - cursor: usize, - subscriptions: HashSet, - outbound_msgs: Vec, -} - -impl LocalBroadcaster { - pub fn new() -> Self { - let shared = Rc::new(RefCell::new(BroadcasterShared { - messages: VecDeque::new(), - base_index: 0, - })); - - let cursor = shared.borrow().tail(); - Self { - shared, - cursor, - subscriptions: HashSet::new(), - outbound_msgs: Vec::new(), - } - } - - /// Returns a new consumer that shares the same message store but has its - /// own independent cursor — it starts from the beginning of each address - /// queue regardless of what any other consumer has already processed. - pub fn new_consumer(&self) -> Self { - let inner = self.shared.clone(); - let cursor = inner.borrow().tail(); - Self { - shared: inner, - cursor, - subscriptions: HashSet::new(), - outbound_msgs: Vec::new(), - } - } - - /// Pulls all messages this consumer has not yet seen on `address`, - /// applying any registered filter. Advances the cursor so the same - /// messages are not returned again. - pub fn poll(&mut self) -> Option> { - loop { - let next = self.cursor; - match self.shared.borrow().read(next) { - None => return None, - Some(ae) => { - self.cursor = next + 1; - if self.subscriptions.contains(ae.delivery_address.as_str()) - && self.is_inbound(ae) - { - return Some(ae.data.clone()); - } - } - } - } - } - - fn msg_id(msg: &AddressedEnvelope) -> String { - blake2b_hex::(&[msg.data.as_slice()]) - } - - fn is_inbound(&self, msg: &AddressedEnvelope) -> bool { - let mid = Self::msg_id(msg); - !self.outbound_msgs.contains(&mid) - } -} - -impl DeliveryService for LocalBroadcaster { - type Error = String; - - fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), Self::Error> { - self.outbound_msgs.push(Self::msg_id(&envelope)); - self.shared.borrow_mut().messages.push_back(envelope); - - Ok(()) - } - - fn subscribe(&mut self, delivery_address: &str) -> Result<(), Self::Error> { - // Strict temporal ordering of subscriptions is not enforced. - // Subscruptions are evaluated on polling, not when the message is published - self.subscriptions.insert(delivery_address.to_string()); - Ok(()) - } -} - -/// A Contact Registry used for Tests. -/// This implementation stores bundle bytes and then returns them when -/// retrieved -/// - -#[derive(Clone)] -pub struct EphemeralRegistry { - registry: Arc>>>, -} - -impl EphemeralRegistry { - pub fn new() -> Self { - Self { - registry: Arc::new(Mutex::new(HashMap::new())), - } - } -} - -impl Debug for EphemeralRegistry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let registry = self.registry.lock().unwrap(); - let truncated: Vec<(&String, String)> = registry - .iter() - .map(|(k, v)| { - let hex = if v.len() <= 8 { - hex::encode(v) - } else { - format!( - "{}..{}", - hex::encode(&v[..4]), - hex::encode(&v[v.len() - 4..]) - ) - }; - (k, hex) - }) - .collect(); - f.debug_struct("EphemeralRegistry") - .field("registry", &truncated) - .finish() - } -} - -impl RegistrationService for EphemeralRegistry { - type Error = String; - fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { - self.registry - .lock() - .unwrap() - .insert(identity.to_string(), key_bundle); - Ok(()) - } - - fn retrieve(&self, identity: &AccountId) -> Result>, Self::Error> { - Ok(self - .registry - .lock() - .unwrap() - .get(identity.as_str()) - .cloned()) - } -} - -pub struct MemStore { - convos: HashMap, -} - -impl MemStore { - pub fn new() -> Self { - Self { - convos: HashMap::new(), - } - } -} - -impl ConversationStore for MemStore { - fn save_conversation( - &mut self, - meta: &storage::ConversationMeta, - ) -> Result<(), storage::StorageError> { - self.convos - .insert(meta.local_convo_id.clone(), meta.clone()); - Ok(()) - } - - fn load_conversation( - &self, - local_convo_id: &str, - ) -> Result, storage::StorageError> { - let a = self.convos.get(local_convo_id).cloned(); - Ok(a) - } - - fn remove_conversation(&mut self, _local_convo_id: &str) -> Result<(), storage::StorageError> { - todo!() - } - - fn load_conversations(&self) -> Result, storage::StorageError> { - Ok(self.convos.values().cloned().collect()) - } - - fn has_conversation(&self, local_convo_id: &str) -> Result { - Ok(self.convos.contains_key(local_convo_id)) - } -} - -impl IdentityStore for MemStore { - fn load_identity(&self) -> Result, storage::StorageError> { - // todo!() - Ok(None) - } - - fn save_identity(&mut self, _identity: &crypto::Identity) -> Result<(), storage::StorageError> { - // todo!() - Ok(()) - } -} - -impl EphemeralKeyStore for MemStore { - fn save_ephemeral_key( - &mut self, - _public_key_hex: &str, - _private_key: &crypto::PrivateKey, - ) -> Result<(), storage::StorageError> { - todo!() - } - - fn load_ephemeral_key( - &self, - _public_key_hex: &str, - ) -> Result, storage::StorageError> { - todo!() - } - - fn remove_ephemeral_key(&mut self, _public_key_hex: &str) -> Result<(), storage::StorageError> { - todo!() - } -} - -impl RatchetStore for MemStore { - fn save_ratchet_state( - &mut self, - _conversation_id: &str, - _state: &storage::RatchetStateRecord, - _skipped_keys: &[storage::SkippedKeyRecord], - ) -> Result<(), storage::StorageError> { - todo!() - } - - fn load_ratchet_state( - &self, - _conversation_id: &str, - ) -> Result { - todo!() - } - - fn load_skipped_keys( - &self, - _conversation_id: &str, - ) -> Result, storage::StorageError> { - todo!() - } - - fn has_ratchet_state(&self, _conversation_id: &str) -> Result { - todo!() - } - - fn delete_ratchet_state( - &mut self, - _conversation_id: &str, - ) -> Result<(), storage::StorageError> { - todo!() - } - - fn cleanup_old_skipped_keys( - &mut self, - _max_age_secs: i64, - ) -> Result { - todo!() - } -} From da7e06e495684331b1ad67780a8f3f1089e3a815 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:03:49 -0700 Subject: [PATCH 11/39] Wrap constructor in result --- core/conversations/src/conversation/group_v1.rs | 16 ++++++++-------- core/conversations/src/inbox_v2.rs | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index f54c56d..b3e461f 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -78,7 +78,7 @@ where ctx: Rc>, ds: Rc>, keypkg_provider: Rc>, - ) -> Self { + ) -> Result { let config = Self::mls_create_config(); let mls_group = { let ctx_ref = ctx.borrow(); @@ -91,7 +91,7 @@ where .unwrap() }; let convo_id = hex::encode(mls_group.group_id().as_slice()); - Self::subscribe(&mut ds.borrow_mut(), &convo_id); + Self::subscribe(&mut ds.borrow_mut(), &convo_id)?; println!( "@ Create Convo: {}. {}. d:{} dc:{}", @@ -100,13 +100,13 @@ where Self::delivery_address_from_id(&convo_id), Self::ctrl_delivery_address_from_id(&convo_id) ); - Self { + Ok(Self { ctx, ds, keypkg_provider, mls_group, convo_id, - } + }) } pub fn new_from_welcome( @@ -114,7 +114,7 @@ where ds: Rc>, keypkg_provider: Rc>, welcome: Welcome, - ) -> Self { + ) -> Result { let mls_group = { let ctx_borrow = ctx.borrow(); let provider = ctx_borrow.provider(); @@ -128,7 +128,7 @@ where }; let convo_id = hex::encode(mls_group.group_id().as_slice()); - Self::subscribe(&mut *ds.borrow_mut(), &convo_id); + Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; println!( "@ Welcome Convo: I:{}. {}. d:{} dc:{}", @@ -138,13 +138,13 @@ where Self::ctrl_delivery_address_from_id(&convo_id) ); - GroupV1Convo { + Ok(Self { ctx, ds, keypkg_provider, mls_group, convo_id, - } + }) } pub fn load( diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 4aea37a..7413c22 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -141,8 +141,7 @@ where } pub fn create_group_v1(&self) -> Result, ChatError> { - let convo = GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone()); - Ok(convo) + GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone()) } pub fn handle_frame(&self, payload_bytes: &[u8]) -> Result<(), ChatError> { @@ -187,7 +186,7 @@ where self.ds.clone(), self.reg_service.clone(), welcome, - ); + )?; self.persist_convo(convo) } From 94a2904c0e9e50859beb3c5e5189670ecfefe838 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:06:19 -0700 Subject: [PATCH 12/39] Warning fixups --- Cargo.lock | 1 + Cargo.toml | 5 +++- core/conversations/src/context.rs | 3 +-- .../src/conversation/group_v1.rs | 4 +-- core/conversations/src/inbox_v2.rs | 4 +-- crates/client-ffi/src/delivery.rs | 6 +++++ crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 25 +++++++++++-------- crates/client/src/delivery_in_process.rs | 9 +++++-- crates/client/src/errors.rs | 2 +- 10 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5c8689..5a1c753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,6 +364,7 @@ name = "client" version = "0.1.0" dependencies = [ "chat-sqlite", + "components", "libchat", "tempfile", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 9fb2e90..3d6a969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,15 @@ default-members = [ "core/double-ratchets", "core/storage", "core/integration_tests_core", + "crates/client", + "crates/client-ffi", ] [workspace.dependencies] blake2 = "0.10" - crypto = { path = "core/crypto" } + crypto = { path = "core/crypto" } libchat = { path = "core/conversations" } + logoschat_components = {package="components", path ="extensions/components"} sqlite = { path = "core/sqlite"} storage = { path = "core/storage" } diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index fef0bf4..b9a79cd 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -78,7 +78,7 @@ where .map_err(ChatError::generic)?; Ok(Self { - identity: identity, + identity, ds, store, inbox, @@ -319,7 +319,6 @@ where } } - #[allow(unused)] // Temporary until GroupIntegration is completed fn load_group_convo( &mut self, convo_id: ConversationId, diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index b3e461f..e3e1fbd 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -172,9 +172,9 @@ where } fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { - ds.subscribe(&Self::delivery_address_from_id(&convo_id)) + ds.subscribe(&Self::delivery_address_from_id(convo_id)) .map_err(ChatError::generic)?; - ds.subscribe(&Self::ctrl_delivery_address_from_id(&convo_id)) + ds.subscribe(&Self::ctrl_delivery_address_from_id(convo_id)) .map_err(ChatError::generic)?; Ok(()) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 7413c22..c40ff6f 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -50,13 +50,13 @@ impl MlsContext for PqMlsContext { }; let envelope = EnvelopeV1 { - conversation_hint: ProtocolParams::conversation_id_for_account_id(&account_id), + conversation_hint: ProtocolParams::conversation_id_for_account_id(account_id), salt: 0, payload: frame.encode_to_vec().into(), }; let outbound_msg = AddressedEnvelope { - delivery_address: ProtocolParams::delivery_address_for_account_id(&account_id), + delivery_address: ProtocolParams::delivery_address_for_account_id(account_id), data: envelope.encode_to_vec(), }; diff --git a/crates/client-ffi/src/delivery.rs b/crates/client-ffi/src/delivery.rs index b58f7d2..a403c2d 100644 --- a/crates/client-ffi/src/delivery.rs +++ b/crates/client-ffi/src/delivery.rs @@ -14,6 +14,8 @@ pub type DeliverFn = Option< ) -> i32, >; +#[derive(Debug)] + pub struct CDelivery { pub callback: DeliverFn, } @@ -28,4 +30,8 @@ impl DeliveryService for CDelivery { let rc = unsafe { cb(addr.as_ptr(), addr.len(), data.as_ptr(), data.len()) }; if rc < 0 { Err(rc) } else { Ok(()) } } + + fn subscribe(&mut self, _delivery_address: &str) -> Result<(), Self::Error> { + todo!() + } } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 5aa4d4c..7bec801 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["rlib"] [dependencies] libchat = { workspace = true } +logoschat_components = { workspace = true} chat-sqlite = { path = "../../core/sqlite" } thiserror = "2" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index a51b397..9148cad 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,22 +1,23 @@ use libchat::{ - AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, RegistrationService - DeliveryService, Introduction, StorageConfig, + AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, + DeliveryService, Introduction, RegistrationService, StorageConfig, }; +use logoschat_components::EphemeralRegistry; + use crate::errors::ClientError; -pub struct ChatClient { - ctx: Context, - delivery: D, +pub struct ChatClient { + ctx: Context, } -impl ChatClient { +impl ChatClient { /// Create an in-memory, ephemeral client. Identity is lost on drop. pub fn new(name: impl Into, delivery: D) -> Self { + let registry = EphemeralRegistry::new(); let store = ChatStorage::in_memory(); Self { - ctx: Context::new_with_name(name, store), - delivery, + ctx: Context::new_with_name(name, delivery, registry, store).unwrap(), } } @@ -30,8 +31,9 @@ impl ChatClient { delivery: D, ) -> Result> { let store = ChatStorage::new(config).map_err(ChatError::from)?; - let ctx = Context::new_from_store(name, store)?; - Ok(Self { ctx, delivery }) + let registry = EphemeralRegistry::new(); + let ctx = Context::new_from_store(name, delivery, registry, store)?; + Ok(Self { ctx }) } /// Returns the installation name (identity label) of this client. @@ -86,7 +88,8 @@ impl ChatClient { envelopes: Vec, ) -> Result<(), ClientError> { for env in envelopes { - self.delivery.publish(env).map_err(ClientError::Delivery)?; + let mut delivery = self.ctx.ds(); + delivery.publish(env).map_err(ClientError::Delivery)?; } Ok(()) } diff --git a/crates/client/src/delivery_in_process.rs b/crates/client/src/delivery_in_process.rs index 6cceb25..3feff06 100644 --- a/crates/client/src/delivery_in_process.rs +++ b/crates/client/src/delivery_in_process.rs @@ -10,7 +10,7 @@ type Message = Vec; /// Messages are stored in an append-only log per delivery address. Readers hold /// independent [`Cursor`]s and advance their position without consuming messages, /// so multiple consumers on the same address each see every message. -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct MessageBus { log: Arc>>>, } @@ -80,7 +80,7 @@ impl Iterator for Cursor { /// clients can share one logical delivery service. Construct with a /// [`MessageBus`] and use [`cursor`](InProcessDelivery::cursor) / /// [`cursor_at_tail`](InProcessDelivery::cursor_at_tail) to read messages. -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct InProcessDelivery(MessageBus); impl InProcessDelivery { @@ -108,4 +108,9 @@ impl DeliveryService for InProcessDelivery { self.0.push(envelope.delivery_address, envelope.data); Ok(()) } + + fn subscribe(&mut self, _delivery_address: &str) -> Result<(), Self::Error> { + // TODO: (P1) implement subscribe + Ok(()) + } } diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index ff7ac27..322104c 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -1,7 +1,7 @@ use libchat::ChatError; #[derive(Debug, thiserror::Error)] -pub enum ClientError { +pub enum ClientError { #[error(transparent)] Chat(#[from] ChatError), /// Crypto state advanced but at least one envelope failed delivery. From 4737ae0f06dcd95d7a4c183e9093a864b58d1dcb Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:50:23 -0700 Subject: [PATCH 13/39] Appease clippy --- core/conversations/src/inbox_v2.rs | 2 +- crates/client/src/client.rs | 2 +- extensions/components/src/contact_registry.rs | 11 ++++++++++- .../components/src/delivery/local_broadcaster.rs | 6 ++++++ extensions/components/src/storage/in_memory_store.rs | 6 ++++++ 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index c40ff6f..1bcef35 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -87,7 +87,7 @@ pub struct InboxV2 { ctx: Rc>, } -impl<'a, DS, CS, RS> InboxV2 +impl InboxV2 where DS: DeliveryService, RS: RegistrationService, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 9148cad..97870f9 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,6 +1,6 @@ use libchat::{ AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, - DeliveryService, Introduction, RegistrationService, StorageConfig, + DeliveryService, Introduction, StorageConfig, }; use logoschat_components::EphemeralRegistry; diff --git a/extensions/components/src/contact_registry.rs b/extensions/components/src/contact_registry.rs index bd67775..2037cf6 100644 --- a/extensions/components/src/contact_registry.rs +++ b/extensions/components/src/contact_registry.rs @@ -24,6 +24,12 @@ impl EphemeralRegistry { } } +impl Default for EphemeralRegistry { + fn default() -> Self { + Self::new() + } +} + impl Debug for EphemeralRegistry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let registry = self.registry.lock().unwrap(); @@ -52,7 +58,10 @@ impl RegistrationService for EphemeralRegistry { type Error = String; fn register(&mut self, identity: &str, key_bundle: Vec) -> Result<(), Self::Error> { - self.registry.lock().unwrap().insert(identity.to_string(), key_bundle); + self.registry + .lock() + .unwrap() + .insert(identity.to_string(), key_bundle); Ok(()) } diff --git a/extensions/components/src/delivery/local_broadcaster.rs b/extensions/components/src/delivery/local_broadcaster.rs index a366ad2..5889def 100644 --- a/extensions/components/src/delivery/local_broadcaster.rs +++ b/extensions/components/src/delivery/local_broadcaster.rs @@ -97,6 +97,12 @@ impl LocalBroadcaster { } } +impl Default for LocalBroadcaster { + fn default() -> Self { + Self::new() + } +} + impl DeliveryService for LocalBroadcaster { type Error = String; diff --git a/extensions/components/src/storage/in_memory_store.rs b/extensions/components/src/storage/in_memory_store.rs index fcbc8b1..2bf84f9 100644 --- a/extensions/components/src/storage/in_memory_store.rs +++ b/extensions/components/src/storage/in_memory_store.rs @@ -22,6 +22,12 @@ impl MemStore { } } +impl Default for MemStore { + fn default() -> Self { + Self::new() + } +} + impl ConversationStore for MemStore { fn save_conversation( &mut self, From 06690ac2a667b64039f2b5aef0021f2a5cef8dee Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:00:25 -0700 Subject: [PATCH 14/39] Update comments --- core/conversations/src/context.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index b9a79cd..10c95d7 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -182,7 +182,8 @@ where &mut self, participants: &[&AccountId], ) -> Result>, ChatError> { - // TODO: (!) Perform this in InboxV2? + // TODO: (P1) Ensure errors are handled propertly. This is a high chance for desynchronized state. + // MlsGroup persistence, conversation persistence, and invite delivery all happen seperately let mut convo = self.pq_inbox.create_group_v1()?; self.store .borrow_mut() From 3fdddfb23317067711b9d35cd64c3820ae42d2fc Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:02:45 -0700 Subject: [PATCH 15/39] Update todo --- core/conversations/src/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index 10c95d7..a01b4f7 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -117,7 +117,7 @@ where store.clone(), ); - // TODO: (!) This seems weird here + // TODO: (P2) Initialize Account in Context or upper client. pq_inbox.register()?; ds.borrow_mut() From 61129d30c042fad7b6b66b2d3b5f559b8807be9f Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:52:26 -0700 Subject: [PATCH 16/39] Clean up warnings --- core/integration_tests_core/tests/mls_integration.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs index f0bc16a..03119e7 100644 --- a/core/integration_tests_core/tests/mls_integration.rs +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -28,10 +28,10 @@ impl Client { for data in messages { let res = self.handle_payload(&data).unwrap(); - if let Some(cb) = &self.on_content { - if let Some(content_data) = res { - cb(content_data); - } + if let Some(cb) = &self.on_content + && let Some(content_data) = res + { + cb(content_data); } } } @@ -62,11 +62,11 @@ impl DerefMut for Client { // Higher order function to handle printing fn pretty_print(prefix: impl Into) -> Box { let prefix = prefix.into(); - return Box::new(move |c: ContentData| { + Box::new(move |c: ContentData| { let cid = hex_trunc(c.conversation_id.as_bytes()); let content = String::from_utf8(c.data).unwrap(); println!("{} ({:?}) {}", prefix, cid, content) - }); + }) } fn process(clients: &mut Vec) { From e20b7983ed060c8eac19dfd3114f4848c2ecb191 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:27:04 -0700 Subject: [PATCH 17/39] Avoid panic --- crates/client-ffi/src/delivery.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/client-ffi/src/delivery.rs b/crates/client-ffi/src/delivery.rs index a403c2d..26f8c4c 100644 --- a/crates/client-ffi/src/delivery.rs +++ b/crates/client-ffi/src/delivery.rs @@ -32,6 +32,7 @@ impl DeliveryService for CDelivery { } fn subscribe(&mut self, _delivery_address: &str) -> Result<(), Self::Error> { - todo!() + // TODO: (P1) CDelivery does not support delivery_address filtering + Ok(()) } } From 0efbdeae6aaacc9d360e918b1385d8deb4dd6268 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:03:13 -0700 Subject: [PATCH 18/39] Fix libchat import in chat-cli --- Cargo.lock | 126 +++++++++++++++++++++++++++++++++------- bin/chat-cli/Cargo.toml | 3 +- 2 files changed, 108 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a1c753..bd44e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,7 +278,7 @@ dependencies = [ "arboard", "base64", "clap", - "client", + "client 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", "crossterm 0.29.0", "ratatui", "serde", @@ -300,14 +300,26 @@ dependencies = [ name = "chat-sqlite" version = "0.1.0" dependencies = [ - "crypto", + "crypto 0.1.0", "hex", "rusqlite", - "storage", + "storage 0.1.0", "tempfile", "zeroize", ] +[[package]] +name = "chat-sqlite" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "crypto 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "hex", + "rusqlite", + "storage 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "zeroize", +] + [[package]] name = "cipher" version = "0.4.4" @@ -363,19 +375,29 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" name = "client" version = "0.1.0" dependencies = [ - "chat-sqlite", + "chat-sqlite 0.1.0", "components", - "libchat", + "libchat 0.1.0", "tempfile", "thiserror", ] +[[package]] +name = "client" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "chat-sqlite 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "libchat 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "thiserror", +] + [[package]] name = "client-ffi" version = "0.1.0" dependencies = [ - "client", - "libchat", + "client 0.1.0", + "libchat 0.1.0", "safer-ffi", ] @@ -412,10 +434,10 @@ dependencies = [ name = "components" version = "0.1.0" dependencies = [ - "crypto", + "crypto 0.1.0", "hex", - "libchat", - "storage", + "libchat 0.1.0", + "storage 0.1.0", ] [[package]] @@ -551,6 +573,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "ed25519-dalek", + "generic-array 1.3.5", + "hkdf", + "rand_core 0.6.4", + "sha2", + "thiserror", + "x25519-dalek", + "xeddsa", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -725,18 +763,35 @@ version = "0.0.1" dependencies = [ "blake2", "chacha20poly1305", - "chat-sqlite", + "chat-sqlite 0.1.0", "hkdf", "rand 0.9.4", "rand_core 0.6.4", "serde", - "storage", + "storage 0.1.0", "tempfile", "thiserror", "x25519-dalek", "zeroize", ] +[[package]] +name = "double-ratchets" +version = "0.0.1" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "blake2", + "chacha20poly1305", + "hkdf", + "rand 0.9.4", + "rand_core 0.6.4", + "serde", + "storage 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "thiserror", + "x25519-dalek", + "zeroize", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -1328,10 +1383,10 @@ dependencies = [ name = "integration_tests_core" version = "0.1.0" dependencies = [ - "chat-sqlite", + "chat-sqlite 0.1.0", "components", - "libchat", - "storage", + "libchat 0.1.0", + "storage 0.1.0", "tempfile", ] @@ -1419,10 +1474,10 @@ dependencies = [ "base64", "blake2", "chat-proto", - "chat-sqlite", + "chat-sqlite 0.1.0", "components", - "crypto", - "double-ratchets", + "crypto 0.1.0", + "double-ratchets 0.0.1", "hex", "openmls", "openmls_libcrux_crypto 0.3.1", @@ -1430,12 +1485,34 @@ dependencies = [ "prost", "rand_core 0.6.4", "safer-ffi", - "storage", + "storage 0.1.0", "tempfile", "thiserror", "x25519-dalek", ] +[[package]] +name = "libchat" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "base64", + "blake2", + "chat-proto", + "chat-sqlite 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "crypto 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "double-ratchets 0.0.1 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "hex", + "openmls", + "openmls_traits 0.5.0", + "prost", + "rand_core 0.6.4", + "safer-ffi", + "storage 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", + "thiserror", + "x25519-dalek", +] + [[package]] name = "libcrux-aead" version = "0.0.6" @@ -2919,7 +2996,16 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" name = "storage" version = "0.1.0" dependencies = [ - "crypto", + "crypto 0.1.0", + "thiserror", +] + +[[package]] +name = "storage" +version = "0.1.0" +source = "git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa#39bf26756448dd16ddff89a6c0054f79236494aa" +dependencies = [ + "crypto 0.1.0 (git+https://github.com/logos-messaging/libchat?rev=39bf26756448dd16ddff89a6c0054f79236494aa)", "thiserror", ] diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index 8a7b784..3cfd90b 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -8,7 +8,8 @@ name = "chat-cli" path = "src/main.rs" [dependencies] -client = { path = "../../crates/client" } +# Reference a specific commit so updates to the Core does not break examples +client = { git = "https://github.com/logos-messaging/libchat", rev = "39bf26756448dd16ddff89a6c0054f79236494aa" } ratatui = "0.29" crossterm = "0.29" From ef360ae130d0400911bdca934d76bb8b6f5e8563 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:22:40 -0700 Subject: [PATCH 19/39] Add InboxV2 comment --- core/conversations/src/inbox_v2.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 1bcef35..5bc2b27 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -79,6 +79,9 @@ impl InboxProtocolParams { type ProtocolParams = InboxProtocolParams; +/// An PQ focused Conversation initializer. +/// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols +/// such as MLS. pub struct InboxV2 { account_id: AccountId, ds: Rc>, From 4675e121ae3ea0fe6faa5897e5a14b1b12167a7c Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:05:57 -0700 Subject: [PATCH 20/39] Add comments to GroupV1Convo --- core/conversations/src/conversation/group_v1.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index e3e1fbd..919c202 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -1,3 +1,7 @@ +/// GroupV1 is a conversationType which provides effecient handling of multiple participants +/// Properties: +/// - Harvest Now Decrypt Later (HNDL) protection provided by XWING +/// - Multiple use std::cell::RefCell; use std::rc::Rc; @@ -74,6 +78,7 @@ where DS: DeliveryService, KP: KeyPackageProvider, { + // Create a new conversation with the creator as the only participant. pub fn new( ctx: Rc>, ds: Rc>, @@ -109,6 +114,7 @@ where }) } + // Constructs a new conversation upon receiving a MlsWelcome message. pub fn new_from_welcome( ctx: Rc>, ds: Rc>, @@ -171,6 +177,7 @@ where }) } + // Configure the delivery service to listen for the required delivery addresses. fn subscribe(ds: &mut DS, convo_id: &str) -> Result<(), ChatError> { ds.subscribe(&Self::delivery_address_from_id(convo_id)) .map_err(ChatError::generic)?; @@ -307,7 +314,7 @@ where let provider = ctx_borrow.provider(); if protocol_message.epoch() < self.mls_group.epoch() { - // TODO: (!) Determine how to handle messages for old epochs. Minimally log this. + // TODO: (P1) Add logging for messages arriving from past epoch. return Ok(None); } From 78b085895c334f440e5f86b632ed554b5584f84a Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:52:09 -0700 Subject: [PATCH 21/39] Update doc comments --- core/conversations/src/service_traits.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/conversations/src/service_traits.rs b/core/conversations/src/service_traits.rs index 028a1c6..8e37253 100644 --- a/core/conversations/src/service_traits.rs +++ b/core/conversations/src/service_traits.rs @@ -1,3 +1,6 @@ +/// Service traits define the functionality which must be externally supplied by +/// platform clients. Platforms can alter the behaviour of the chat core by supplying +/// different implementations. use std::{fmt::Debug, fmt::Display}; use crate::types::{AccountId, AddressedEnvelope}; From 7ceb15b763f51e7444ef2dafe21183923667fb30 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:39:51 -0700 Subject: [PATCH 22/39] reduce visibility --- core/conversations/src/conversation/group_v1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 919c202..725b423 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -53,7 +53,7 @@ pub struct GroupV1Convo { ctx: Rc>, ds: Rc>, keypkg_provider: Rc>, - pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility + mls_group: MlsGroup, convo_id: String, } From ea06cf51867eb8407d21dec28beea0dff031f0d0 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:40:18 -0700 Subject: [PATCH 23/39] Doc Integration tests --- core/integration_tests_core/README.md | 12 ++++++++++++ core/integration_tests_core/src/lib.rs | 22 ---------------------- 2 files changed, 12 insertions(+), 22 deletions(-) create mode 100644 core/integration_tests_core/README.md diff --git a/core/integration_tests_core/README.md b/core/integration_tests_core/README.md new file mode 100644 index 0000000..d44b7de --- /dev/null +++ b/core/integration_tests_core/README.md @@ -0,0 +1,12 @@ +This crate is dedicated to backend integration tests. + +Tests can be built using any supplied service implementation. +Various implementations are available in the `Extensions/components` crate. + +## Running Tests + +Integration tests are executed when running `cargo test` from the workspace folder. + +Alternatively they can be executed from any crate, using + +`cargo test --package integration_tests_core` diff --git a/core/integration_tests_core/src/lib.rs b/core/integration_tests_core/src/lib.rs index 7d18d35..8b13789 100644 --- a/core/integration_tests_core/src/lib.rs +++ b/core/integration_tests_core/src/lib.rs @@ -1,23 +1 @@ -// use std::ops::{Deref, DerefMut}; -// use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; -// use libchat::{ -// AddressedEnvelope, ChatStorage, ContentData, Context, ConversationIdOwned, Introduction, -// StorageConfig, -// }; - -// fn send_and_verify( -// sender: &mut Context, -// receiver: &mut Context, -// convo_id: &str, -// content: &[u8], -// ) { -// let payloads = sender.send_content(convo_id, content).unwrap(); -// let payload = payloads.first().unwrap(); -// let received = receiver -// .handle_payload(&payload.data) -// .unwrap() -// .expect("expected content"); -// assert_eq!(content, received.data.as_slice()); -// assert!(!received.is_new_convo); -// } From fbb64408d19d74717a8fdeff239e9e1eb26cd5af Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Wed, 29 Apr 2026 07:18:14 -0700 Subject: [PATCH 24/39] Hashlen update --- core/conversations/src/inbox_v2.rs | 2 +- core/conversations/src/utils.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 5bc2b27..0efbc21 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -73,7 +73,7 @@ impl InboxProtocolParams { } fn conversation_id_for_account_id(account_id: &AccountId) -> String { - blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) } } diff --git a/core/conversations/src/utils.rs b/core/conversations/src/utils.rs index 3c81443..93eaf85 100644 --- a/core/conversations/src/utils.rs +++ b/core/conversations/src/utils.rs @@ -1,3 +1,4 @@ +use blake2::{Blake2b, Digest}; use std::time::{SystemTime, UNIX_EPOCH}; pub fn timestamp_millis() -> i64 { @@ -33,16 +34,15 @@ pub mod hash_size { }; } - use blake2::digest::consts::{U4, U8}; + use blake2::digest::consts::{U6, U8}; hash_sizes! { - /// Generic hash size for tests and debug - Testing => U4, /// Account ID hash length AccountId => U8, + /// Conversation ID hash length + ConvoId => U6, } } -use blake2::{Blake2b, Digest}; /// This establishes an easy to use wrapper for hashes in this crate. /// The output is formatted string of hex characters pub fn blake2b_hex(components: &[impl AsRef<[u8]>]) -> String { From 56f48c6a382c3182ca74489fd6d2fd4806a5bdcb Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:11:25 -0700 Subject: [PATCH 25/39] remove type alias for ProtocolParams --- core/conversations/src/inbox_v2.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 0efbc21..84a7f72 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -50,13 +50,13 @@ impl MlsContext for PqMlsContext { }; let envelope = EnvelopeV1 { - conversation_hint: ProtocolParams::conversation_id_for_account_id(account_id), + conversation_hint: InboxProtocolParams::conversation_id_for_account_id(account_id), salt: 0, payload: frame.encode_to_vec().into(), }; let outbound_msg = AddressedEnvelope { - delivery_address: ProtocolParams::delivery_address_for_account_id(account_id), + delivery_address: InboxProtocolParams::delivery_address_for_account_id(account_id), data: envelope.encode_to_vec(), }; @@ -77,8 +77,6 @@ impl InboxProtocolParams { } } -type ProtocolParams = InboxProtocolParams; - /// An PQ focused Conversation initializer. /// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols /// such as MLS. @@ -136,11 +134,11 @@ where } pub fn delivery_address(&self) -> String { - ProtocolParams::delivery_address_for_account_id(&self.account_id) + InboxProtocolParams::delivery_address_for_account_id(&self.account_id) } pub fn id(&self) -> String { - ProtocolParams::conversation_id_for_account_id(&self.account_id) + InboxProtocolParams::conversation_id_for_account_id(&self.account_id) } pub fn create_group_v1(&self) -> Result, ChatError> { From e4cebe79db7605c87ebab277d461ae7d19c24a71 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:58:50 -0700 Subject: [PATCH 26/39] Remove stray printlines --- .../src/conversation/group_v1.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index 725b423..fd15304 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -98,13 +98,6 @@ where let convo_id = hex::encode(mls_group.group_id().as_slice()); Self::subscribe(&mut ds.borrow_mut(), &convo_id)?; - println!( - "@ Create Convo: {}. {}. d:{} dc:{}", - ctx.borrow().ident().friendly_name(), - convo_id, - Self::delivery_address_from_id(&convo_id), - Self::ctrl_delivery_address_from_id(&convo_id) - ); Ok(Self { ctx, ds, @@ -136,14 +129,6 @@ where let convo_id = hex::encode(mls_group.group_id().as_slice()); Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; - println!( - "@ Welcome Convo: I:{}. {}. d:{} dc:{}", - ctx.borrow().ident().friendly_name(), - convo_id, - Self::delivery_address_from_id(&convo_id), - Self::ctrl_delivery_address_from_id(&convo_id) - ); - Ok(Self { ctx, ds, @@ -335,8 +320,8 @@ where .map_err(ChatError::generic)?; Ok(None) } - x => { - println!("Unhabled Message ttype {:?}", x); + _ => { + // TODO: (P2) Log unknown message type Ok(None) } } From 710c0bab1b3854c246549bb148067255364c71c5 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:02:19 -0700 Subject: [PATCH 27/39] Review fixes --- core/conversations/src/conversation/group_v1.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index fd15304..b7d8c87 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -343,11 +343,11 @@ where DS: DeliveryService, KP: KeyPackageProvider, { + // add_members returns: + // commit — the Commit message Alice broadcasts to all members + // welcome — the Welcome message sent privately to each new joiner + // _group_info — used for external joins; ignore for now fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> { - // add_members returns: - // commit — the Commit message Alice broadcasts to all members - // welcome — the Welcome message sent privately to each new joiner - // _group_info — used for external joins; ignore for now let ctx_ref = self.ctx.borrow(); let provider = ctx_ref.provider(); @@ -362,7 +362,6 @@ where // The account_id is kept so invites can be addressed properly let keypkgs = members .iter() - // .map(|ident| self.key_package_for_account(ctx, ident)) .map(|ident| self.key_package_for_account(ident)) .collect::, ChatError>>()?; From a8e83ccaa73fad83626a05646dc7daeafdcb15e4 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:11:04 -0700 Subject: [PATCH 28/39] PR review changes --- core/conversations/src/conversation/group_v1.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index b7d8c87..ff2629e 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -145,11 +145,9 @@ where convo_id: String, group_id: GroupId, ) -> Result { - let Some(mls_group) = MlsGroup::load(ctx.borrow().provider().storage(), &group_id) + let mls_group = MlsGroup::load(ctx.borrow().provider().storage(), &group_id) .map_err(ChatError::generic)? - else { - return Err(ChatError::NoConvo("mls group not found".into())); - }; + .ok_or_else(|| ChatError::NoConvo("mls group not found".into()))?; Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; From e56c9578c00e90c687fa9d583665ba40a9c5dd4d Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:44:29 -0700 Subject: [PATCH 29/39] Add trait comments --- core/conversations/src/conversation/group_v1.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index ff2629e..f41d785 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -22,11 +22,26 @@ use crate::{ types::{AddressedEncryptedPayload, ContentData}, }; +/// Provides the identity information needed to participate in an MLS group. +/// +/// Implementors must also implement [`OpenMlsSigner`] so they can sign MLS +/// messages. The two methods here supply what [`MlsContext::get_credential`] +/// needs to build a [`CredentialWithKey`]: `friendly_name` becomes the +/// `BasicCredential` label and `public_key` becomes the signature-verification key. pub trait IdentityProvider: OpenMlsSigner { fn friendly_name(&self) -> String; fn public_key(&self) -> &Ed25519VerifyingKey; } +/// Connects the MLS protocol engine to app-level identity and transport. +/// +/// `GroupV1Convo` is generic over this trait so the MLS logic stays +/// independent of how identities are stored or how invites are delivered. +/// Implementors supply: +/// - a [`LibcruxProvider`] for MLS crypto operations +/// - an [`IdentityProvider`] for signing and credential construction +/// - [`invite_user`] — the app-specific logic for routing a [`Welcome`] +/// message to a new member's inbox pub trait MlsContext { type IDENT: IdentityProvider; From a162729d3217d78c5ff111476907d0b276e4ebba Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:49:22 -0700 Subject: [PATCH 30/39] chat_proto import paths --- core/conversations/src/conversation/group_v1.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index f41d785..eadc441 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -272,13 +272,9 @@ where let a = AddressedEncryptedPayload { delivery_address: self.delivery_address(), data: EncryptedPayload { - encryption: Some( - chat_proto::logoschat::encryption::encrypted_payload::Encryption::Plaintext( - Plaintext { - payload: mls_message_out.to_bytes().unwrap().into(), - }, - ), - ), + encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext { + payload: mls_message_out.to_bytes().unwrap().into(), + })), }, }; @@ -289,10 +285,8 @@ where &mut self, encoded_payload: EncryptedPayload, ) -> Result, ChatError> { - use chat_proto::logoschat::encryption::encrypted_payload::Encryption; - let bytes = match encoded_payload.encryption { - Some(Encryption::Plaintext(pt)) => pt.payload, + Some(encrypted_payload::Encryption::Plaintext(pt)) => pt.payload, _ => { return Err(ChatError::ProtocolExpectation( "None", From df35abe19ad99850204bd452d6e2da6c3a99a2df Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 1 May 2026 10:33:51 -0700 Subject: [PATCH 31/39] PR Feedback fixes --- core/conversations/src/inbox_v2.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index 84a7f72..ef7b030 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -50,13 +50,13 @@ impl MlsContext for PqMlsContext { }; let envelope = EnvelopeV1 { - conversation_hint: InboxProtocolParams::conversation_id_for_account_id(account_id), + conversation_hint: conversation_id_for(account_id), salt: 0, payload: frame.encode_to_vec().into(), }; let outbound_msg = AddressedEnvelope { - delivery_address: InboxProtocolParams::delivery_address_for_account_id(account_id), + delivery_address: delivery_address_for(account_id), data: envelope.encode_to_vec(), }; @@ -65,16 +65,13 @@ impl MlsContext for PqMlsContext { } } -struct InboxProtocolParams {} +// Define unique Identifiers derivations used in InboxV2 +fn delivery_address_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) +} -impl InboxProtocolParams { - fn delivery_address_for_account_id(account_id: &AccountId) -> String { - blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) - } - - fn conversation_id_for_account_id(account_id: &AccountId) -> String { - blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) - } +fn conversation_id_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) } /// An PQ focused Conversation initializer. @@ -134,11 +131,11 @@ where } pub fn delivery_address(&self) -> String { - InboxProtocolParams::delivery_address_for_account_id(&self.account_id) + delivery_address_for(&self.account_id) } pub fn id(&self) -> String { - InboxProtocolParams::conversation_id_for_account_id(&self.account_id) + conversation_id_for(&self.account_id) } pub fn create_group_v1(&self) -> Result, ChatError> { From 8988fb0d26cc888470bcebcf65b97ba5625cea61 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 1 May 2026 12:24:47 -0700 Subject: [PATCH 32/39] Sort all Cargo.toml deps for less conflicts --- Cargo.toml | 23 +++++++++++++---------- bin/chat-cli/Cargo.toml | 16 ++++++++-------- core/conversations/Cargo.toml | 20 ++++++++++++-------- core/crypto/Cargo.toml | 13 +++++++------ core/double-ratchets/Cargo.toml | 20 ++++++++++++-------- core/integration_tests_core/Cargo.toml | 4 +++- core/sqlite/Cargo.toml | 4 +++- core/storage/Cargo.toml | 1 + crates/client-ffi/Cargo.toml | 7 +++++-- crates/client/Cargo.toml | 4 ++++ extensions/components/Cargo.toml | 6 ++++-- 11 files changed, 72 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d6a969..05efa50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,15 +3,15 @@ resolver = "3" members = [ - "core/sqlite", + "bin/chat-cli", "core/conversations", "core/crypto", "core/double-ratchets", - "core/storage", "core/integration_tests_core", - "crates/client", + "core/sqlite", + "core/storage", "crates/client-ffi", - "bin/chat-cli", + "crates/client", "extensions/components", ] @@ -27,12 +27,15 @@ default-members = [ ] [workspace.dependencies] - blake2 = "0.10" - crypto = { path = "core/crypto" } - libchat = { path = "core/conversations" } - logoschat_components = {package="components", path ="extensions/components"} - sqlite = { path = "core/sqlite"} - storage = { path = "core/storage" } +# Internal Workspace dependency declarations (sorted) +crypto = { path = "core/crypto" } +libchat = { path = "core/conversations" } +logoschat_components = {package = "components", path = "extensions/components"} +sqlite = { path = "core/sqlite" } +storage = { path = "core/storage" } + +# External Workspace dependency declarations (sorted) +blake2 = "0.10" # Panicking across FFI boundaries is UB; abort is the correct strategy for a # C FFI library. diff --git a/bin/chat-cli/Cargo.toml b/bin/chat-cli/Cargo.toml index 3cfd90b..efc9cf3 100644 --- a/bin/chat-cli/Cargo.toml +++ b/bin/chat-cli/Cargo.toml @@ -8,17 +8,17 @@ name = "chat-cli" path = "src/main.rs" [dependencies] -# Reference a specific commit so updates to the Core does not break examples -client = { git = "https://github.com/logos-messaging/libchat", rev = "39bf26756448dd16ddff89a6c0054f79236494aa" } - -ratatui = "0.29" -crossterm = "0.29" -clap = { version = "4", features = ["derive"] } +# External dependencies (sorted) anyhow = "1.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" arboard = "3" base64 = "0.22" +clap = { version = "4", features = ["derive"] } +# Reference a specific commit so updates to the Core does not break examples +client = { git = "https://github.com/logos-messaging/libchat", rev = "39bf26756448dd16ddff89a6c0054f79236494aa" } +crossterm = "0.29" +ratatui = "0.29" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 512cfa6..42d1f91 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -7,23 +7,27 @@ edition = "2024" crate-type = ["rlib","staticlib"] [dependencies] +# Workspace dependencies (sorted) +blake2 = { workspace = true } + +# External dependencies (sorted) base64 = "0.22" -sqlite = { package = "chat-sqlite", path = "../sqlite" } -blake2.workspace = true chat-proto = { git = "https://github.com/logos-messaging/chat_proto" } crypto = { path = "../crypto" } double-ratchets = { path = "../double-ratchets" } hex = "0.4.3" -prost = "0.14.1" -rand_core = { version = "0.6" } -safer-ffi = "0.1.13" -thiserror = "2.0.17" -x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } -storage = { path = "../storage" } openmls = { version = "0.8.1", features = ["libcrux-provider"] } openmls_libcrux_crypto = "0.3.1" openmls_traits = "0.5.0" +prost = "0.14.1" +rand_core = { version = "0.6" } +safer-ffi = "0.1.13" +sqlite = { package = "chat-sqlite", path = "../sqlite" } +storage = { path = "../storage" } +thiserror = "2.0.17" +x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } [dev-dependencies] +# External dependencies (sorted) components = { package = "components", path = "../../extensions/components" } tempfile = "3" diff --git a/core/crypto/Cargo.toml b/core/crypto/Cargo.toml index 04100aa..91df0f9 100644 --- a/core/crypto/Cargo.toml +++ b/core/crypto/Cargo.toml @@ -4,12 +4,13 @@ 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"] } +# External dependencies (sorted) ed25519-dalek = { version = "2.2.0", features = ["rand_core"] } -xeddsa = "1.0.2" -zeroize = {version = "1.8.2", features= ["derive"]} generic-array = "1.3.5" +hkdf = "0.12" +rand_core = { version = "0.6", features = ["getrandom"] } +sha2 = "0.10" thiserror = "2" +x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } +xeddsa = "1.0.2" +zeroize = { version = "1.8.2", features = ["derive"] } diff --git a/core/double-ratchets/Cargo.toml b/core/double-ratchets/Cargo.toml index 4d9ea08..ce9da38 100644 --- a/core/double-ratchets/Cargo.toml +++ b/core/double-ratchets/Cargo.toml @@ -7,17 +7,21 @@ edition = "2024" crate-type = ["rlib", "cdylib"] [dependencies] -x25519-dalek = { version="2.0.1", features=["static_secrets"] } -chacha20poly1305 = "0.10.1" -rand_core = "0.6.4" -rand = "0.9.3" -hkdf = "0.12.4" -thiserror = "2" -blake2 = "0.10.6" -zeroize = "1.8.2" +# Workspace dependencies (sorted) storage = { workspace = true } + +# External dependencies (sorted) +blake2 = "0.10.6" +chacha20poly1305 = "0.10.1" +hkdf = "0.12.4" +rand = "0.9.3" +rand_core = "0.6.4" serde = "1.0" +thiserror = "2" +x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } +zeroize = "1.8.2" [dev-dependencies] +# External dependencies (sorted) sqlite = { package = "chat-sqlite", path = "../sqlite" } tempfile = "3" diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml index 34ff420..46fa3ae 100644 --- a/core/integration_tests_core/Cargo.toml +++ b/core/integration_tests_core/Cargo.toml @@ -7,9 +7,11 @@ edition = "2024" # name = "integration_tests_core" [dev-dependencies] +# Workspace dependencies (sorted) libchat = { workspace = true } storage = { workspace = true } -sqlite = {package = "chat-sqlite", path ="../sqlite"} +# External dependencies (sorted) components = { path = "../../extensions/components" } +sqlite = { package = "chat-sqlite", path = "../sqlite" } tempfile = "3" diff --git a/core/sqlite/Cargo.toml b/core/sqlite/Cargo.toml index bd0e3ee..3e1727a 100644 --- a/core/sqlite/Cargo.toml +++ b/core/sqlite/Cargo.toml @@ -5,11 +5,13 @@ edition = "2024" description = "SQLite storage implementation for libchat" [dependencies] +# External dependencies (sorted) crypto = { path = "../crypto" } hex = "0.4.3" +rusqlite = { version = "0.35", features = ["bundled-sqlcipher-vendored-openssl"] } storage = { path = "../storage" } zeroize = { version = "1.8.2", features = ["derive"] } -rusqlite = { version = "0.35", features = ["bundled-sqlcipher-vendored-openssl"] } [dev-dependencies] +# External dependencies (sorted) tempfile = "3" diff --git a/core/storage/Cargo.toml b/core/storage/Cargo.toml index b176087..7357d39 100644 --- a/core/storage/Cargo.toml +++ b/core/storage/Cargo.toml @@ -5,5 +5,6 @@ edition = "2024" description = "Shared storage layer for libchat" [dependencies] +# External dependencies (sorted) crypto = { path = "../crypto" } thiserror = "2" diff --git a/crates/client-ffi/Cargo.toml b/crates/client-ffi/Cargo.toml index 43d8014..ea52119 100644 --- a/crates/client-ffi/Cargo.toml +++ b/crates/client-ffi/Cargo.toml @@ -11,9 +11,12 @@ name = "generate-headers" required-features = ["headers"] [dependencies] -safer-ffi = "0.1.13" -client = { path = "../client" } +# Workspace dependencies (sorted) libchat = { workspace = true } +# External dependencies (sorted) +client = { path = "../client" } +safer-ffi = "0.1.13" + [features] headers = ["safer-ffi/headers"] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 7bec801..4dc08f6 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -7,10 +7,14 @@ edition = "2024" crate-type = ["rlib"] [dependencies] +# Workspace dependencies (sorted) libchat = { workspace = true } logoschat_components = { workspace = true} + +# External dependencies (sorted) chat-sqlite = { path = "../../core/sqlite" } thiserror = "2" [dev-dependencies] +# External dependencies (sorted) tempfile = "3" diff --git a/extensions/components/Cargo.toml b/extensions/components/Cargo.toml index 0be31bc..055fad2 100644 --- a/extensions/components/Cargo.toml +++ b/extensions/components/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] +# Workspace dependencies (sorted) +crypto = { workspace = true } # Needed because Storage traits require "Identity" struct libchat = { workspace = true } storage = { workspace = true } -crypto = { workspace = true } # Needed because Storage traits require "Identity" struct -hex = "0.4.3" \ No newline at end of file +# External dependencies (sorted) +hex = "0.4.3" From 05e4697514158d4e17260f8285a284eec3d7f8cd Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 1 May 2026 13:25:35 -0700 Subject: [PATCH 33/39] Move relative path deps to workspace --- Cargo.toml | 6 ++++-- core/conversations/Cargo.toml | 10 ++++++---- core/double-ratchets/Cargo.toml | 4 +++- core/integration_tests_core/Cargo.toml | 4 ++-- core/sqlite/Cargo.toml | 6 ++++-- core/storage/Cargo.toml | 4 +++- crates/client/Cargo.toml | 2 +- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05efa50..509fb55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,12 @@ default-members = [ [workspace.dependencies] # Internal Workspace dependency declarations (sorted) +chat-sqlite = { path = "core/sqlite" } +components = { path = "extensions/components" } crypto = { path = "core/crypto" } libchat = { path = "core/conversations" } -logoschat_components = {package = "components", path = "extensions/components"} -sqlite = { path = "core/sqlite" } +logoschat_components = { package = "components", path = "extensions/components" } # TODO: Remove alias +sqlite = { package = "chat-sqlite", path = "core/sqlite" } # TODO: remove alias storage = { path = "core/storage" } # External Workspace dependency declarations (sorted) diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 42d1f91..f91ae8d 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -9,11 +9,13 @@ crate-type = ["rlib","staticlib"] [dependencies] # Workspace dependencies (sorted) blake2 = { workspace = true } +crypto = { workspace = true } +sqlite = { workspace = true } +storage = { workspace = true } # External dependencies (sorted) base64 = "0.22" chat-proto = { git = "https://github.com/logos-messaging/chat_proto" } -crypto = { path = "../crypto" } double-ratchets = { path = "../double-ratchets" } hex = "0.4.3" openmls = { version = "0.8.1", features = ["libcrux-provider"] } @@ -22,12 +24,12 @@ openmls_traits = "0.5.0" prost = "0.14.1" rand_core = { version = "0.6" } safer-ffi = "0.1.13" -sqlite = { package = "chat-sqlite", path = "../sqlite" } -storage = { path = "../storage" } thiserror = "2.0.17" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } [dev-dependencies] +# Workspace dependencies (sorted) +components = { workspace = true } + # External dependencies (sorted) -components = { package = "components", path = "../../extensions/components" } tempfile = "3" diff --git a/core/double-ratchets/Cargo.toml b/core/double-ratchets/Cargo.toml index ce9da38..e9d48e7 100644 --- a/core/double-ratchets/Cargo.toml +++ b/core/double-ratchets/Cargo.toml @@ -22,6 +22,8 @@ x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } zeroize = "1.8.2" [dev-dependencies] +# Workspace dependencies (sorted) +sqlite = { workspace = true } + # External dependencies (sorted) -sqlite = { package = "chat-sqlite", path = "../sqlite" } tempfile = "3" diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml index 46fa3ae..7b90b0a 100644 --- a/core/integration_tests_core/Cargo.toml +++ b/core/integration_tests_core/Cargo.toml @@ -8,10 +8,10 @@ edition = "2024" [dev-dependencies] # Workspace dependencies (sorted) +components = { workspace = true } libchat = { workspace = true } +sqlite = { workspace = true } storage = { workspace = true } # External dependencies (sorted) -components = { path = "../../extensions/components" } -sqlite = { package = "chat-sqlite", path = "../sqlite" } tempfile = "3" diff --git a/core/sqlite/Cargo.toml b/core/sqlite/Cargo.toml index 3e1727a..4fabf33 100644 --- a/core/sqlite/Cargo.toml +++ b/core/sqlite/Cargo.toml @@ -5,11 +5,13 @@ edition = "2024" description = "SQLite storage implementation for libchat" [dependencies] +# Workspace dependencies (sorted) +crypto = { workspace = true } +storage = { workspace = true } + # External dependencies (sorted) -crypto = { path = "../crypto" } hex = "0.4.3" rusqlite = { version = "0.35", features = ["bundled-sqlcipher-vendored-openssl"] } -storage = { path = "../storage" } zeroize = { version = "1.8.2", features = ["derive"] } [dev-dependencies] diff --git a/core/storage/Cargo.toml b/core/storage/Cargo.toml index 7357d39..0c0775c 100644 --- a/core/storage/Cargo.toml +++ b/core/storage/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" description = "Shared storage layer for libchat" [dependencies] +# Workspace dependencies (sorted) +crypto = { workspace = true } + # External dependencies (sorted) -crypto = { path = "../crypto" } thiserror = "2" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 4dc08f6..eea2e3b 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -8,11 +8,11 @@ crate-type = ["rlib"] [dependencies] # Workspace dependencies (sorted) +chat-sqlite = { workspace = true } libchat = { workspace = true } logoschat_components = { workspace = true} # External dependencies (sorted) -chat-sqlite = { path = "../../core/sqlite" } thiserror = "2" [dev-dependencies] From b64abc16187ea6bed102a650ea267128a0046ad3 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Sat, 2 May 2026 09:49:16 -0700 Subject: [PATCH 34/39] Standardize workspace imports --- Cargo.toml | 2 -- core/conversations/Cargo.toml | 2 +- core/conversations/src/conversation/privatev1.rs | 2 +- core/conversations/src/inbox/handler.rs | 2 +- core/conversations/src/lib.rs | 4 ++-- core/double-ratchets/Cargo.toml | 2 +- core/double-ratchets/examples/out_of_order_demo.rs | 2 +- core/double-ratchets/examples/storage_demo.rs | 2 +- core/double-ratchets/src/storage/session.rs | 2 +- core/integration_tests_core/Cargo.toml | 2 +- core/integration_tests_core/tests/private_integration.rs | 2 +- crates/client/Cargo.toml | 2 +- crates/client/src/client.rs | 2 +- 13 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 509fb55..ed76dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,6 @@ chat-sqlite = { path = "core/sqlite" } components = { path = "extensions/components" } crypto = { path = "core/crypto" } libchat = { path = "core/conversations" } -logoschat_components = { package = "components", path = "extensions/components" } # TODO: Remove alias -sqlite = { package = "chat-sqlite", path = "core/sqlite" } # TODO: remove alias storage = { path = "core/storage" } # External Workspace dependency declarations (sorted) diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index f91ae8d..eff4823 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -9,8 +9,8 @@ crate-type = ["rlib","staticlib"] [dependencies] # Workspace dependencies (sorted) blake2 = { workspace = true } +chat-sqlite = { workspace = true } crypto = { workspace = true } -sqlite = { workspace = true } storage = { workspace = true } # External dependencies (sorted) diff --git a/core/conversations/src/conversation/privatev1.rs b/core/conversations/src/conversation/privatev1.rs index b7736d8..f4f39e5 100644 --- a/core/conversations/src/conversation/privatev1.rs +++ b/core/conversations/src/conversation/privatev1.rs @@ -281,8 +281,8 @@ impl Debug for PrivateV1Convo { #[cfg(test)] mod tests { + use chat_sqlite::{ChatStorage, StorageConfig}; use crypto::PrivateKey; - use sqlite::{ChatStorage, StorageConfig}; use super::*; diff --git a/core/conversations/src/inbox/handler.rs b/core/conversations/src/inbox/handler.rs index 9b90ac3..ca02240 100644 --- a/core/conversations/src/inbox/handler.rs +++ b/core/conversations/src/inbox/handler.rs @@ -260,7 +260,7 @@ mod tests { use std::cell::RefCell; use super::*; - use sqlite::{ChatStorage, StorageConfig}; + use chat_sqlite::{ChatStorage, StorageConfig}; #[test] fn test_invite_privatev1_roundtrip() { diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 4f855f6..b48e521 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -11,11 +11,11 @@ mod types; mod utils; pub use account::LogosAccount; +pub use chat_sqlite::ChatStorage; +pub use chat_sqlite::StorageConfig; pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use conversation::GroupConvo; pub use errors::ChatError; pub use service_traits::{DeliveryService, RegistrationService}; -pub use sqlite::ChatStorage; -pub use sqlite::StorageConfig; pub use types::{AccountId, AddressedEnvelope, ContentData}; pub use utils::hex_trunc; diff --git a/core/double-ratchets/Cargo.toml b/core/double-ratchets/Cargo.toml index e9d48e7..ded93b4 100644 --- a/core/double-ratchets/Cargo.toml +++ b/core/double-ratchets/Cargo.toml @@ -23,7 +23,7 @@ zeroize = "1.8.2" [dev-dependencies] # Workspace dependencies (sorted) -sqlite = { workspace = true } +chat-sqlite = { workspace = true } # External dependencies (sorted) tempfile = "3" diff --git a/core/double-ratchets/examples/out_of_order_demo.rs b/core/double-ratchets/examples/out_of_order_demo.rs index b01de57..beef652 100644 --- a/core/double-ratchets/examples/out_of_order_demo.rs +++ b/core/double-ratchets/examples/out_of_order_demo.rs @@ -2,8 +2,8 @@ //! //! Run with: cargo run --example out_of_order_demo -p double-ratchets +use chat_sqlite::{ChatStorage, StorageConfig}; use double_ratchets::{InstallationKeyPair, RatchetSession}; -use sqlite::{ChatStorage, StorageConfig}; use tempfile::NamedTempFile; fn main() { diff --git a/core/double-ratchets/examples/storage_demo.rs b/core/double-ratchets/examples/storage_demo.rs index 258d655..5222538 100644 --- a/core/double-ratchets/examples/storage_demo.rs +++ b/core/double-ratchets/examples/storage_demo.rs @@ -2,8 +2,8 @@ //! //! Run with: cargo run --example storage_demo -p double-ratchets +use chat_sqlite::{ChatStorage, StorageConfig}; use double_ratchets::{InstallationKeyPair, RatchetSession}; -use sqlite::{ChatStorage, StorageConfig}; use tempfile::NamedTempFile; fn main() { diff --git a/core/double-ratchets/src/storage/session.rs b/core/double-ratchets/src/storage/session.rs index 069ba4d..99ea7f5 100644 --- a/core/double-ratchets/src/storage/session.rs +++ b/core/double-ratchets/src/storage/session.rs @@ -168,7 +168,7 @@ fn save_state( mod tests { use super::*; use crate::hkdf::DefaultDomain; - use sqlite::ChatStorage; + use chat_sqlite::ChatStorage; fn create_test_storage() -> ChatStorage { ChatStorage::in_memory() diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml index 7b90b0a..dec26c4 100644 --- a/core/integration_tests_core/Cargo.toml +++ b/core/integration_tests_core/Cargo.toml @@ -10,7 +10,7 @@ edition = "2024" # Workspace dependencies (sorted) components = { workspace = true } libchat = { workspace = true } -sqlite = { workspace = true } +chat-sqlite = { workspace = true } storage = { workspace = true } # External dependencies (sorted) diff --git a/core/integration_tests_core/tests/private_integration.rs b/core/integration_tests_core/tests/private_integration.rs index 90d2a9b..165ba76 100644 --- a/core/integration_tests_core/tests/private_integration.rs +++ b/core/integration_tests_core/tests/private_integration.rs @@ -1,5 +1,5 @@ +use chat_sqlite::{ChatStorage, StorageConfig}; use libchat::{Context, Introduction}; -use sqlite::{ChatStorage, StorageConfig}; use storage::{ConversationStore, IdentityStore}; use tempfile::tempdir; diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index eea2e3b..a8fda67 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -9,8 +9,8 @@ crate-type = ["rlib"] [dependencies] # Workspace dependencies (sorted) chat-sqlite = { workspace = true } +components = { workspace = true} libchat = { workspace = true } -logoschat_components = { workspace = true} # External dependencies (sorted) thiserror = "2" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 97870f9..ad63606 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -3,7 +3,7 @@ use libchat::{ DeliveryService, Introduction, StorageConfig, }; -use logoschat_components::EphemeralRegistry; +use components::EphemeralRegistry; use crate::errors::ClientError; From e8ffc9d48e1d7d1aa7497805d1b80764203c4f4c Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Fri, 1 May 2026 14:41:04 -0700 Subject: [PATCH 35/39] Remove naming conflict with Signatures --- core/conversations/src/inbox/handshake.rs | 2 +- core/conversations/src/inbox/introduction.rs | 12 ++++++------ core/crypto/src/lib.rs | 4 ++-- core/crypto/src/x3dh.rs | 8 ++++---- core/crypto/src/xeddsa_sign.rs | 14 +++++++------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/core/conversations/src/inbox/handshake.rs b/core/conversations/src/inbox/handshake.rs index 8a93a5a..b92667d 100644 --- a/core/conversations/src/inbox/handshake.rs +++ b/core/conversations/src/inbox/handshake.rs @@ -97,7 +97,7 @@ mod tests { let bob_bundle = PrekeyBundle { identity_key: PublicKey::from(&bob_identity), signed_prekey: bob_signed_prekey_pub, - signature: crypto::Ed25519Signature([0u8; 64]), + signature: crypto::XedDsaSignature([0u8; 64]), onetime_prekey: None, }; diff --git a/core/conversations/src/inbox/introduction.rs b/core/conversations/src/inbox/introduction.rs index 9f6f5c0..d326e4e 100644 --- a/core/conversations/src/inbox/introduction.rs +++ b/core/conversations/src/inbox/introduction.rs @@ -1,6 +1,6 @@ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use chat_proto::logoschat::intro::IntroBundle; -use crypto::{Ed25519Signature, PrivateKey, PublicKey}; +use crypto::{PrivateKey, PublicKey, XedDsaSignature}; use prost::Message; use rand_core::{CryptoRng, RngCore}; @@ -19,7 +19,7 @@ pub(crate) fn sign_intro_binding( secret: &PrivateKey, ephemeral: &PublicKey, rng: R, -) -> Ed25519Signature { +) -> XedDsaSignature { let message = intro_binding_message(ephemeral); crypto::xeddsa_sign(secret, &message, rng) } @@ -27,7 +27,7 @@ pub(crate) fn sign_intro_binding( pub(crate) fn verify_intro_binding( pubkey: &PublicKey, ephemeral: &PublicKey, - signature: &Ed25519Signature, + signature: &XedDsaSignature, ) -> Result<(), crypto::SignatureError> { let message = intro_binding_message(ephemeral); crypto::xeddsa_verify(pubkey, &message, signature) @@ -37,7 +37,7 @@ pub(crate) fn verify_intro_binding( pub struct Introduction { installation_key: PublicKey, ephemeral_key: PublicKey, - signature: Ed25519Signature, + signature: XedDsaSignature, } impl Introduction { @@ -64,7 +64,7 @@ impl Introduction { &self.ephemeral_key } - pub fn signature(&self) -> &Ed25519Signature { + pub fn signature(&self) -> &XedDsaSignature { &self.signature } } @@ -127,7 +127,7 @@ impl TryFrom<&[u8]> for Introduction { let installation_key = PublicKey::from(installation_bytes); let ephemeral_key = PublicKey::from(ephemeral_bytes); - let signature = Ed25519Signature(signature_bytes); + let signature = XedDsaSignature::from(signature_bytes); verify_intro_binding(&installation_key, &ephemeral_key, &signature) .map_err(|_| ChatError::BadBundleValue("invalid signature".into()))?; diff --git a/core/crypto/src/lib.rs b/core/crypto/src/lib.rs index 1759091..e1d71c6 100644 --- a/core/crypto/src/lib.rs +++ b/core/crypto/src/lib.rs @@ -6,6 +6,6 @@ mod xeddsa_sign; pub use identity::Identity; pub use keys::{PrivateKey, PublicKey, SymmetricKey32}; -pub use signatures::{Ed25519SigningKey, Ed25519VerifyingKey}; +pub use signatures::{Ed25519Signature, Ed25519SigningKey, Ed25519VerifyingKey}; pub use x3dh::{DomainSeparator, PrekeyBundle, X3Handshake}; -pub use xeddsa_sign::{Ed25519Signature, SignatureError, xeddsa_sign, xeddsa_verify}; +pub use xeddsa_sign::{SignatureError, XedDsaSignature, xeddsa_sign, xeddsa_verify}; diff --git a/core/crypto/src/x3dh.rs b/core/crypto/src/x3dh.rs index b5a9a73..7e921b7 100644 --- a/core/crypto/src/x3dh.rs +++ b/core/crypto/src/x3dh.rs @@ -5,14 +5,14 @@ use rand_core::{CryptoRng, RngCore}; use sha2::Sha256; use crate::keys::{PrivateKey, PublicKey, SymmetricKey32}; -use crate::xeddsa_sign::Ed25519Signature; +use crate::xeddsa_sign::XedDsaSignature; /// 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: Ed25519Signature, + pub signature: XedDsaSignature, pub onetime_prekey: Option, } @@ -151,7 +151,7 @@ mod tests { let bob_bundle = PrekeyBundle { identity_key: bob_identity_pub, signed_prekey: bob_signed_prekey_pub, - signature: Ed25519Signature::empty(), + signature: XedDsaSignature::empty(), onetime_prekey: Some(bob_onetime_prekey_pub), }; @@ -191,7 +191,7 @@ mod tests { let bob_bundle = PrekeyBundle { identity_key: bob_identity_pub, signed_prekey: bob_signed_prekey_pub, - signature: Ed25519Signature::empty(), + signature: XedDsaSignature::empty(), onetime_prekey: None, }; diff --git a/core/crypto/src/xeddsa_sign.rs b/core/crypto/src/xeddsa_sign.rs index 20dd3c0..e027131 100644 --- a/core/crypto/src/xeddsa_sign.rs +++ b/core/crypto/src/xeddsa_sign.rs @@ -9,21 +9,21 @@ use xeddsa::{Sign, Verify, xed25519}; use crate::{PrivateKey, PublicKey}; /// A 64-byte XEdDSA signature over an Ed25519-compatible curve. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Ed25519Signature(pub [u8; 64]); +pub struct XedDsaSignature(pub [u8; 64]); -impl Ed25519Signature { +impl XedDsaSignature { pub fn empty() -> Self { Self([0u8; 64]) } } -impl AsRef<[u8; 64]> for Ed25519Signature { +impl AsRef<[u8; 64]> for XedDsaSignature { fn as_ref(&self) -> &[u8; 64] { &self.0 } } -impl From<[u8; 64]> for Ed25519Signature { +impl From<[u8; 64]> for XedDsaSignature { fn from(bytes: [u8; 64]) -> Self { Self(bytes) } @@ -47,9 +47,9 @@ pub fn xeddsa_sign( secret: &PrivateKey, message: &[u8], mut rng: R, -) -> Ed25519Signature { +) -> XedDsaSignature { let signing_key = xed25519::PrivateKey::from(secret); - Ed25519Signature(signing_key.sign(message, &mut rng)) + XedDsaSignature(signing_key.sign(message, &mut rng)) } /// Verify an XEdDSA signature using an X25519 public key. @@ -64,7 +64,7 @@ pub fn xeddsa_sign( pub fn xeddsa_verify( pubkey: &PublicKey, message: &[u8], - signature: &Ed25519Signature, + signature: &XedDsaSignature, ) -> Result<(), SignatureError> { let verify_key = xed25519::PublicKey::from(pubkey); verify_key From 6ba8bbc9ce8fd2ec84150cff5d9db9f9bbe24ce9 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Sat, 2 May 2026 09:39:48 -0700 Subject: [PATCH 36/39] Make conversations generic over IdentityProvider --- Cargo.lock | 11 ++ Cargo.toml | 3 + core/account/Cargo.toml | 14 ++ core/account/src/account.rs | 43 +++++ core/account/src/lib.rs | 5 + core/conversations/Cargo.toml | 1 + core/conversations/src/account.rs | 53 ------ core/conversations/src/context.rs | 32 ++-- core/conversations/src/conversation.rs | 2 +- .../src/conversation/group_v1.rs | 148 ++++++--------- core/conversations/src/inbox_v2.rs | 174 +++++++++++++----- core/conversations/src/lib.rs | 4 +- core/conversations/src/service_traits.rs | 11 ++ core/integration_tests_core/Cargo.toml | 3 +- .../tests/mls_integration.rs | 33 +++- .../tests/private_integration.rs | 105 +++++++---- crates/client/Cargo.toml | 3 +- crates/client/src/client.rs | 30 ++- crates/client/tests/saro_and_raya.rs | 7 +- 19 files changed, 418 insertions(+), 264 deletions(-) create mode 100644 core/account/Cargo.toml create mode 100644 core/account/src/account.rs create mode 100644 core/account/src/lib.rs delete mode 100644 core/conversations/src/account.rs diff --git a/Cargo.lock b/Cargo.lock index bd44e97..a927f72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,7 @@ dependencies = [ "chat-sqlite 0.1.0", "components", "libchat 0.1.0", + "logos-account", "tempfile", "thiserror", ] @@ -1386,6 +1387,7 @@ dependencies = [ "chat-sqlite 0.1.0", "components", "libchat 0.1.0", + "logos-account", "storage 0.1.0", "tempfile", ] @@ -1481,6 +1483,7 @@ dependencies = [ "hex", "openmls", "openmls_libcrux_crypto 0.3.1", + "openmls_memory_storage 0.5.0", "openmls_traits 0.5.0", "prost", "rand_core 0.6.4", @@ -1821,6 +1824,14 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos-account" +version = "0.1.0" +dependencies = [ + "crypto 0.1.0", + "libchat 0.1.0", +] + [[package]] name = "lru" version = "0.12.5" diff --git a/Cargo.toml b/Cargo.toml index ed76dd1..bb151df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "3" members = [ "bin/chat-cli", + "core/account", "core/conversations", "core/crypto", "core/double-ratchets", @@ -16,6 +17,7 @@ members = [ ] default-members = [ + "core/account", "core/sqlite", "core/conversations", "core/crypto", @@ -32,6 +34,7 @@ chat-sqlite = { path = "core/sqlite" } components = { path = "extensions/components" } crypto = { path = "core/crypto" } libchat = { path = "core/conversations" } +logos-account = { path = "core/account" } storage = { path = "core/storage" } # External Workspace dependency declarations (sorted) diff --git a/core/account/Cargo.toml b/core/account/Cargo.toml new file mode 100644 index 0000000..5162bf2 --- /dev/null +++ b/core/account/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "logos-account" +version = "0.1.0" +edition = "2024" + +[features] +dev = [] + +[dependencies] +# Workspace dependencies (sorted) +crypto = { workspace = true } +libchat = { workspace = true } + +# External dependencies (sorted) diff --git a/core/account/src/account.rs b/core/account/src/account.rs new file mode 100644 index 0000000..6705cfd --- /dev/null +++ b/core/account/src/account.rs @@ -0,0 +1,43 @@ +use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; + +use libchat::{AccountId, IdentityProvider}; + +/// Logos Account represents a single account across +/// multiple installations and services. +pub struct TestLogosAccount { + id: AccountId, + signing_key: Ed25519SigningKey, + verifying_key: Ed25519VerifyingKey, +} + +/// A Test Focused LogosAccount using a pre-defined identifier. +/// The test account is not persisted, and uses a single user provided id +impl TestLogosAccount { + pub fn new(explicit_id: impl Into) -> Self { + let signing_key = Ed25519SigningKey::generate(); + let verifying_key = signing_key.verifying_key(); + Self { + id: AccountId::new(explicit_id.into()), + signing_key, + verifying_key, + } + } +} + +impl IdentityProvider for TestLogosAccount { + fn account_id(&self) -> &AccountId { + &self.id + } + + fn friendly_name(&self) -> String { + self.id.to_string() + } + + fn public_key(&self) -> &Ed25519VerifyingKey { + &self.verifying_key + } + + fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature { + self.signing_key.sign(payload) + } +} diff --git a/core/account/src/lib.rs b/core/account/src/lib.rs new file mode 100644 index 0000000..c33c296 --- /dev/null +++ b/core/account/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "dev")] +mod account; + +#[cfg(feature = "dev")] +pub use account::TestLogosAccount; diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index eff4823..dd3c732 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -20,6 +20,7 @@ double-ratchets = { path = "../double-ratchets" } hex = "0.4.3" openmls = { version = "0.8.1", features = ["libcrux-provider"] } openmls_libcrux_crypto = "0.3.1" +openmls_memory_storage = "0.5.0" openmls_traits = "0.5.0" prost = "0.14.1" rand_core = { version = "0.6" } diff --git a/core/conversations/src/account.rs b/core/conversations/src/account.rs deleted file mode 100644 index 161710f..0000000 --- a/core/conversations/src/account.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; -use openmls::prelude::SignatureScheme; -use openmls_traits::signatures::Signer; - -use crate::{conversation::IdentityProvider, types::AccountId}; - -/// Logos Account represents a single account across -/// multiple installations and services. -pub struct LogosAccount { - id: AccountId, - signing_key: Ed25519SigningKey, - verifying_key: Ed25519VerifyingKey, -} - -impl LogosAccount { - /// Create a test LogosAccount using a pre-defined identifier. - /// This should only be used during MLS integration. Not suitable for production use. - /// TODO: (P1) Remove once implementation is ready. - pub fn new_test(explicit_id: impl Into) -> Self { - let signing_key = Ed25519SigningKey::generate(); - let verifying_key = signing_key.verifying_key(); - Self { - id: AccountId::new(explicit_id.into()), - signing_key, - verifying_key, - } - } - - pub fn account_id(&self) -> &AccountId { - &self.id - } -} - -impl Signer for LogosAccount { - // TODO: (P2) Remove OpenMLS dependency to make accounts more portable - fn sign(&self, payload: &[u8]) -> Result, openmls_traits::signatures::SignerError> { - Ok(self.signing_key.sign(payload).as_ref().to_vec()) - } - - fn signature_scheme(&self) -> SignatureScheme { - SignatureScheme::ED25519 - } -} - -impl IdentityProvider for LogosAccount { - fn friendly_name(&self) -> String { - self.id.to_string() - } - - fn public_key(&self) -> &Ed25519VerifyingKey { - &self.verifying_key - } -} diff --git a/core/conversations/src/context.rs b/core/conversations/src/context.rs index a01b4f7..3c538be 100644 --- a/core/conversations/src/context.rs +++ b/core/conversations/src/context.rs @@ -2,10 +2,9 @@ use std::cell::{Ref, RefMut}; use std::sync::Arc; use std::{cell::RefCell, rc::Rc}; -use crate::account::LogosAccount; use crate::conversation::{Convo, GroupConvo}; -use crate::{DeliveryService, RegistrationService}; +use crate::{DeliveryService, IdentityProvider, RegistrationService}; use crate::{ conversation::{Conversation, Id, PrivateV1Convo}, errors::ChatError, @@ -22,16 +21,22 @@ pub use crate::inbox::Introduction; // This is the main entry point to the conversations api. // Ctx manages lifetimes of objects to process and generate payloads. -pub struct Context { +pub struct Context< + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, + CS: ChatStore, +> { identity: Rc, ds: Rc>, store: Rc>, inbox: Inbox, - pq_inbox: InboxV2, + pq_inbox: InboxV2, } -impl Context +impl Context where + IP: IdentityProvider + 'static, DS: DeliveryService + 'static, RS: RegistrationService + 'static, CS: ChatStore + 'static, @@ -42,6 +47,7 @@ where /// Otherwise, a new identity will be created with the given name and saved. pub fn new_from_store( name: impl Into, + account: IP, delivery: DS, registration: RS, store: CS, @@ -65,12 +71,7 @@ where let identity = Rc::new(identity); let inbox = Inbox::new(Rc::clone(&store), Rc::clone(&identity)); - let pq_inbox = InboxV2::new( - LogosAccount::new_test(name), - ds.clone(), - contact_registry.clone(), - store.clone(), - ); + let pq_inbox = InboxV2::new(account, ds.clone(), contact_registry.clone(), store.clone()); // Subscribe ds.borrow_mut() @@ -91,6 +92,7 @@ where /// Uses in-memory SQLite database. Each call creates a new isolated database. pub fn new_with_name( name: impl Into, + account: IP, delivery: DS, registration: RS, chat_store: CS, @@ -110,12 +112,8 @@ where let identity = Rc::new(identity); let inbox = Inbox::new(store.clone(), Rc::clone(&identity)); - let mut pq_inbox = InboxV2::new( - LogosAccount::new_test(name), - ds.clone(), - contact_registry.clone(), - store.clone(), - ); + let mut pq_inbox = + InboxV2::new(account, ds.clone(), contact_registry.clone(), store.clone()); // TODO: (P2) Initialize Account in Context or upper client. pq_inbox.register()?; diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 702ca93..94b5ff7 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use storage::{ConversationKind, ConversationStore, RatchetStore}; pub use crate::errors::ChatError; -pub use group_v1::{GroupV1Convo, IdentityProvider}; +pub use group_v1::GroupV1Convo; pub use privatev1::PrivateV1Convo; pub type ConversationId<'a> = &'a str; diff --git a/core/conversations/src/conversation/group_v1.rs b/core/conversations/src/conversation/group_v1.rs index eadc441..cc0aba7 100644 --- a/core/conversations/src/conversation/group_v1.rs +++ b/core/conversations/src/conversation/group_v1.rs @@ -7,114 +7,72 @@ use std::rc::Rc; use blake2::{Blake2b, Digest, digest::consts::U6}; use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; -use crypto::Ed25519VerifyingKey; use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::*; -use openmls_libcrux_crypto::Provider as LibcruxProvider; -use openmls_traits::signatures::Signer as OpenMlsSigner; use storage::ConversationKind; +use crate::inbox_v2::{MlsIdentityProvider, MlsProvider}; use crate::types::AccountId; use crate::{ DeliveryService, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, - service_traits::KeyPackageProvider, + service_traits::{IdentityProvider, KeyPackageProvider}, types::{AddressedEncryptedPayload, ContentData}, }; -/// Provides the identity information needed to participate in an MLS group. -/// -/// Implementors must also implement [`OpenMlsSigner`] so they can sign MLS -/// messages. The two methods here supply what [`MlsContext::get_credential`] -/// needs to build a [`CredentialWithKey`]: `friendly_name` becomes the -/// `BasicCredential` label and `public_key` becomes the signature-verification key. -pub trait IdentityProvider: OpenMlsSigner { - fn friendly_name(&self) -> String; - fn public_key(&self) -> &Ed25519VerifyingKey; -} - -/// Connects the MLS protocol engine to app-level identity and transport. -/// -/// `GroupV1Convo` is generic over this trait so the MLS logic stays -/// independent of how identities are stored or how invites are delivered. -/// Implementors supply: -/// - a [`LibcruxProvider`] for MLS crypto operations -/// - an [`IdentityProvider`] for signing and credential construction -/// - [`invite_user`] — the app-specific logic for routing a [`Welcome`] -/// message to a new member's inbox -pub trait MlsContext { - type IDENT: IdentityProvider; - - fn ident(&self) -> &Self::IDENT; - fn provider(&self) -> &LibcruxProvider; - - // Build an MLS Credential from the supplied IdentityProvider - fn get_credential(&self) -> CredentialWithKey { - CredentialWithKey { - credential: BasicCredential::new(self.ident().friendly_name().into()).into(), - signature_key: self.ident().public_key().as_ref().into(), - } - } - - fn invite_user( - &self, - ds: &mut DS, - account_id: &AccountId, - welcome: &MlsMessageOut, - ) -> Result<(), ChatError>; -} - -pub struct GroupV1Convo { - ctx: Rc>, +pub struct GroupV1Convo { + identity_provider: Rc>>, + mls_provider: Rc>, ds: Rc>, keypkg_provider: Rc>, mls_group: MlsGroup, convo_id: String, } -impl std::fmt::Debug for GroupV1Convo +impl std::fmt::Debug for GroupV1Convo where - MlsCtx: MlsContext, + IP: IdentityProvider, + MP: MlsProvider, DS: DeliveryService, KP: KeyPackageProvider, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GroupV1Convo") - .field("name", &self.ctx.borrow().ident().friendly_name()) + .field("name", &self.identity_provider.borrow().friendly_name()) .field("convo_id", &self.convo_id) .field("mls_epoch", &self.mls_group.epoch()) .finish_non_exhaustive() } } -impl GroupV1Convo +impl GroupV1Convo where - MlsCtx: MlsContext, + IP: IdentityProvider, + MP: MlsProvider, DS: DeliveryService, KP: KeyPackageProvider, { // Create a new conversation with the creator as the only participant. pub fn new( - ctx: Rc>, + identity_provider: Rc>>, + mls_provider: Rc>, ds: Rc>, keypkg_provider: Rc>, ) -> Result { let config = Self::mls_create_config(); let mls_group = { - let ctx_ref = ctx.borrow(); - MlsGroup::new( - ctx_ref.provider(), - ctx_ref.ident(), - &config, - ctx_ref.get_credential(), - ) - .unwrap() + let mls_provider_ref = mls_provider.borrow(); + let signer = identity_provider.borrow(); + let credential = signer.get_credential(); + + MlsGroup::new(&*mls_provider_ref, &*signer, &config, credential).unwrap() }; let convo_id = hex::encode(mls_group.group_id().as_slice()); Self::subscribe(&mut ds.borrow_mut(), &convo_id)?; Ok(Self { - ctx, + identity_provider, + mls_provider, ds, keypkg_provider, mls_group, @@ -124,15 +82,14 @@ where // Constructs a new conversation upon receiving a MlsWelcome message. pub fn new_from_welcome( - ctx: Rc>, + identity_provider: Rc>>, + mls_provider: Rc>, ds: Rc>, keypkg_provider: Rc>, welcome: Welcome, ) -> Result { let mls_group = { - let ctx_borrow = ctx.borrow(); - let provider = ctx_borrow.provider(); - + let provider = &*mls_provider.borrow(); StagedWelcome::build_from_welcome(provider, &Self::mls_join_config(), welcome) .unwrap() .build() @@ -145,7 +102,8 @@ where Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; Ok(Self { - ctx, + identity_provider, + mls_provider, ds, keypkg_provider, mls_group, @@ -154,20 +112,22 @@ where } pub fn load( - ctx: Rc>, + identity_provider: Rc>>, + mls_provider: Rc>, ds: Rc>, keypkg_provider: Rc>, convo_id: String, group_id: GroupId, ) -> Result { - let mls_group = MlsGroup::load(ctx.borrow().provider().storage(), &group_id) + let mls_group = MlsGroup::load(mls_provider.borrow().storage(), &group_id) .map_err(ChatError::generic)? .ok_or_else(|| ChatError::NoConvo("mls group not found".into()))?; Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; Ok(GroupV1Convo { - ctx, + identity_provider, + mls_provider, ds, keypkg_provider, mls_group, @@ -233,17 +193,16 @@ where }; let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?; - let keypkg = key_package_in.validate( - self.ctx.borrow().provider().crypto(), - ProtocolVersion::Mls10, - )?; //TODO: P3 - Hardcoded Protocol Version + let keypkg = + key_package_in.validate(self.mls_provider.borrow().crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version Ok(keypkg) } } -impl Id for GroupV1Convo +impl Id for GroupV1Convo where - MlsCtx: MlsContext, + IP: IdentityProvider, + MP: MlsProvider, DS: DeliveryService, KP: KeyPackageProvider, { @@ -252,9 +211,10 @@ where } } -impl Convo for GroupV1Convo +impl Convo for GroupV1Convo where - MlsCtx: MlsContext, + IP: IdentityProvider, + MP: MlsProvider, DS: DeliveryService, KP: KeyPackageProvider, { @@ -262,11 +222,13 @@ where &mut self, content: &[u8], ) -> Result, ChatError> { - let ctx_ref = self.ctx.borrow(); - let provider = ctx_ref.provider(); let mls_message_out = self .mls_group - .create_message(provider, ctx_ref.ident(), content) + .create_message( + &*self.mls_provider.borrow(), + &*self.identity_provider.borrow(), + content, + ) .unwrap(); let a = AddressedEncryptedPayload { @@ -302,8 +264,7 @@ where .try_into_protocol_message() .map_err(ChatError::generic)?; - let ctx_borrow = self.ctx.borrow(); - let provider = ctx_borrow.provider(); + let provider = &*self.mls_provider.borrow(); if protocol_message.epoch() < self.mls_group.epoch() { // TODO: (P1) Add logging for messages arriving from past epoch. @@ -344,9 +305,10 @@ where } } -impl GroupConvo for GroupV1Convo +impl GroupConvo for GroupV1Convo where - MlsCtx: MlsContext, + IP: IdentityProvider, + MP: MlsProvider, DS: DeliveryService, KP: KeyPackageProvider, { @@ -355,8 +317,8 @@ where // welcome — the Welcome message sent privately to each new joiner // _group_info — used for external joins; ignore for now fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> { - let ctx_ref = self.ctx.borrow(); - let provider = ctx_ref.provider(); + let identity_provider = &*self.identity_provider.borrow(); + let mls_provider = &*self.mls_provider.borrow(); if members.len() > 50 { // This is a temporary limit that originates from the the De-MLS epoch time. @@ -374,14 +336,18 @@ where let (commit, welcome, _group_info) = self .mls_group - .add_members(provider, ctx_ref.ident(), keypkgs.iter().as_slice()) + .add_members(mls_provider, identity_provider, keypkgs.iter().as_slice()) .unwrap(); - self.mls_group.merge_pending_commit(provider).unwrap(); + self.mls_group.merge_pending_commit(mls_provider).unwrap(); // TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users for account_id in members { - ctx_ref.invite_user(&mut *self.ds.borrow_mut(), account_id, &welcome)?; + self.mls_provider.borrow().invite_user( + &mut *self.ds.borrow_mut(), + account_id, + &welcome, + )?; } let encrypted_payload = EncryptedPayload { diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index ef7b030..c667799 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -1,10 +1,14 @@ use std::cell::RefCell; +use std::ops::Deref; use std::rc::Rc; use chat_proto::logoschat::envelope::EnvelopeV1; use openmls::prelude::tls_codec::Serialize; use openmls::prelude::*; -use openmls_libcrux_crypto::Provider as LibcruxProvider; +use openmls_libcrux_crypto::CryptoProvider as LibcruxCryptoProvider; +use openmls_memory_storage::MemoryStorage; +use openmls_traits::signatures::Signer; +use openmls_traits::signatures::SignerError; use prost::{Message, Oneof}; use storage::ChatStore; use storage::ConversationMeta; @@ -12,29 +16,95 @@ use storage::ConversationMeta; use crate::AddressedEnvelope; use crate::ChatError; use crate::DeliveryService; +use crate::IdentityProvider; use crate::RegistrationService; -use crate::account::LogosAccount; -use crate::conversation::GroupConvo; -use crate::conversation::group_v1::MlsContext; -use crate::conversation::{GroupV1Convo, IdentityProvider}; +use crate::conversation::{GroupConvo, GroupV1Convo}; use crate::types::AccountId; use crate::utils::{blake2b_hex, hash_size}; -pub struct PqMlsContext { - ident_provider: LogosAccount, - provider: LibcruxProvider, + +// Define unique Identifiers derivations used in InboxV2 +fn delivery_address_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) } -impl MlsContext for PqMlsContext { - type IDENT = LogosAccount; +fn conversation_id_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) +} - fn ident(&self) -> &LogosAccount { - &self.ident_provider +pub struct MlsIdentityProvider(T); + +impl MlsIdentityProvider { + pub fn get_credential(&self) -> CredentialWithKey { + CredentialWithKey { + credential: BasicCredential::new(self.friendly_name().into()).into(), + signature_key: self.public_key().as_ref().into(), + } + } +} + +impl Deref for MlsIdentityProvider { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl IdentityProvider for MlsIdentityProvider { + fn account_id(&self) -> &AccountId { + self.0.account_id() } - fn provider(&self) -> &LibcruxProvider { - &self.provider + fn friendly_name(&self) -> String { + self.0.friendly_name() } + fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature { + self.0.sign(payload) + } + + fn public_key(&self) -> &crypto::Ed25519VerifyingKey { + self.0.public_key() + } +} + +impl Signer for MlsIdentityProvider { + fn sign(&self, payload: &[u8]) -> Result, SignerError> { + Ok(self.0.sign(payload).as_ref().to_vec()) + } + + fn signature_scheme(&self) -> SignatureScheme { + SignatureScheme::ED25519 + } +} + +/// An Extension trait which extends OpenMlsProvider to add required functionality +/// All MLS based Conversation should use this trait for defining requirements. +pub trait MlsProvider: OpenMlsProvider { + fn invite_user( + &self, + ds: &mut DS, + account_id: &AccountId, + welcome: &MlsMessageOut, + ) -> Result<(), ChatError>; +} + +/// This is a PQ based provider that uses in memory storage. +pub struct MlsEphemeralPqProvider { + crypto: LibcruxCryptoProvider, + storage: MemoryStorage, +} + +impl MlsEphemeralPqProvider { + pub fn new() -> Result { + let crypto = LibcruxCryptoProvider::new()?; + let storage = MemoryStorage::default(); + + Ok(Self { crypto, storage }) + } +} + +impl MlsProvider for MlsEphemeralPqProvider { fn invite_user( &self, ds: &mut DS, @@ -65,49 +135,62 @@ impl MlsContext for PqMlsContext { } } -// Define unique Identifiers derivations used in InboxV2 -fn delivery_address_for(account_id: &AccountId) -> String { - blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) -} +impl OpenMlsProvider for MlsEphemeralPqProvider { + type CryptoProvider = LibcruxCryptoProvider; + type RandProvider = LibcruxCryptoProvider; + type StorageProvider = openmls_memory_storage::MemoryStorage; -fn conversation_id_for(account_id: &AccountId) -> String { - blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) + fn storage(&self) -> &Self::StorageProvider { + &self.storage + } + + fn crypto(&self) -> &Self::CryptoProvider { + &self.crypto + } + + fn rand(&self) -> &Self::RandProvider { + &self.crypto + } } /// An PQ focused Conversation initializer. /// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols /// such as MLS. -pub struct InboxV2 { +pub struct InboxV2 +where + IP: IdentityProvider, +{ account_id: AccountId, + account: Rc>>, ds: Rc>, reg_service: Rc>, store: Rc>, - ctx: Rc>, + mls_provider: Rc>, } -impl InboxV2 +impl InboxV2 where + IP: IdentityProvider, DS: DeliveryService, RS: RegistrationService, CS: ChatStore, { pub fn new( - account: LogosAccount, + account: IP, ds: Rc>, reg_service: Rc>, store: Rc>, ) -> Self { + // Avoid referencing a temporary value by caching it. let account_id = account.account_id().clone(); - let provider = LibcruxProvider::new().unwrap(); + let provider = MlsEphemeralPqProvider::new().unwrap(); Self { account_id, + account: Rc::new(RefCell::new(MlsIdentityProvider(account))), ds, reg_service, store, - ctx: Rc::new(RefCell::new(PqMlsContext { - ident_provider: account, - provider, - })), + mls_provider: Rc::new(RefCell::new(provider)), } } @@ -123,10 +206,7 @@ where // "LastResort" package or publish multiple self.reg_service .borrow_mut() - .register( - &self.ctx.borrow().ident_provider.friendly_name(), - keypackage_bytes, - ) + .register(&self.account.borrow().friendly_name(), keypackage_bytes) .map_err(ChatError::generic) } @@ -138,8 +218,15 @@ where conversation_id_for(&self.account_id) } - pub fn create_group_v1(&self) -> Result, ChatError> { - GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone()) + pub fn create_group_v1( + &self, + ) -> Result, ChatError> { + GroupV1Convo::new( + self.account.clone(), + self.mls_provider.clone(), + self.ds.clone(), + self.reg_service.clone(), + ) } pub fn handle_frame(&self, payload_bytes: &[u8]) -> Result<(), ChatError> { @@ -180,7 +267,8 @@ where }; let convo = GroupV1Convo::new_from_welcome( - self.ctx.clone(), + self.account.clone(), + self.mls_provider.clone(), self.ds.clone(), self.reg_service.clone(), welcome, @@ -189,20 +277,21 @@ where } fn create_keypackage(&self) -> Result { - let ctx_borrow = self.ctx.borrow(); let capabilities = Capabilities::builder() .ciphersuites(vec![ Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, ]) .extensions(vec![ExtensionType::ApplicationId]) .build(); + + let signer = self.account.borrow(); let a = KeyPackage::builder() .leaf_node_capabilities(capabilities) .build( Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, - ctx_borrow.provider(), - ctx_borrow.ident(), - ctx_borrow.get_credential(), + &*self.mls_provider.borrow(), + &*signer, + signer.get_credential(), ) .expect("Failed to build KeyPackage"); @@ -212,11 +301,12 @@ where pub fn load_mls_convo( &self, convo_id: String, - ) -> Result, ChatError> { + ) -> Result, ChatError> { let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?; let group_id = GroupId::from_slice(&group_id_bytes); let convo = GroupV1Convo::load( - self.ctx.clone(), + self.account.clone(), + self.mls_provider.clone(), self.ds.clone(), self.reg_service.clone(), convo_id, diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index b48e521..2436cf9 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -1,4 +1,3 @@ -mod account; mod context; mod conversation; mod crypto; @@ -10,12 +9,11 @@ mod service_traits; mod types; mod utils; -pub use account::LogosAccount; pub use chat_sqlite::ChatStorage; pub use chat_sqlite::StorageConfig; pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use conversation::GroupConvo; pub use errors::ChatError; -pub use service_traits::{DeliveryService, RegistrationService}; +pub use service_traits::{DeliveryService, IdentityProvider, RegistrationService}; pub use types::{AccountId, AddressedEnvelope, ContentData}; pub use utils::hex_trunc; diff --git a/core/conversations/src/service_traits.rs b/core/conversations/src/service_traits.rs index 8e37253..441955c 100644 --- a/core/conversations/src/service_traits.rs +++ b/core/conversations/src/service_traits.rs @@ -3,6 +3,8 @@ /// different implementations. use std::{fmt::Debug, fmt::Display}; +use crypto::{Ed25519Signature, Ed25519VerifyingKey}; + use crate::types::{AccountId, AddressedEnvelope}; /// A Delivery service is responsible for payload transport. @@ -39,3 +41,12 @@ impl KeyPackageProvider for T { RegistrationService::retrieve(self, identity) } } + +/// Represents an external Identity +/// Implement this to provide an Authentication model for users/installations +pub trait IdentityProvider { + fn account_id(&self) -> &AccountId; + fn friendly_name(&self) -> String; + fn sign(&self, payload: &[u8]) -> Ed25519Signature; + fn public_key(&self) -> &Ed25519VerifyingKey; +} diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml index dec26c4..c9ade4b 100644 --- a/core/integration_tests_core/Cargo.toml +++ b/core/integration_tests_core/Cargo.toml @@ -8,9 +8,10 @@ edition = "2024" [dev-dependencies] # Workspace dependencies (sorted) +chat-sqlite = { workspace = true } components = { workspace = true } libchat = { workspace = true } -chat-sqlite = { workspace = true } +logos-account = { workspace = true, features = ["dev"]} storage = { workspace = true } # External dependencies (sorted) diff --git a/core/integration_tests_core/tests/mls_integration.rs b/core/integration_tests_core/tests/mls_integration.rs index 03119e7..a064dec 100644 --- a/core/integration_tests_core/tests/mls_integration.rs +++ b/core/integration_tests_core/tests/mls_integration.rs @@ -2,16 +2,16 @@ use std::ops::{Deref, DerefMut}; use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; use libchat::{ContentData, Context, GroupConvo, hex_trunc}; - +use logos_account::TestLogosAccount; // Simple client Functionality for testing struct Client { - inner: Context, + inner: Context, on_content: Option>, } impl Client { fn init( - ctx: Context, + ctx: Context, cb: Option, ) -> Self { Client { @@ -46,7 +46,7 @@ impl Client { } impl Deref for Client { - type Target = Context; + type Target = Context; fn deref(&self) -> &Self::Target { &self.inner @@ -80,9 +80,25 @@ fn create_group() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); - let saro_ctx = - Context::new_with_name("saro", ds.new_consumer(), rs.clone(), MemStore::new()).unwrap(); - let raya_ctx = Context::new_with_name("raya", ds.clone(), rs.clone(), MemStore::new()).unwrap(); + let saro_account = TestLogosAccount::new("saro"); + let saro_ctx = Context::new_with_name( + "saro", + saro_account, + ds.new_consumer(), + rs.clone(), + MemStore::new(), + ) + .unwrap(); + + let raya_account = TestLogosAccount::new("raya"); + let raya_ctx = Context::new_with_name( + "raya", + raya_account, + ds.clone(), + rs.clone(), + MemStore::new(), + ) + .unwrap(); let mut clients = vec![ Client::init(saro_ctx, Some(pretty_print(" Saro "))), @@ -115,7 +131,8 @@ fn create_group() { process(&mut clients); - let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); + let pax_account = TestLogosAccount::new("pax"); + let pax_ctx = Context::new_with_name("pax", pax_account, ds, rs, MemStore::new()).unwrap(); clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax")))); const PAX: usize = 2; diff --git a/core/integration_tests_core/tests/private_integration.rs b/core/integration_tests_core/tests/private_integration.rs index 165ba76..71f0048 100644 --- a/core/integration_tests_core/tests/private_integration.rs +++ b/core/integration_tests_core/tests/private_integration.rs @@ -1,13 +1,13 @@ use chat_sqlite::{ChatStorage, StorageConfig}; +use components::{EphemeralRegistry, LocalBroadcaster}; use libchat::{Context, Introduction}; +use logos_account::TestLogosAccount; use storage::{ConversationStore, IdentityStore}; use tempfile::tempdir; -use components::{EphemeralRegistry, LocalBroadcaster}; - fn send_and_verify( - sender: &mut Context, - receiver: &mut Context, + sender: &mut Context, + receiver: &mut Context, convo_id: &str, content: &[u8], ) { @@ -26,9 +26,18 @@ fn ctx_integration() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); - let mut saro = - Context::new_with_name("saro", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); - let mut raya = Context::new_with_name("raya", ds, rs, ChatStorage::in_memory()).unwrap(); + let saro_account = TestLogosAccount::new("saro"); + let raya_account = TestLogosAccount::new("raya"); + let mut saro = Context::new_with_name( + "saro", + saro_account, + ds.clone(), + rs.clone(), + ChatStorage::in_memory(), + ) + .unwrap(); + let mut raya = + Context::new_with_name("raya", raya_account, ds, rs, ChatStorage::in_memory()).unwrap(); // Raya creates intro bundle and sends to Saro let bundle = raya.create_intro_bundle().unwrap(); @@ -64,13 +73,14 @@ fn identity_persistence() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap(); - let ctx1 = Context::new_with_name("alice", ds, rs, store1).unwrap(); + let account = TestLogosAccount::new("saro"); + let ctx1 = Context::new_with_name("saro", account, ds, rs, store1).unwrap(); let pubkey1 = ctx1.identity().public_key(); let name1 = ctx1.installation_name().to_string(); // For persistence tests with file-based storage, we'd need a shared db. // With in-memory, we just verify the identity was created. - assert_eq!(name1, "alice"); + assert_eq!(name1, "saro"); assert!(!pubkey1.as_bytes().iter().all(|&b| b == 0)); } @@ -83,14 +93,15 @@ fn open_persists_new_identity() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap(); - let ctx = Context::new_from_store("alice", ds, rs, store).unwrap(); + let account = TestLogosAccount::new("saro"); + let ctx = Context::new_from_store("saro", account, ds, rs, store).unwrap(); let pubkey = ctx.identity().public_key(); drop(ctx); let store = ChatStorage::new(StorageConfig::File(db_path)).unwrap(); let persisted = store.load_identity().unwrap().unwrap(); - assert_eq!(persisted.get_name(), "alice"); + assert_eq!(persisted.get_name(), "saro"); assert_eq!(persisted.public_key(), pubkey); } @@ -98,19 +109,28 @@ fn open_persists_new_identity() { fn conversation_metadata_persistence() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); - let mut alice = - Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); - let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); + let account_saro = TestLogosAccount::new("saro"); + let mut saro = Context::new_with_name( + "saro", + account_saro, + ds.clone(), + rs.clone(), + ChatStorage::in_memory(), + ) + .unwrap(); + let account_raya = TestLogosAccount::new("raya"); + let mut raya = + Context::new_with_name("raya", account_raya, ds, rs, ChatStorage::in_memory()).unwrap(); - let bundle = alice.create_intro_bundle().unwrap(); + let bundle = saro.create_intro_bundle().unwrap(); let intro = Introduction::try_from(bundle.as_slice()).unwrap(); - let (_, payloads) = bob.create_private_convo(&intro, b"hi").unwrap(); + let (_, payloads) = raya.create_private_convo(&intro, b"hi").unwrap(); let payload = payloads.first().unwrap(); - let content = alice.handle_payload(&payload.data).unwrap().unwrap(); + let content = saro.handle_payload(&payload.data).unwrap().unwrap(); assert!(content.is_new_convo); - let convos = alice.store().load_conversations().unwrap(); + let convos = saro.store().load_conversations().unwrap(); assert_eq!(convos.len(), 1); assert_eq!(convos[0].kind.as_str(), "private_v1"); } @@ -119,45 +139,56 @@ fn conversation_metadata_persistence() { fn conversation_full_flow() { let ds = LocalBroadcaster::new(); let rs = EphemeralRegistry::new(); - let mut alice = - Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); - let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); - let bundle = alice.create_intro_bundle().unwrap(); + let account_saro = TestLogosAccount::new("saro"); + let account_raya = TestLogosAccount::new("raya"); + + let mut saro = Context::new_with_name( + "saro", + account_saro, + ds.clone(), + rs.clone(), + ChatStorage::in_memory(), + ) + .unwrap(); + let mut raya = + Context::new_with_name("raya", account_raya, ds, rs, ChatStorage::in_memory()).unwrap(); + + let bundle = saro.create_intro_bundle().unwrap(); let intro = Introduction::try_from(bundle.as_slice()).unwrap(); - let (bob_convo_id, payloads) = bob.create_private_convo(&intro, b"hello").unwrap(); + let (raya_convo_id, payloads) = raya.create_private_convo(&intro, b"hello").unwrap(); let payload = payloads.first().unwrap(); - let content = alice.handle_payload(&payload.data).unwrap().unwrap(); - let alice_convo_id = content.conversation_id; + let content = saro.handle_payload(&payload.data).unwrap().unwrap(); + let saro_convo_id = content.conversation_id; - let payloads = alice.send_content(&alice_convo_id, b"reply 1").unwrap(); + let payloads = saro.send_content(&saro_convo_id, b"reply 1").unwrap(); let payload = payloads.first().unwrap(); - bob.handle_payload(&payload.data).unwrap().unwrap(); + raya.handle_payload(&payload.data).unwrap().unwrap(); - let payloads = bob.send_content(&bob_convo_id, b"reply 2").unwrap(); + let payloads = raya.send_content(&raya_convo_id, b"reply 2").unwrap(); let payload = payloads.first().unwrap(); - alice.handle_payload(&payload.data).unwrap().unwrap(); + saro.handle_payload(&payload.data).unwrap().unwrap(); // Verify conversation list - let convo_ids = alice.list_conversations().unwrap(); + let convo_ids = saro.list_conversations().unwrap(); assert_eq!(convo_ids.len(), 1); // Continue exchanging messages - let payloads = bob.send_content(&bob_convo_id, b"more messages").unwrap(); + let payloads = raya.send_content(&raya_convo_id, b"more messages").unwrap(); let payload = payloads.first().unwrap(); - let content = alice + let content = saro .handle_payload(&payload.data) .expect("should decrypt") .expect("should have content"); assert_eq!(content.data, b"more messages"); - // Alice can also send back - let payloads = alice.send_content(&alice_convo_id, b"alice reply").unwrap(); + // saro can also send back + let payloads = saro.send_content(&saro_convo_id, b"saro reply").unwrap(); let payload = payloads.first().unwrap(); - let content = bob + let content = raya .handle_payload(&payload.data) .unwrap() - .expect("bob should receive"); - assert_eq!(content.data, b"alice reply"); + .expect("raya should receive"); + assert_eq!(content.data, b"saro reply"); } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index a8fda67..c2db6a8 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -9,8 +9,9 @@ crate-type = ["rlib"] [dependencies] # Workspace dependencies (sorted) chat-sqlite = { workspace = true } -components = { workspace = true} +components = { workspace = true } libchat = { workspace = true } +logos-account = { workspace = true, features = ["dev"] } # External dependencies (sorted) thiserror = "2" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index ad63606..5544dcc 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,23 +1,31 @@ use libchat::{ AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, - DeliveryService, Introduction, StorageConfig, + DeliveryService, IdentityProvider, Introduction, StorageConfig, }; +use logos_account::TestLogosAccount; use components::EphemeralRegistry; use crate::errors::ClientError; -pub struct ChatClient { - ctx: Context, +pub struct ChatClient +where + D: DeliveryService + 'static, +{ + ctx: Context, } -impl ChatClient { +impl ChatClient +where + D: DeliveryService + 'static, +{ /// Create an in-memory, ephemeral client. Identity is lost on drop. - pub fn new(name: impl Into, delivery: D) -> Self { + pub fn new(name: impl Into + Clone, delivery: D) -> Self { + let account = TestLogosAccount::new(name.clone()); let registry = EphemeralRegistry::new(); let store = ChatStorage::in_memory(); Self { - ctx: Context::new_with_name(name, delivery, registry, store).unwrap(), + ctx: Context::new_with_name(name, account, delivery, registry, store).unwrap(), } } @@ -26,13 +34,19 @@ impl ChatClient { /// If an identity already exists in storage it is loaded; otherwise a new /// one is created and saved. pub fn open( - name: impl Into, + identity: TestLogosAccount, config: StorageConfig, delivery: D, ) -> Result> { let store = ChatStorage::new(config).map_err(ChatError::from)?; let registry = EphemeralRegistry::new(); - let ctx = Context::new_from_store(name, delivery, registry, store)?; + let ctx = Context::new_from_store( + identity.account_id().to_string(), + identity, + delivery, + registry, + store, + )?; Ok(Self { ctx }) } diff --git a/crates/client/tests/saro_and_raya.rs b/crates/client/tests/saro_and_raya.rs index 9429270..711480c 100644 --- a/crates/client/tests/saro_and_raya.rs +++ b/crates/client/tests/saro_and_raya.rs @@ -1,6 +1,7 @@ use client::{ ChatClient, ContentData, ConversationIdOwned, Cursor, InProcessDelivery, StorageConfig, }; +use logos_account::TestLogosAccount; use std::sync::Arc; fn receive(receiver: &mut ChatClient, cursor: &mut Cursor) -> ContentData { @@ -57,11 +58,13 @@ fn open_persistent_client() { let db_path = dir.path().join("test.db").to_string_lossy().to_string(); let config = StorageConfig::File(db_path); - let client1 = ChatClient::open("saro", config.clone(), InProcessDelivery::default()).unwrap(); + let ident1 = TestLogosAccount::new("saro"); + let client1 = ChatClient::open(ident1, config.clone(), InProcessDelivery::default()).unwrap(); let name1 = client1.installation_name().to_string(); drop(client1); - let client2 = ChatClient::open("saro", config, InProcessDelivery::default()).unwrap(); + let ident2 = TestLogosAccount::new("saro"); + let client2 = ChatClient::open(ident2, config, InProcessDelivery::default()).unwrap(); let name2 = client2.installation_name().to_string(); assert_eq!( From b13959f6d5b28e1ad3de6de525a581b89ebc22dd Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Sat, 2 May 2026 12:57:48 -0700 Subject: [PATCH 37/39] make group_v1 private --- core/conversations/src/conversation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/conversations/src/conversation.rs b/core/conversations/src/conversation.rs index 94b5ff7..05ca5c1 100644 --- a/core/conversations/src/conversation.rs +++ b/core/conversations/src/conversation.rs @@ -1,4 +1,4 @@ -pub mod group_v1; +mod group_v1; mod privatev1; use crate::{ From 4c6286234b82f0224a0e0111ec55d9bc7682c920 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Sun, 10 May 2026 23:08:18 -0700 Subject: [PATCH 38/39] Core_client refactor --- Cargo.lock | 21 ++ Cargo.toml | 10 +- core/account/Cargo.toml | 2 + core/account/src/account.rs | 6 +- core/conversations/src/inbox_v2.rs | 8 +- core/conversations/src/lib.rs | 2 +- core/conversations/src/service_traits.rs | 17 +- core/core_client/Cargo.toml | 25 ++ core/core_client/src/conversation.rs | 51 +++ core/core_client/src/conversation/group_v1.rs | 332 ++++++++++++++++++ core/core_client/src/core_client.rs | 278 +++++++++++++++ core/core_client/src/errors.rs | 21 ++ core/core_client/src/inbox_v2.rs | 314 +++++++++++++++++ core/core_client/src/lib.rs | 13 + core/core_client/src/utils.rs | 64 ++++ core/integration_tests_core/Cargo.toml | 2 + .../integration_tests_core/tests/dev_tests.rs | 104 ++++++ .../src/delivery/local_broadcaster.rs | 4 + 18 files changed, 1262 insertions(+), 12 deletions(-) create mode 100644 core/core_client/Cargo.toml create mode 100644 core/core_client/src/conversation.rs create mode 100644 core/core_client/src/conversation/group_v1.rs create mode 100644 core/core_client/src/core_client.rs create mode 100644 core/core_client/src/errors.rs create mode 100644 core/core_client/src/inbox_v2.rs create mode 100644 core/core_client/src/lib.rs create mode 100644 core/core_client/src/utils.rs create mode 100644 core/integration_tests_core/tests/dev_tests.rs diff --git a/Cargo.lock b/Cargo.lock index a927f72..b42b888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,25 @@ dependencies = [ "rand 0.9.4", ] +[[package]] +name = "core_client" +version = "0.1.0" +dependencies = [ + "blake2", + "chat-proto", + "chat-sqlite 0.1.0", + "crypto 0.1.0", + "hex", + "libchat 0.1.0", + "openmls", + "openmls_libcrux_crypto 0.3.1", + "openmls_memory_storage 0.5.0", + "openmls_traits 0.5.0", + "prost", + "storage 0.1.0", + "thiserror", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1386,6 +1405,7 @@ version = "0.1.0" dependencies = [ "chat-sqlite 0.1.0", "components", + "core_client", "libchat 0.1.0", "logos-account", "storage 0.1.0", @@ -1828,6 +1848,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" name = "logos-account" version = "0.1.0" dependencies = [ + "core_client", "crypto 0.1.0", "libchat 0.1.0", ] diff --git a/Cargo.toml b/Cargo.toml index bb151df..effacf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bin/chat-cli", "core/account", "core/conversations", + "core/core_client", "core/crypto", "core/double-ratchets", "core/integration_tests_core", @@ -13,19 +14,20 @@ members = [ "core/storage", "crates/client-ffi", "crates/client", - "extensions/components", + "extensions/components", ] default-members = [ "core/account", - "core/sqlite", "core/conversations", + "core/core_client", "core/crypto", "core/double-ratchets", - "core/storage", "core/integration_tests_core", - "crates/client", + "core/sqlite", + "core/storage", "crates/client-ffi", + "crates/client", ] [workspace.dependencies] diff --git a/core/account/Cargo.toml b/core/account/Cargo.toml index 5162bf2..6f23fad 100644 --- a/core/account/Cargo.toml +++ b/core/account/Cargo.toml @@ -11,4 +11,6 @@ dev = [] crypto = { workspace = true } libchat = { workspace = true } +core_client = {path = "../core_client"} + # External dependencies (sorted) diff --git a/core/account/src/account.rs b/core/account/src/account.rs index 6705cfd..523896c 100644 --- a/core/account/src/account.rs +++ b/core/account/src/account.rs @@ -1,9 +1,11 @@ -use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; +use std::fmt::Debug; +use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; use libchat::{AccountId, IdentityProvider}; /// Logos Account represents a single account across /// multiple installations and services. +#[derive(Debug)] pub struct TestLogosAccount { id: AccountId, signing_key: Ed25519SigningKey, @@ -17,7 +19,7 @@ impl TestLogosAccount { let signing_key = Ed25519SigningKey::generate(); let verifying_key = signing_key.verifying_key(); Self { - id: AccountId::new(explicit_id.into()), + id: AccountId::new(explicit_id), signing_key, verifying_key, } diff --git a/core/conversations/src/inbox_v2.rs b/core/conversations/src/inbox_v2.rs index c667799..3d07263 100644 --- a/core/conversations/src/inbox_v2.rs +++ b/core/conversations/src/inbox_v2.rs @@ -31,13 +31,14 @@ fn conversation_id_for(account_id: &AccountId) -> String { blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) } +#[derive(Debug)] pub struct MlsIdentityProvider(T); impl MlsIdentityProvider { pub fn get_credential(&self) -> CredentialWithKey { CredentialWithKey { - credential: BasicCredential::new(self.friendly_name().into()).into(), - signature_key: self.public_key().as_ref().into(), + credential: BasicCredential::new(self.0.friendly_name().into()).into(), + signature_key: self.0.public_key().as_ref().into(), } } } @@ -202,8 +203,7 @@ where pub fn register(&mut self) -> Result<(), ChatError> { let keypackage_bytes = self.create_keypackage()?.tls_serialize_detached()?; - // TODO: (P3) Each keypackage can only be used once either enable... - // "LastResort" package or publish multiple + // TODO: (P3) Each keypackage can only be used once — enable LastResort or publish multiple self.reg_service .borrow_mut() .register(&self.account.borrow().friendly_name(), keypackage_bytes) diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index 2436cf9..ccda27c 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -15,5 +15,5 @@ pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use conversation::GroupConvo; pub use errors::ChatError; pub use service_traits::{DeliveryService, IdentityProvider, RegistrationService}; -pub use types::{AccountId, AddressedEnvelope, ContentData}; +pub use types::{AccountId, AddressedEncryptedPayload, AddressedEnvelope, ContentData}; pub use utils::hex_trunc; diff --git a/core/conversations/src/service_traits.rs b/core/conversations/src/service_traits.rs index 441955c..c7acf39 100644 --- a/core/conversations/src/service_traits.rs +++ b/core/conversations/src/service_traits.rs @@ -44,9 +44,24 @@ impl KeyPackageProvider for T { /// Represents an external Identity /// Implement this to provide an Authentication model for users/installations -pub trait IdentityProvider { +pub trait IdentityProvider: Debug { fn account_id(&self) -> &AccountId; fn friendly_name(&self) -> String; fn sign(&self, payload: &[u8]) -> Ed25519Signature; fn public_key(&self) -> &Ed25519VerifyingKey; } + +impl IdentityProvider for &T { + fn account_id(&self) -> &AccountId { + (**self).account_id() + } + fn friendly_name(&self) -> String { + (**self).friendly_name() + } + fn sign(&self, payload: &[u8]) -> Ed25519Signature { + (**self).sign(payload) + } + fn public_key(&self) -> &Ed25519VerifyingKey { + (**self).public_key() + } +} diff --git a/core/core_client/Cargo.toml b/core/core_client/Cargo.toml new file mode 100644 index 0000000..bc8691b --- /dev/null +++ b/core/core_client/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "core_client" +version = "0.1.0" +edition = "2024" + + + + +[dependencies] +# Workspace dependencies (sorted) +blake2 = { workspace = true } +chat-sqlite = { workspace = true } +crypto = { workspace = true } +libchat = { workspace = true } +storage = { workspace = true } + +# External dependencies (sorted) +chat-proto = { git = "https://github.com/logos-messaging/chat_proto" } +thiserror = "2.0.18" +prost = "0.14.3" +hex = "0.4.3" +openmls = "0.8.1" +openmls_libcrux_crypto = "0.3.1" +openmls_memory_storage = "0.5.0" +openmls_traits = "0.5.0" diff --git a/core/core_client/src/conversation.rs b/core/core_client/src/conversation.rs new file mode 100644 index 0000000..58aa4f1 --- /dev/null +++ b/core/core_client/src/conversation.rs @@ -0,0 +1,51 @@ +mod group_v1; + +use crate::{AccountId, ContentData, DeliveryService, RegistrationService}; +use chat_proto::logoschat::encryption::EncryptedPayload; +use libchat::IdentityProvider; + +use std::fmt::Debug; + +pub use crate::ChatError; +pub use group_v1::GroupV1Convo; + +pub type ConversationIdRef<'a> = &'a str; +pub type ConversationId = String; + +pub struct ServiceContext { + pub identity_provider: IP, + pub ds: DS, + pub rs: RS, +} + +pub trait Id: Debug { + fn id(&self) -> ConversationIdRef<'_>; +} + +pub trait BaseConvo: + Id + Debug +{ + fn init(&self, service_ctx: &mut ServiceContext) -> Result<(), ChatError>; + + fn send_content( + &mut self, + service_ctx: &mut ServiceContext, + content: &[u8], + ) -> Result<(), ChatError>; + + fn handle_frame( + &mut self, + service_ctx: &mut ServiceContext, + enc_payload: EncryptedPayload, + ) -> Result, ChatError>; +} + +pub trait BaseGroupConvo: + BaseConvo +{ + fn add_member( + &mut self, + service_ctx: &mut ServiceContext, + members: &[&AccountId], + ) -> Result<(), ChatError>; +} diff --git a/core/core_client/src/conversation/group_v1.rs b/core/core_client/src/conversation/group_v1.rs new file mode 100644 index 0000000..91ee1cf --- /dev/null +++ b/core/core_client/src/conversation/group_v1.rs @@ -0,0 +1,332 @@ +/// GroupV1 is a conversationType which provides effecient handling of multiple participants +/// Properties: +/// - Harvest Now Decrypt Later (HNDL) protection provided by XWING +/// - Multiple +use std::cell::RefCell; +use std::rc::Rc; + +use blake2::{Blake2b, Digest, digest::consts::U6}; +use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; +use openmls::prelude::tls_codec::Deserialize; +use openmls::prelude::*; + +use crate::AccountId; +use crate::conversation::{ConversationIdRef, ServiceContext}; +use crate::inbox_v2::{MlsIdentityProvider, MlsProvider}; +use crate::{ + AddressedEncryptedPayload, ContentData, DeliveryService, IdentityProvider, RegistrationService, + conversation::{BaseConvo, BaseGroupConvo, ChatError, Id}, +}; + +pub struct GroupV1Convo { + mls_provider: Rc>, + mls_group: MlsGroup, + convo_id: String, +} + +impl std::fmt::Debug for GroupV1Convo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GroupV1Convo") + .field("convo_id", &self.convo_id) + .field("mls_epoch", &self.mls_group.epoch()) + .finish_non_exhaustive() + } +} + +impl GroupV1Convo { + // Create a new conversation with the creator as the only participant. + pub fn new( + identity_provider: MlsIdentityProvider, + mls_provider: Rc>, + ) -> Result { + let config = Self::mls_create_config(); + let mls_group = { + let credential = identity_provider.get_credential(); + MlsGroup::new( + &*mls_provider.borrow(), + &identity_provider, + &config, + credential, + ) + .unwrap() + }; + let convo_id = hex::encode(mls_group.group_id().as_slice()); + + Ok(Self { + mls_provider, + mls_group, + convo_id, + }) + } + + // Constructs a new conversation upon receiving a MlsWelcome message. + pub fn new_from_welcome( + mls_provider: Rc>, + welcome: Welcome, + ) -> Result { + let mls_group = { + let provider = &*mls_provider.borrow(); + StagedWelcome::build_from_welcome(provider, &Self::mls_join_config(), welcome) + .unwrap() + .build() + .unwrap() + .into_group(provider) + .unwrap() + }; + + let convo_id = hex::encode(mls_group.group_id().as_slice()); + + Ok(Self { + mls_provider, + mls_group, + convo_id, + }) + } + + fn mls_create_config() -> MlsGroupCreateConfig { + MlsGroupCreateConfig::builder() + .ciphersuite(Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519) + .use_ratchet_tree_extension(true) // This is handy for now, until there is central store for this data + .build() + } + + fn mls_join_config() -> MlsGroupJoinConfig { + MlsGroupJoinConfig::builder().build() + } + + fn delivery_address_from_id(convo_id: &str) -> String { + let hash = Blake2b::::new() + .chain_update("delivery_addr|") + .chain_update(convo_id) + .finalize(); + hex::encode(hash) + } + + fn delivery_address(&self) -> String { + Self::delivery_address_from_id(&self.convo_id) + } + + fn ctrl_delivery_address_from_id(convo_id: &str) -> String { + let hash = Blake2b::::new() + .chain_update("ctrl_delivery_addr|") + .chain_update(convo_id) + .finalize(); + hex::encode(hash) + } + + fn ctrl_delivery_address(&self) -> String { + Self::ctrl_delivery_address_from_id(&self.convo_id) + } +} + +impl Id for GroupV1Convo +where + MP: MlsProvider, +{ + fn id(&self) -> ConversationIdRef<'_> { + &self.convo_id + } +} + +impl BaseConvo for GroupV1Convo +where + IP: IdentityProvider, + MP: MlsProvider, + DS: DeliveryService, + RS: RegistrationService, + // KP: RegistrationService, +{ + fn init(&self, service_ctx: &mut super::ServiceContext) -> Result<(), ChatError> { + // Configure the delivery service to listen for the required delivery addresses. + + service_ctx + .ds + .subscribe(&Self::delivery_address_from_id(&self.convo_id)) + .map_err(ChatError::generic)?; + service_ctx + .ds + .subscribe(&Self::ctrl_delivery_address_from_id(&self.convo_id)) + .map_err(ChatError::generic)?; + + Ok(()) + } + + fn send_content( + &mut self, + service_ctx: &mut super::ServiceContext, + content: &[u8], + ) -> Result<(), ChatError> { + let signer = MlsIdentityProvider(&service_ctx.identity_provider); + let mls_message_out = self + .mls_group + .create_message(&*self.mls_provider.borrow(), &signer, content) + .unwrap(); + + let payload = AddressedEncryptedPayload { + delivery_address: self.delivery_address(), + data: EncryptedPayload { + encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext { + payload: mls_message_out.to_bytes().unwrap().into(), + })), + }, + }; + + let env = payload.into_envelope(self.id().into()); + service_ctx + .ds + .publish(env) + .map_err(|e| ChatError::Delivery(e.to_string()))?; + + Ok(()) + } + + fn handle_frame( + &mut self, + _service_ctx: &mut super::ServiceContext, + encoded_payload: EncryptedPayload, + ) -> Result, ChatError> { + let bytes = match encoded_payload.encryption { + Some(encrypted_payload::Encryption::Plaintext(pt)) => pt.payload, + _ => { + return Err(ChatError::generic("Expected plaintext")); + } + }; + + let mls_message = + MlsMessageIn::tls_deserialize_exact_bytes(&bytes).map_err(ChatError::generic)?; + + let protocol_message: ProtocolMessage = mls_message + .try_into_protocol_message() + .map_err(ChatError::generic)?; + + let provider = &*self.mls_provider.borrow(); + + if protocol_message.epoch() < self.mls_group.epoch() { + // TODO: (P1) Add logging for messages arriving from past epoch. + return Ok(None); + } + + let processed = self + .mls_group + .process_message(provider, protocol_message) + .map_err(ChatError::generic)?; + + match processed.into_content() { + ProcessedMessageContent::ApplicationMessage(msg) => Ok(Some(ContentData { + conversation_id: hex::encode(self.mls_group.group_id().as_slice()), + data: msg.into_bytes(), + is_new_convo: false, + })), + ProcessedMessageContent::StagedCommitMessage(commit) => { + self.mls_group + .merge_staged_commit(provider, *commit) + .map_err(ChatError::generic)?; + Ok(None) + } + _ => { + // TODO: (P2) Log unknown message type + Ok(None) + } + } + } +} + +impl BaseGroupConvo for GroupV1Convo +where + IP: IdentityProvider, + MP: MlsProvider, + DS: DeliveryService, + RS: RegistrationService, +{ + // add_members returns: + // commit — the Commit message Alice broadcasts to all members + // welcome — the Welcome message sent privately to each new joiner + // _group_info — used for external joins; ignore for now + fn add_member( + &mut self, + service_ctx: &mut ServiceContext, + members: &[&AccountId], + ) -> Result<(), ChatError> { + let mls_provider = &*self.mls_provider.borrow(); + + if members.len() > 50 { + // This is a temporary limit that originates from the the De-MLS epoch time. + return Err(ChatError::generic( + "Cannot add more than 50 Members at a time", + )); + } + + if members.is_empty() { + return Ok(()); + } + + // Get the Keypacakages and transpose any errors. + // The account_id is kept so invites can be addressed properly + let keypkgs = members + .iter() + .map(|ident| self.key_package_for_account(service_ctx, ident)) + .collect::, ChatError>>()?; + + let signer = MlsIdentityProvider(&service_ctx.identity_provider); + let (commit, welcome, _group_info) = self + .mls_group + .add_members(mls_provider, &signer, keypkgs.iter().as_slice()) + .unwrap(); + + self.mls_group.merge_pending_commit(mls_provider).unwrap(); + + // TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users + for account_id in members { + self.mls_provider + .borrow() + .invite_user(&mut service_ctx.ds, account_id, &welcome)?; + } + + let encrypted_payload = EncryptedPayload { + encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext { + payload: commit.to_bytes().map_err(ChatError::generic)?.into(), + })), + }; + + let addr_enc_payload = AddressedEncryptedPayload { + delivery_address: self.ctrl_delivery_address(), + data: encrypted_payload, + }; + // Prepare commit message + // TODO: (P1) Make GroupConvos agnostic to framing so its less error prone and more + let env = addr_enc_payload.into_envelope(self.convo_id.clone()); + + service_ctx + .ds + .publish(env) + .map_err(|e| ChatError::Generic(format!("Publish: {e}"))) + } +} + +impl GroupV1Convo { + fn key_package_for_account< + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, + >( + &self, + service_ctx: &mut ServiceContext, + ident: &AccountId, + ) -> Result { + let retrieved_bytes = service_ctx + .rs + .retrieve(ident) + .map_err(|e: RS::Error| ChatError::Generic(e.to_string()))?; + + // dbg!(ctx.contact_registry()); + let Some(keypkg_bytes) = retrieved_bytes else { + return Err(ChatError::generic("Group Contact Not Found")); + }; + + let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?; + let keypkg = key_package_in + .validate(self.mls_provider.borrow().crypto(), ProtocolVersion::Mls10) + .map_err(ChatError::generic)?; //TODO: P3 - Hardcoded Protocol Version + Ok(keypkg) + } +} diff --git a/core/core_client/src/core_client.rs b/core/core_client/src/core_client.rs new file mode 100644 index 0000000..ce0f347 --- /dev/null +++ b/core/core_client/src/core_client.rs @@ -0,0 +1,278 @@ +use std::cell::RefMut; +use std::collections::HashMap; +use std::{cell::RefCell, rc::Rc}; + +use crate::conversation::{BaseGroupConvo, ConversationId, ConversationIdRef, Id, ServiceContext}; + +use crate::inbox_v2::InboxV2; +use crate::{AccountId, errors::ChatError}; +use crate::{DeliveryService, IdentityProvider, RegistrationService}; +use chat_proto::logoschat::encryption::EncryptedPayload; +use chat_proto::logoschat::envelope::EnvelopeV1; +use libchat::ContentData; +use prost::Message; +use storage::ChatStore; + +#[derive(Debug)] +enum ConvoTypeOwned { + // Pairwise(Box>), + Group(Box>), +} + +impl Id for ConvoTypeOwned +where + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, +{ + fn id(&self) -> crate::conversation::ConversationIdRef<'_> { + match self { + // ConvoTypeOwned::Pairwise(convo) => convo.id(), + ConvoTypeOwned::Group(convo) => convo.id(), + } + } +} + +pub struct GroupConvo< + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, + CS: ChatStore, +> { + client: Rc>>, + convo_id: ConversationId, +} + +impl GroupConvo +where + IP: IdentityProvider + 'static, + DS: DeliveryService + 'static, + RS: RegistrationService + 'static, + CS: ChatStore + 'static, +{ + pub fn send_content(&self, content: &[u8]) -> Result<(), ChatError> { + let mut client = self.client.borrow_mut(); + client.send_content(self.convo_id.as_str(), content) + } +} + +pub struct CoreClient< + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, + CS: ChatStore, +> { + inner: Rc>>, +} + +impl CoreClient +where + IP: IdentityProvider + 'static, + DS: DeliveryService + 'static, + RS: RegistrationService + 'static, + CS: ChatStore + 'static, +{ + pub fn new(account: IP, delivery: DS, registration: RS, store: CS) -> Result { + let c = InnerClient::new(account, delivery, registration, store)?; + Ok(Self { + inner: Rc::new(RefCell::new(c)), + }) + } + + pub fn account_id(&self) -> AccountId { + self.inner.borrow().account_id().clone() + } + + pub fn ds(&self) -> RefMut<'_, DS> { + RefMut::map(self.inner.borrow_mut(), |c| c.ds()) + } + + pub fn create_group_convo( + &self, + participants: &[&AccountId], + ) -> Result, ChatError> { + let convo_id = self.inner.borrow_mut().create_group_convo(participants)?; + Ok(GroupConvo { + client: self.inner.clone(), + convo_id, + }) + } + + pub fn list_conversations(&self) -> Result, ChatError> { + self.inner.borrow().list_conversations() + } + + pub fn send_content( + &self, + convo_id: ConversationIdRef, + content: &[u8], + ) -> Result<(), ChatError> { + self.inner.borrow_mut().send_content(convo_id, content) + } + + pub fn handle_payload(&self, payload: &[u8]) -> Result, ChatError> { + self.inner.borrow_mut().handle_payload(payload) + } + + pub fn convo(&self, convo_id: ConversationIdRef) -> Option> { + let client = self.inner.clone(); + + if !client.borrow().has_conversation(convo_id) { + return None; + } + + Some(GroupConvo { + client, + convo_id: convo_id.to_string(), + }) + } +} + +struct InnerClient< + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, + CS: ChatStore, +> { + service_ctx: ServiceContext, + _store: Rc>, + + pq_inbox: InboxV2, + + // Cache of loaded conversations + cached_convos: HashMap>, +} + +impl InnerClient +where + IP: IdentityProvider + 'static, + DS: DeliveryService + 'static, + RS: RegistrationService + 'static, + CS: ChatStore + 'static, +{ + pub fn new(account: IP, delivery: DS, registration: RS, store: CS) -> Result { + // Services for sharing with Converastions/Inboxes + + let mut service_ctx = ServiceContext { + identity_provider: account, + ds: delivery, + rs: registration, + }; + + // let contact_registry = Rc::new(RefCell::new(registration)); + let _store = Rc::new(RefCell::new(store)); + + let pq_inbox = InboxV2::new(&mut service_ctx, _store.clone()); + pq_inbox.register(&mut service_ctx)?; + + // Subscribe + service_ctx + .ds + .subscribe(&pq_inbox.delivery_address()) + .map_err(ChatError::generic)?; + + Ok(Self { + service_ctx, + _store, + pq_inbox, + cached_convos: HashMap::new(), + }) + } + + pub fn ds(&mut self) -> &mut DS { + &mut self.service_ctx.ds + } + + /// Returns the unique identifier associated with the account + pub fn account_id(&self) -> &AccountId { + self.pq_inbox.account_id() + } + + pub fn create_group_convo(&mut self, participants: &[&AccountId]) -> Result { + let convo = self.pq_inbox.create_group_v1(&mut self.service_ctx)?; + let mut convo: Box> = Box::new(convo); + convo.init(&mut self.service_ctx)?; + convo.add_member(&mut self.service_ctx, participants)?; + + let convo_id = convo.id().to_string(); + + self.register_convo(ConvoTypeOwned::Group(convo))?; + + Ok(convo_id) + } + + pub fn list_conversations(&self) -> Result, ChatError> { + Ok(self.cached_convos.keys().cloned().collect()) + } + + pub fn has_conversation(&self, convo_id: ConversationIdRef) -> bool { + self.cached_convos.contains_key(convo_id) + } + + pub fn send_content( + &mut self, + convo_id: ConversationIdRef, + content: &[u8], + ) -> Result<(), ChatError> { + let Some(convo) = self.cached_convos.get_mut(convo_id) else { + return Err(ChatError::generic("No Convo Found")); + }; + let convo = match convo { + // ConvoTypeOwned::Pairwise(_) => todo!(), + ConvoTypeOwned::Group(c) => c.as_mut(), + }; + convo.send_content(&mut self.service_ctx, content) + } + + // Decode bytes and send to protocol for processing. + pub fn handle_payload(&mut self, payload: &[u8]) -> Result, ChatError> { + let env = EnvelopeV1::decode(payload)?; + + // TODO: Impl Conversation hinting + let convo_id = env.conversation_hint; + match convo_id { + c if c == self.pq_inbox.id() => self.dispatch_to_inbox2(&env.payload), + c if self.cached_convos.contains_key(c.as_str()) => { + self.dispatch_to_convo(c, &env.payload) + } + _ => Ok(None), + } + } + + // Dispatch encrypted payload to Inbox, and register the created Conversation + fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result, ChatError> { + if let Some(convo) = self.pq_inbox.handle_frame(&mut self.service_ctx, payload)? { + let convo: Box> = Box::new(convo); + self.register_convo(ConvoTypeOwned::Group(convo))?; + } + Ok(None) + } + + // Dispatch encrypted payload to its corresponding conversation + fn dispatch_to_convo( + &mut self, + convo_id: ConversationId, + enc_payload_bytes: &[u8], + ) -> Result, ChatError> { + let enc_payload = EncryptedPayload::decode(enc_payload_bytes)?; + + let Some(convo) = self.cached_convos.get_mut(&convo_id) else { + return Err(ChatError::generic("No Convo Found")); + }; + let convo = match convo { + // ConvoTypeOwned::Pairwise(_) => todo!(), + ConvoTypeOwned::Group(c) => c.as_mut(), + }; + + convo.handle_frame(&mut self.service_ctx, enc_payload) + } + + fn register_convo(&mut self, convo: ConvoTypeOwned) -> Result<(), ChatError> { + let res = self.cached_convos.insert(convo.id().to_string(), convo); + + match res { + Some(_) => Err(ChatError::generic("Convo already exists. Cannot save")), + None => Ok(()), + } + } +} diff --git a/core/core_client/src/errors.rs b/core/core_client/src/errors.rs new file mode 100644 index 0000000..b246f0b --- /dev/null +++ b/core/core_client/src/errors.rs @@ -0,0 +1,21 @@ +use openmls::prelude::tls_codec; +pub use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ChatError { + #[error("generic: {0}")] + Generic(String), + #[error("TlsCodec: {0}")] + TlsCodec(#[from] tls_codec::Error), + #[error("Protobuf decode: {0}")] + ProtobufDecodeError(#[from] prost::DecodeError), + #[error("delivery: {0}")] + Delivery(String), +} + +impl ChatError { + // This is a stopgap until there is a proper error system in place + pub fn generic(e: impl ToString) -> Self { + Self::Generic(e.to_string()) + } +} diff --git a/core/core_client/src/inbox_v2.rs b/core/core_client/src/inbox_v2.rs new file mode 100644 index 0000000..2e5d00b --- /dev/null +++ b/core/core_client/src/inbox_v2.rs @@ -0,0 +1,314 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; + +use chat_proto::logoschat::envelope::EnvelopeV1; +use openmls::prelude::tls_codec::Serialize; +use openmls::prelude::*; +use openmls_libcrux_crypto::CryptoProvider as LibcruxCryptoProvider; +use openmls_memory_storage::MemoryStorage; +use openmls_traits::signatures::Signer; +use openmls_traits::signatures::SignerError; +use prost::{Message, Oneof}; +use storage::ChatStore; +use storage::ConversationMeta; + +use crate::AccountId; +use crate::AddressedEnvelope; +use crate::ChatError; +use crate::DeliveryService; +use crate::IdentityProvider; +use crate::RegistrationService; +use crate::conversation::BaseConvo; +use crate::conversation::ServiceContext; +use crate::conversation::{GroupV1Convo, Id}; +use crate::utils::{blake2b_hex, hash_size}; + +// Define unique Identifiers derivations used in InboxV2 +fn delivery_address_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "delivery_address|", account_id.as_str()]) +} + +fn conversation_id_for(account_id: &AccountId) -> String { + blake2b_hex::(&["InboxV2|", "conversation_id|", account_id.as_str()]) +} + +#[derive(Debug)] +pub struct MlsIdentityProvider(pub T); + +impl MlsIdentityProvider { + pub fn get_credential(&self) -> CredentialWithKey { + CredentialWithKey { + credential: BasicCredential::new(self.0.friendly_name().into()).into(), + signature_key: self.0.public_key().as_ref().into(), + } + } +} + +impl Deref for MlsIdentityProvider { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl IdentityProvider for MlsIdentityProvider { + fn account_id(&self) -> &AccountId { + self.0.account_id() + } + + fn friendly_name(&self) -> String { + self.0.friendly_name() + } + + fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature { + self.0.sign(payload) + } + + fn public_key(&self) -> &crypto::Ed25519VerifyingKey { + self.0.public_key() + } +} + +impl Signer for MlsIdentityProvider { + fn sign(&self, payload: &[u8]) -> Result, SignerError> { + Ok(self.0.sign(payload).as_ref().to_vec()) + } + + fn signature_scheme(&self) -> SignatureScheme { + SignatureScheme::ED25519 + } +} + +/// An Extension trait which extends OpenMlsProvider to add required functionality +/// All MLS based Conversation should use this trait for defining requirements. +pub trait MlsProvider: OpenMlsProvider { + fn invite_user( + &self, + ds: &mut DS, + account_id: &AccountId, + welcome: &MlsMessageOut, + ) -> Result<(), ChatError>; +} + +/// This is a PQ based provider that uses in memory storage. +pub struct MlsEphemeralPqProvider { + crypto: LibcruxCryptoProvider, + storage: MemoryStorage, +} + +impl MlsEphemeralPqProvider { + pub fn new() -> Result { + let crypto = LibcruxCryptoProvider::new()?; + let storage = MemoryStorage::default(); + + Ok(Self { crypto, storage }) + } +} + +impl MlsProvider for MlsEphemeralPqProvider { + fn invite_user( + &self, + ds: &mut DS, + account_id: &AccountId, + welcome: &MlsMessageOut, + ) -> Result<(), ChatError> { + let invite = GroupV1HeavyInvite { + welcome_bytes: welcome.to_bytes().map_err(ChatError::generic)?, + }; + + let frame = InboxV2Frame { + payload: Some(InviteType::GroupV1(invite)), + }; + + let envelope = EnvelopeV1 { + conversation_hint: conversation_id_for(account_id), + salt: 0, + payload: frame.encode_to_vec().into(), + }; + + let outbound_msg = AddressedEnvelope { + delivery_address: delivery_address_for(account_id), + data: envelope.encode_to_vec(), + }; + + ds.publish(outbound_msg).map_err(ChatError::generic)?; + Ok(()) + } +} + +impl OpenMlsProvider for MlsEphemeralPqProvider { + type CryptoProvider = LibcruxCryptoProvider; + type RandProvider = LibcruxCryptoProvider; + type StorageProvider = openmls_memory_storage::MemoryStorage; + + fn storage(&self) -> &Self::StorageProvider { + &self.storage + } + + fn crypto(&self) -> &Self::CryptoProvider { + &self.crypto + } + + fn rand(&self) -> &Self::RandProvider { + &self.crypto + } +} + +/// An PQ focused Conversation initializer. +/// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols +/// such as MLS. +pub struct InboxV2 { + account_id: AccountId, + _store: Rc>, + mls_provider: Rc>, +} + +impl InboxV2 { + pub fn new( + service_ctx: &mut ServiceContext, + _store: Rc>, + ) -> Self { + // Avoid referencing a temporary value by caching it. + let account_id = service_ctx.identity_provider.account_id().clone(); + let provider = MlsEphemeralPqProvider::new().unwrap(); + Self { + account_id, + _store, + mls_provider: Rc::new(RefCell::new(provider)), + } + } + + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + + pub fn delivery_address(&self) -> String { + delivery_address_for(&self.account_id) + } + + pub fn id(&self) -> String { + conversation_id_for(&self.account_id) + } + + /// Submit MlsKeypackage to registration service + pub fn register( + &self, + service_ctx: &mut ServiceContext, + ) -> Result<(), ChatError> { + let mls_ident = MlsIdentityProvider(&service_ctx.identity_provider); + let keypackage_bytes = self + .create_keypackage(&mls_ident)? + .tls_serialize_detached()?; + + // TODO: (P3) Each keypackage can only be used once either enable... + // "LastResort" package or publish multiple + service_ctx + .rs + .register( + &service_ctx.identity_provider.friendly_name(), + keypackage_bytes, + ) + .map_err(ChatError::generic) + } + + pub fn create_group_v1( + &self, + service_ctx: &mut ServiceContext, + ) -> Result, ChatError> { + let mls_ident = MlsIdentityProvider(&service_ctx.identity_provider); + GroupV1Convo::new(mls_ident, self.mls_provider.clone()) + } + + fn create_keypackage( + &self, + signer: &MlsIdentityProvider, + ) -> Result { + let capabilities = Capabilities::builder() + .ciphersuites(vec![ + Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, + ]) + .extensions(vec![ExtensionType::ApplicationId]) + .build(); + + let a = KeyPackage::builder() + .leaf_node_capabilities(capabilities) + .build( + Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, + &*self.mls_provider.borrow(), + signer, + signer.get_credential(), + ) + .expect("Failed to build KeyPackage"); + + Ok(a.key_package().clone()) + } +} + +impl InboxV2 { + pub fn handle_frame( + &self, + service_ctx: &mut ServiceContext, + payload_bytes: &[u8], + ) -> Result>, ChatError> { + let inbox_frame = InboxV2Frame::decode(payload_bytes)?; + + let Some(payload) = inbox_frame.payload else { + return Err(ChatError::Generic("InboxV2Payload missing".into())); + }; + + match payload { + InviteType::GroupV1(group_v1_heavy_invite) => self + .handle_heavy_invite(service_ctx, group_v1_heavy_invite) + .map(Some), + } + } + + fn handle_heavy_invite( + &self, + service_ctx: &mut ServiceContext, + invite: GroupV1HeavyInvite, + ) -> Result, ChatError> { + let (msg_in, _rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?; + + let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else { + return Err(ChatError::Generic("Expected Welcome".into())); + }; + + let convo = GroupV1Convo::new_from_welcome(self.mls_provider.clone(), welcome)?; + convo.init(service_ctx)?; + self.persist_convo(convo.id())?; + Ok(convo) + } + + fn persist_convo(&self, local_convo_id: &str) -> Result<(), ChatError> { + let meta = ConversationMeta { + local_convo_id: local_convo_id.to_string(), + remote_convo_id: "0".into(), + kind: storage::ConversationKind::GroupV1, + }; + self._store + .borrow_mut() + .save_conversation(&meta) + .map_err(ChatError::generic) + } +} + +#[derive(Clone, PartialEq, Message)] +pub struct InboxV2Frame { + #[prost(oneof = "InviteType", tags = "1")] + pub payload: Option, +} + +#[derive(Clone, PartialEq, Oneof)] +pub enum InviteType { + #[prost(message, tag = "1")] + GroupV1(GroupV1HeavyInvite), +} + +#[derive(Clone, PartialEq, Message)] +pub struct GroupV1HeavyInvite { + #[prost(bytes, tag = "1")] + pub welcome_bytes: Vec, +} diff --git a/core/core_client/src/lib.rs b/core/core_client/src/lib.rs new file mode 100644 index 0000000..c63baab --- /dev/null +++ b/core/core_client/src/lib.rs @@ -0,0 +1,13 @@ +mod conversation; +mod core_client; +mod errors; +mod inbox_v2; +mod utils; + +pub use libchat::{ + AccountId, AddressedEncryptedPayload, AddressedEnvelope, ContentData, DeliveryService, + IdentityProvider, RegistrationService, +}; + +pub use core_client::{CoreClient, GroupConvo}; +pub use errors::ChatError; diff --git a/core/core_client/src/utils.rs b/core/core_client/src/utils.rs new file mode 100644 index 0000000..5afd21c --- /dev/null +++ b/core/core_client/src/utils.rs @@ -0,0 +1,64 @@ +use blake2::{Blake2b, Digest}; + +/// Track hash sizes in use across the crate. +pub mod hash_size { + use blake2::digest::{ + consts::U64, + generic_array::ArrayLength, + typenum::{IsLessOrEqual, NonZero}, + }; + + pub trait HashLen + where + >::Output: NonZero, + { + type Size: ArrayLength + IsLessOrEqual; + } + + /// This macro generates HashLen for the given typenum::length + macro_rules! hash_sizes { + ($($(#[$attr:meta])* $name:ident => $size:ty),* $(,)?) => { + $( + $(#[$attr])* + pub struct $name; + impl HashLen for $name { type Size = $size; } + )* + }; + } + + use blake2::digest::consts::{U6, U8}; + hash_sizes! { + /// Account ID hash length + AccountId => U8, + /// Conversation ID hash length + ConvoId => U6, + } +} + +/// This establishes an easy to use wrapper for hashes in this crate. +/// The output is formatted string of hex characters +pub fn blake2b_hex(components: &[impl AsRef<[u8]>]) -> String { + //A + let mut hash = Blake2b::::new(); + + for c in components { + hash.update(c); + } + + let output = hash.finalize(); + hex::encode(output) +} + +/// Shorten byte slices for testing and logging +#[allow(unused)] +pub fn hex_trunc(data: &[u8]) -> String { + if data.len() <= 8 { + hex::encode(data) + } else { + format!( + "{}..{}", + hex::encode(&data[..4]), + hex::encode(&data[data.len() - 4..]) + ) + } +} diff --git a/core/integration_tests_core/Cargo.toml b/core/integration_tests_core/Cargo.toml index c9ade4b..3fd5ee8 100644 --- a/core/integration_tests_core/Cargo.toml +++ b/core/integration_tests_core/Cargo.toml @@ -14,5 +14,7 @@ libchat = { workspace = true } logos-account = { workspace = true, features = ["dev"]} storage = { workspace = true } +core_client = {path = "../core_client"} + # External dependencies (sorted) tempfile = "3" diff --git a/core/integration_tests_core/tests/dev_tests.rs b/core/integration_tests_core/tests/dev_tests.rs new file mode 100644 index 0000000..353efd1 --- /dev/null +++ b/core/integration_tests_core/tests/dev_tests.rs @@ -0,0 +1,104 @@ +use std::ops::{Deref, DerefMut}; + +use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; + +use core_client::{ChatError, CoreClient}; +use libchat::{ContentData, hex_trunc}; +use logos_account::TestLogosAccount; + +struct PollableClient { + inner: CoreClient, + on_content: Option>, +} + +impl PollableClient { + fn init( + ctx: CoreClient, + cb: Option, + ) -> Self { + Self { + inner: ctx, + on_content: cb.map(|f| Box::new(f) as Box), + } + } + + fn process_messages(&mut self) { + let messages = self.inner.ds().poll_all(); + for data in messages { + let res = self.handle_payload(&data).unwrap(); + if let Some(cb) = &self.on_content + && let Some(content_data) = res + { + cb(content_data); + } + } + } +} + +impl Deref for PollableClient { + type Target = CoreClient; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for PollableClient { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +fn process(clients: &mut Vec) { + for client in clients { + client.process_messages(); + } +} + +// Higher order function to handle printing +fn pretty_print(prefix: impl Into) -> Box { + let prefix = prefix.into(); + Box::new(move |c: ContentData| { + let cid = hex_trunc(c.conversation_id.as_bytes()); + let content = String::from_utf8(c.data).unwrap(); + println!("{} ({:?}) {}", prefix, cid, content) + }) +} + +#[test] +fn core_client() { + let ds = LocalBroadcaster::new(); + let rs = EphemeralRegistry::new(); + + let saro_account = TestLogosAccount::new("saro"); + + let raya_account = TestLogosAccount::new("raya"); + + let saro = CoreClient::new(saro_account, ds.clone(), rs.clone(), MemStore::new()).unwrap(); + let raya = CoreClient::new(raya_account, ds, rs, MemStore::new()).unwrap(); + + let mut clients = vec![ + PollableClient::init(saro, Some(pretty_print(" Saro "))), + PollableClient::init(raya, Some(pretty_print(" Raya "))), + ]; + + const SARO: usize = 0; + const RAYA: usize = 1; + + let s_convo = clients[SARO] + .create_group_convo(&[&clients[RAYA].account_id()]) + .unwrap(); + + process(&mut clients); + + s_convo.send_content(b"HI").unwrap(); + let convo_id = clients[RAYA].list_conversations().unwrap().pop().unwrap(); + let r_convo = clients[RAYA].convo(&convo_id).expect("Convo exists"); + process(&mut clients); + r_convo.send_content(b"PEW").unwrap(); + process(&mut clients); + + s_convo.send_content(b"SARO again").unwrap(); + process(&mut clients); + println!("Hello"); +} diff --git a/extensions/components/src/delivery/local_broadcaster.rs b/extensions/components/src/delivery/local_broadcaster.rs index 5889def..39a6c2b 100644 --- a/extensions/components/src/delivery/local_broadcaster.rs +++ b/extensions/components/src/delivery/local_broadcaster.rs @@ -85,6 +85,10 @@ impl LocalBroadcaster { } } + pub fn poll_all(&mut self) -> Vec> { + std::iter::from_fn(|| self.poll()).collect() + } + fn msg_id(msg: &AddressedEnvelope) -> u64 { let mut hasher = DefaultHasher::new(); msg.data.as_slice().hash(&mut hasher); From 6dc027124f47f0a6b783ccd17d4bc60f4a59cd67 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Tue, 12 May 2026 17:30:22 -0700 Subject: [PATCH 39/39] Add ExternalServices Trait --- core/core_client/src/conversation.rs | 44 ++++++--- core/core_client/src/conversation/group_v1.rs | 33 +++---- core/core_client/src/core_client.rs | 98 ++++++++++--------- core/core_client/src/inbox_v2.rs | 21 ++-- 4 files changed, 104 insertions(+), 92 deletions(-) diff --git a/core/core_client/src/conversation.rs b/core/core_client/src/conversation.rs index 58aa4f1..e257c6b 100644 --- a/core/core_client/src/conversation.rs +++ b/core/core_client/src/conversation.rs @@ -12,40 +12,56 @@ pub use group_v1::GroupV1Convo; pub type ConversationIdRef<'a> = &'a str; pub type ConversationId = String; -pub struct ServiceContext { - pub identity_provider: IP, - pub ds: DS, - pub rs: RS, +/// A trait which bundles all the external service traits into a single scope. +/// This allows for a single bound to be used internally, and cuts down on +/// the clutter +pub trait ExternalServices: Debug { + type IP: IdentityProvider; + type DS: DeliveryService; + type RS: RegistrationService; +} + +#[derive(Debug)] +pub struct ServiceContext { + pub(crate) identity_provider: S::IP, + pub(crate) ds: S::DS, + pub(crate) rs: S::RS, +} + +impl ServiceContext { + pub fn new(identity_provider: S::IP, ds: S::DS, rs: S::RS) -> Self { + ServiceContext { + identity_provider, + ds, + rs, + } + } } pub trait Id: Debug { fn id(&self) -> ConversationIdRef<'_>; } -pub trait BaseConvo: - Id + Debug -{ - fn init(&self, service_ctx: &mut ServiceContext) -> Result<(), ChatError>; +pub trait BaseConvo: Id + Debug { + fn init(&self, service_ctx: &mut ServiceContext) -> Result<(), ChatError>; fn send_content( &mut self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, content: &[u8], ) -> Result<(), ChatError>; fn handle_frame( &mut self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, enc_payload: EncryptedPayload, ) -> Result, ChatError>; } -pub trait BaseGroupConvo: - BaseConvo -{ +pub trait BaseGroupConvo: BaseConvo { fn add_member( &mut self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, members: &[&AccountId], ) -> Result<(), ChatError>; } diff --git a/core/core_client/src/conversation/group_v1.rs b/core/core_client/src/conversation/group_v1.rs index 91ee1cf..d54a506 100644 --- a/core/core_client/src/conversation/group_v1.rs +++ b/core/core_client/src/conversation/group_v1.rs @@ -11,7 +11,7 @@ use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::*; use crate::AccountId; -use crate::conversation::{ConversationIdRef, ServiceContext}; +use crate::conversation::{ConversationIdRef, ExternalServices, ServiceContext}; use crate::inbox_v2::{MlsIdentityProvider, MlsProvider}; use crate::{ AddressedEncryptedPayload, ContentData, DeliveryService, IdentityProvider, RegistrationService, @@ -128,15 +128,12 @@ where } } -impl BaseConvo for GroupV1Convo +impl BaseConvo for GroupV1Convo where - IP: IdentityProvider, + S: ExternalServices, MP: MlsProvider, - DS: DeliveryService, - RS: RegistrationService, - // KP: RegistrationService, { - fn init(&self, service_ctx: &mut super::ServiceContext) -> Result<(), ChatError> { + fn init(&self, service_ctx: &mut super::ServiceContext) -> Result<(), ChatError> { // Configure the delivery service to listen for the required delivery addresses. service_ctx @@ -153,7 +150,7 @@ where fn send_content( &mut self, - service_ctx: &mut super::ServiceContext, + service_ctx: &mut super::ServiceContext, content: &[u8], ) -> Result<(), ChatError> { let signer = MlsIdentityProvider(&service_ctx.identity_provider); @@ -182,7 +179,7 @@ where fn handle_frame( &mut self, - _service_ctx: &mut super::ServiceContext, + _service_ctx: &mut super::ServiceContext, encoded_payload: EncryptedPayload, ) -> Result, ChatError> { let bytes = match encoded_payload.encryption { @@ -231,12 +228,10 @@ where } } -impl BaseGroupConvo for GroupV1Convo +impl BaseGroupConvo for GroupV1Convo where - IP: IdentityProvider, + S: ExternalServices, MP: MlsProvider, - DS: DeliveryService, - RS: RegistrationService, { // add_members returns: // commit — the Commit message Alice broadcasts to all members @@ -244,7 +239,7 @@ where // _group_info — used for external joins; ignore for now fn add_member( &mut self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, members: &[&AccountId], ) -> Result<(), ChatError> { let mls_provider = &*self.mls_provider.borrow(); @@ -304,19 +299,15 @@ where } impl GroupV1Convo { - fn key_package_for_account< - IP: IdentityProvider, - DS: DeliveryService, - RS: RegistrationService, - >( + fn key_package_for_account( &self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, ident: &AccountId, ) -> Result { let retrieved_bytes = service_ctx .rs .retrieve(ident) - .map_err(|e: RS::Error| ChatError::Generic(e.to_string()))?; + .map_err(|e| ChatError::Generic(e.to_string()))?; // dbg!(ctx.contact_registry()); let Some(keypkg_bytes) = retrieved_bytes else { diff --git a/core/core_client/src/core_client.rs b/core/core_client/src/core_client.rs index ce0f347..b1911dc 100644 --- a/core/core_client/src/core_client.rs +++ b/core/core_client/src/core_client.rs @@ -1,8 +1,11 @@ use std::cell::RefMut; use std::collections::HashMap; +use std::fmt::Debug; use std::{cell::RefCell, rc::Rc}; -use crate::conversation::{BaseGroupConvo, ConversationId, ConversationIdRef, Id, ServiceContext}; +use crate::conversation::{ + BaseGroupConvo, ConversationId, ConversationIdRef, ExternalServices, Id, ServiceContext, +}; use crate::inbox_v2::InboxV2; use crate::{AccountId, errors::ChatError}; @@ -14,16 +17,14 @@ use prost::Message; use storage::ChatStore; #[derive(Debug)] -enum ConvoTypeOwned { - // Pairwise(Box>), - Group(Box>), +enum ConvoTypeOwned { + // Pairwise(Box>), + Group(Box>), } -impl Id for ConvoTypeOwned +impl Id for ConvoTypeOwned where - IP: IdentityProvider, - DS: DeliveryService, - RS: RegistrationService, + S: ExternalServices, { fn id(&self) -> crate::conversation::ConversationIdRef<'_> { match self { @@ -33,21 +34,14 @@ where } } -pub struct GroupConvo< - IP: IdentityProvider, - DS: DeliveryService, - RS: RegistrationService, - CS: ChatStore, -> { - client: Rc>>, +pub struct GroupConvo { + client: Rc>>, convo_id: ConversationId, } -impl GroupConvo +impl GroupConvo where - IP: IdentityProvider + 'static, - DS: DeliveryService + 'static, - RS: RegistrationService + 'static, + S: ExternalServices, CS: ChatStore + 'static, { pub fn send_content(&self, content: &[u8]) -> Result<(), ChatError> { @@ -56,20 +50,34 @@ where } } +// This allows the ExternalServices trait to be converted from a tuple. +// This is used in CoreClient to convert from the explicit impls to a +// ExternalServices bundle, which means it does not have to be exposed externally. +impl ExternalServices for (IP, DS, RS) +where + IP: IdentityProvider + Debug, + DS: DeliveryService + Debug, + RS: RegistrationService + Debug, +{ + type IP = IP; + type DS = DS; + type RS = RS; +} + pub struct CoreClient< IP: IdentityProvider, DS: DeliveryService, RS: RegistrationService, CS: ChatStore, > { - inner: Rc>>, + inner: Rc>>, } impl CoreClient where - IP: IdentityProvider + 'static, - DS: DeliveryService + 'static, - RS: RegistrationService + 'static, + IP: IdentityProvider, + DS: DeliveryService, + RS: RegistrationService, CS: ChatStore + 'static, { pub fn new(account: IP, delivery: DS, registration: RS, store: CS) -> Result { @@ -90,7 +98,7 @@ where pub fn create_group_convo( &self, participants: &[&AccountId], - ) -> Result, ChatError> { + ) -> Result, ChatError> { let convo_id = self.inner.borrow_mut().create_group_convo(participants)?; Ok(GroupConvo { client: self.inner.clone(), @@ -114,7 +122,7 @@ where self.inner.borrow_mut().handle_payload(payload) } - pub fn convo(&self, convo_id: ConversationIdRef) -> Option> { + pub fn convo(&self, convo_id: ConversationIdRef) -> Option> { let client = self.inner.clone(); if !client.borrow().has_conversation(convo_id) { @@ -128,36 +136,32 @@ where } } -struct InnerClient< - IP: IdentityProvider, - DS: DeliveryService, - RS: RegistrationService, - CS: ChatStore, -> { - service_ctx: ServiceContext, +struct InnerClient { + service_ctx: ServiceContext, _store: Rc>, pq_inbox: InboxV2, // Cache of loaded conversations - cached_convos: HashMap>, + cached_convos: HashMap>, } -impl InnerClient +impl InnerClient where - IP: IdentityProvider + 'static, - DS: DeliveryService + 'static, - RS: RegistrationService + 'static, + S: ExternalServices, CS: ChatStore + 'static, { - pub fn new(account: IP, delivery: DS, registration: RS, store: CS) -> Result { + pub fn new( + account: S::IP, + delivery: S::DS, + registration: S::RS, + store: CS, + ) -> Result { // Services for sharing with Converastions/Inboxes - let mut service_ctx = ServiceContext { - identity_provider: account, - ds: delivery, - rs: registration, - }; + // let mut service_ctx: ServiceContext = ServiceContext::new(account, delivery, registration); + let mut service_ctx: ServiceContext = + ServiceContext::new(account, delivery, registration); // let contact_registry = Rc::new(RefCell::new(registration)); let _store = Rc::new(RefCell::new(store)); @@ -179,7 +183,7 @@ where }) } - pub fn ds(&mut self) -> &mut DS { + pub fn ds(&mut self) -> &mut S::DS { &mut self.service_ctx.ds } @@ -190,7 +194,7 @@ where pub fn create_group_convo(&mut self, participants: &[&AccountId]) -> Result { let convo = self.pq_inbox.create_group_v1(&mut self.service_ctx)?; - let mut convo: Box> = Box::new(convo); + let mut convo: Box> = Box::new(convo); convo.init(&mut self.service_ctx)?; convo.add_member(&mut self.service_ctx, participants)?; @@ -242,7 +246,7 @@ where // Dispatch encrypted payload to Inbox, and register the created Conversation fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result, ChatError> { if let Some(convo) = self.pq_inbox.handle_frame(&mut self.service_ctx, payload)? { - let convo: Box> = Box::new(convo); + let convo: Box> = Box::new(convo); self.register_convo(ConvoTypeOwned::Group(convo))?; } Ok(None) @@ -267,7 +271,7 @@ where convo.handle_frame(&mut self.service_ctx, enc_payload) } - fn register_convo(&mut self, convo: ConvoTypeOwned) -> Result<(), ChatError> { + fn register_convo(&mut self, convo: ConvoTypeOwned) -> Result<(), ChatError> { let res = self.cached_convos.insert(convo.id().to_string(), convo); match res { diff --git a/core/core_client/src/inbox_v2.rs b/core/core_client/src/inbox_v2.rs index 2e5d00b..7bf4b14 100644 --- a/core/core_client/src/inbox_v2.rs +++ b/core/core_client/src/inbox_v2.rs @@ -20,6 +20,7 @@ use crate::DeliveryService; use crate::IdentityProvider; use crate::RegistrationService; use crate::conversation::BaseConvo; +use crate::conversation::ExternalServices; use crate::conversation::ServiceContext; use crate::conversation::{GroupV1Convo, Id}; use crate::utils::{blake2b_hex, hash_size}; @@ -166,8 +167,8 @@ pub struct InboxV2 { } impl InboxV2 { - pub fn new( - service_ctx: &mut ServiceContext, + pub fn new( + service_ctx: &mut ServiceContext, _store: Rc>, ) -> Self { // Avoid referencing a temporary value by caching it. @@ -193,9 +194,9 @@ impl InboxV2 { } /// Submit MlsKeypackage to registration service - pub fn register( + pub fn register( &self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, ) -> Result<(), ChatError> { let mls_ident = MlsIdentityProvider(&service_ctx.identity_provider); let keypackage_bytes = self @@ -213,9 +214,9 @@ impl InboxV2 { .map_err(ChatError::generic) } - pub fn create_group_v1( + pub fn create_group_v1( &self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, ) -> Result, ChatError> { let mls_ident = MlsIdentityProvider(&service_ctx.identity_provider); GroupV1Convo::new(mls_ident, self.mls_provider.clone()) @@ -247,9 +248,9 @@ impl InboxV2 { } impl InboxV2 { - pub fn handle_frame( + pub fn handle_frame( &self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, payload_bytes: &[u8], ) -> Result>, ChatError> { let inbox_frame = InboxV2Frame::decode(payload_bytes)?; @@ -265,9 +266,9 @@ impl InboxV2 { } } - fn handle_heavy_invite( + fn handle_heavy_invite( &self, - service_ctx: &mut ServiceContext, + service_ctx: &mut ServiceContext, invite: GroupV1HeavyInvite, ) -> Result, ChatError> { let (msg_in, _rest) = MlsMessageIn::tls_deserialize_bytes(invite.welcome_bytes.as_slice())?;