Remove ClientCtx

This commit is contained in:
Jazz Turner-Baggs 2026-04-24 16:59:20 -07:00
parent 3bf8ecb904
commit d8e15dc8d6
No known key found for this signature in database
13 changed files with 245 additions and 278 deletions

1
Cargo.lock generated
View File

@ -1424,6 +1424,7 @@ dependencies = [
"double-ratchets", "double-ratchets",
"hex", "hex",
"openmls", "openmls",
"openmls_libcrux_crypto 0.3.1",
"openmls_traits 0.5.0", "openmls_traits 0.5.0",
"prost", "prost",
"rand_core 0.6.4", "rand_core 0.6.4",

View File

@ -21,6 +21,7 @@ thiserror = "2.0.17"
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] } x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }
storage = { path = "../storage" } storage = { path = "../storage" }
openmls = { version = "0.8.1", features = ["libcrux-provider"] } openmls = { version = "0.8.1", features = ["libcrux-provider"] }
openmls_libcrux_crypto = "0.3.1"
openmls_traits = "0.5.0" openmls_traits = "0.5.0"
[dev-dependencies] [dev-dependencies]

View File

@ -18,11 +18,11 @@ impl LogosAccount {
/// TODO: (P1) Remove once implementation is ready. /// TODO: (P1) Remove once implementation is ready.
pub fn new_test(explicit_id: impl Into<String>) -> Self { pub fn new_test(explicit_id: impl Into<String>) -> Self {
let signing_key = Ed25519SigningKey::generate(); let signing_key = Ed25519SigningKey::generate();
let verifying_key = signing_key.verifying_key() let verifying_key = signing_key.verifying_key();
Self { Self {
id: AccountId::new(explicit_id.into()), id: AccountId::new(explicit_id.into()),
signing_key, signing_key,
verifying_key verifying_key,
} }
} }

View File

@ -1,12 +1,10 @@
use std::cell::Ref; use std::cell::{Ref, RefMut};
use std::sync::Arc; use std::sync::Arc;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use crate::account::LogosAccount; use crate::account::LogosAccount;
use crate::conversation::{Convo, GroupConvo, IdentityProvider}; use crate::conversation::{Convo, GroupConvo};
use crate::ctx::ClientCtx;
use crate::account::LogosAccount;
use crate::{DeliveryService, RegistrationService}; use crate::{DeliveryService, RegistrationService};
use crate::{ use crate::{
conversation::{Conversation, Id, PrivateV1Convo}, conversation::{Conversation, Id, PrivateV1Convo},
@ -26,13 +24,18 @@ pub use crate::inbox::Introduction;
// Ctx manages lifetimes of objects to process and generate payloads. // Ctx manages lifetimes of objects to process and generate payloads.
pub struct Context<DS: DeliveryService, RS: RegistrationService, CS: ChatStore> { pub struct Context<DS: DeliveryService, RS: RegistrationService, CS: ChatStore> {
identity: Rc<Identity>, identity: Rc<Identity>,
client_ctx: ClientCtx<DS, RS, CS>, ds: Rc<RefCell<DS>>,
inbox: Inbox<CS>,
pq_inbox: InboxV2,
store: Rc<RefCell<CS>>, store: Rc<RefCell<CS>>,
inbox: Inbox<CS>,
pq_inbox: InboxV2<DS, RS, CS>,
} }
impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Context<DS, RS, CS> { impl<DS, RS, CS> Context<DS, RS, CS>
where
DS: DeliveryService + 'static,
RS: RegistrationService + 'static,
CS: ChatStore + 'static,
{
/// Opens or creates a Context with the given storage configuration. /// Opens or creates a Context with the given storage configuration.
/// ///
/// If an identity exists in storage, it will be restored. /// If an identity exists in storage, it will be restored.
@ -40,13 +43,15 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
pub fn new_from_store( pub fn new_from_store(
name: impl Into<String>, name: impl Into<String>,
delivery: DS, delivery: DS,
contact_reg: RS, registration: RS,
store: CS, store: CS,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let name = name.into(); 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 store = Rc::new(RefCell::new(store));
let mut ctx = ClientCtx::new(delivery, contact_reg, store.clone());
// Load or create identity // Load or create identity
let identity = if let Some(identity) = store.borrow().load_identity()? { let identity = if let Some(identity) = store.borrow().load_identity()? {
@ -60,20 +65,24 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
let identity = Rc::new(identity); let identity = Rc::new(identity);
let inbox = Inbox::new(Rc::clone(&store), Rc::clone(&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 // Subscribe
ctx.ds() ds.borrow_mut()
.subscribe(&pq_inbox.delivery_address()) .subscribe(&pq_inbox.delivery_address())
.map_err(ChatError::generic)?; .map_err(ChatError::generic)?;
Ok(Self { Ok(Self {
identity: identity, identity: identity,
client_ctx: ctx, ds,
store,
inbox, inbox,
pq_inbox, pq_inbox,
store,
account: LogosAccount::new_test(name.as_str()),
}) })
} }
@ -83,47 +92,55 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
pub fn new_with_name( pub fn new_with_name(
name: impl Into<String>, name: impl Into<String>,
delivery: DS, delivery: DS,
contact_reg: RS, registration: RS,
chat_store: CS, chat_store: CS,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let name = name.into(); let name = name.into();
let identity = Identity::new(&name); let identity = Identity::new(&name);
let chat_store = Rc::new(RefCell::new(chat_store)); // Services for sharing with Converastions/Inboxes
let mut ctx = ClientCtx::new(delivery, contact_reg, chat_store.clone()); let ds = Rc::new(RefCell::new(delivery));
chat_store let contact_registry = Rc::new(RefCell::new(registration));
let store = Rc::new(RefCell::new(chat_store));
store
.borrow_mut() .borrow_mut()
.save_identity(&identity) .save_identity(&identity)
.expect("in-memory storage should not fail"); .expect("in-memory storage should not fail");
let identity = Rc::new(identity); let identity = Rc::new(identity);
let inbox = Inbox::new(Rc::clone(&chat_store), Rc::clone(&identity)); let inbox = Inbox::new(store.clone(), Rc::clone(&identity));
let mut pq_inbox = InboxV2::new_with_account(LogosAccount::new_test(name)); let mut pq_inbox = InboxV2::new(
pq_inbox.register(&mut ctx)?; 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()) .subscribe(&pq_inbox.delivery_address())
.map_err(ChatError::generic)?; .map_err(ChatError::generic)?;
Ok(Self { Ok(Self {
identity, identity,
client_ctx: ctx, ds,
store,
pq_inbox, pq_inbox,
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> { pub fn store(&self) -> Ref<'_, CS> {
self.store.borrow() self.store.borrow()
} }
pub fn client_ctx(&mut self) -> &mut ClientCtx<DS, RS, CS> {
&mut self.client_ctx
}
pub fn identity(&self) -> &Identity { pub fn identity(&self) -> &Identity {
&self.identity &self.identity
} }
@ -164,16 +181,17 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
pub fn create_group_convo( pub fn create_group_convo(
&mut self, &mut self,
participants: &[&AccountId], participants: &[&AccountId],
) -> Result<Box<dyn GroupConvo<DS, RS, CS>>, ChatError> { ) -> Result<Box<dyn GroupConvo<DS, RS>>, ChatError> {
let mut convo = self.pq_inbox.create_group_v1(&mut self.client_ctx)?; // TODO: (!) Perform this in InboxV2?
self.client_ctx let mut convo = self.pq_inbox.create_group_v1()?;
.store() self.store
.borrow_mut()
.save_conversation(&storage::ConversationMeta { .save_conversation(&storage::ConversationMeta {
local_convo_id: convo.id().to_string(), local_convo_id: convo.id().to_string(),
remote_convo_id: "0".into(), remote_convo_id: "0".into(),
kind: ConversationKind::GroupV1, kind: ConversationKind::GroupV1,
})?; })?;
convo.add_member(&mut self.client_ctx, participants)?; convo.add_member(participants)?;
Ok(Box::new(convo)) Ok(Box::new(convo))
} }
@ -246,7 +264,7 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
// Dispatch encrypted payload to Inbox, and register the created Conversation // Dispatch encrypted payload to Inbox, and register the created Conversation
fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result<Option<ContentData>, ChatError> { fn dispatch_to_inbox2(&mut self, payload: &[u8]) -> Result<Option<ContentData>, ChatError> {
self.pq_inbox.handle_frame(&mut self.client_ctx, payload)?; self.pq_inbox.handle_frame(payload)?;
Ok(None) Ok(None)
} }
@ -270,7 +288,7 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
pub fn get_convo( pub fn get_convo(
&mut self, &mut self,
convo_id: ConversationId, convo_id: ConversationId,
) -> Result<Box<dyn GroupConvo<DS, RS, CS>>, ChatError> { ) -> Result<Box<dyn GroupConvo<DS, RS>>, ChatError> {
self.load_group_convo(convo_id) self.load_group_convo(convo_id)
} }
@ -292,8 +310,7 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
Ok(Box::new(private_convo)) Ok(Box::new(private_convo))
} }
ConversationKind::GroupV1 => Ok(Box::new( ConversationKind::GroupV1 => Ok(Box::new(
self.pq_inbox self.pq_inbox.load_mls_convo(record.local_convo_id)?,
.load_mls_convo(&mut self.client_ctx, record.local_convo_id)?,
)), )),
ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!(
"unsupported conversation type: {}", "unsupported conversation type: {}",
@ -306,7 +323,7 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
fn load_group_convo( fn load_group_convo(
&mut self, &mut self,
convo_id: ConversationId, convo_id: ConversationId,
) -> Result<Box<dyn GroupConvo<DS, RS, CS>>, ChatError> { ) -> Result<Box<dyn GroupConvo<DS, RS>>, ChatError> {
let record = self let record = self
.store .store
.borrow() .borrow()
@ -318,8 +335,7 @@ impl<DS: DeliveryService, RS: RegistrationService, CS: ChatStore + 'static> Cont
Err(ChatError::NoConvo("This is not a group convo".into())) Err(ChatError::NoConvo("This is not a group convo".into()))
} }
ConversationKind::GroupV1 => Ok(Box::new( ConversationKind::GroupV1 => Ok(Box::new(
self.pq_inbox self.pq_inbox.load_mls_convo(record.local_convo_id)?,
.load_mls_convo(&mut self.client_ctx, record.local_convo_id)?,
)), )),
ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!( ConversationKind::Unknown(_) => Err(ChatError::BadBundleValue(format!(
"unsupported conversation type: {}", "unsupported conversation type: {}",

View File

@ -2,14 +2,14 @@ pub mod group_v1;
mod privatev1; mod privatev1;
use crate::{ use crate::{
DeliveryService, service_traits::KeyPackageProvider, DeliveryService,
ctx::ClientCtx, service_traits::KeyPackageProvider,
types::{AccountId, AddressedEncryptedPayload, ContentData}, types::{AccountId, AddressedEncryptedPayload, ContentData},
}; };
use chat_proto::logoschat::encryption::EncryptedPayload; use chat_proto::logoschat::encryption::EncryptedPayload;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use storage::{ChatStore, ConversationKind, ConversationStore, RatchetStore}; use storage::{ConversationKind, ConversationStore, RatchetStore};
pub use crate::errors::ChatError; pub use crate::errors::ChatError;
pub use group_v1::{GroupV1Convo, IdentityProvider}; pub use group_v1::{GroupV1Convo, IdentityProvider};
@ -42,27 +42,12 @@ pub trait Convo: Id + Debug {
fn convo_type(&self) -> ConversationKind; fn convo_type(&self) -> ConversationKind;
} }
pub trait GroupConvo<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>: Convo { pub trait GroupConvo<DS: DeliveryService, RS: KeyPackageProvider>: Convo {
fn add_member( fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError>;
&mut self,
ctx: &mut ClientCtx<DS, RS, CS>,
members: &[&AccountId],
) -> Result<(), ChatError>;
// Default implementation which dispatches envelopes to the DeliveryService // This is intended to replace `send_message`. The trait change is that it automatically
fn send_content( // sends the payload directly.
&mut self, fn send_content(&mut self, content: &[u8]) -> Result<(), ChatError>;
ctx: &mut ClientCtx<DS, RS, CS>,
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<S: ConversationStore + RatchetStore> { pub enum Conversation<S: ConversationStore + RatchetStore> {

View File

@ -8,13 +8,13 @@ use openmls::prelude::tls_codec::Deserialize;
use openmls::prelude::*; use openmls::prelude::*;
use openmls_libcrux_crypto::Provider as LibcruxProvider; use openmls_libcrux_crypto::Provider as LibcruxProvider;
use openmls_traits::signatures::Signer as OpenMlsSigner; use openmls_traits::signatures::Signer as OpenMlsSigner;
use storage::{ChatStore, ConversationKind}; use storage::ConversationKind;
use crate::types::AccountId; use crate::types::AccountId;
use crate::{ use crate::{
DeliveryService, service_traits::KeyPackageProvider, DeliveryService,
conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id},
ctx::ClientCtx, service_traits::KeyPackageProvider,
types::{AddressedEncryptedPayload, ContentData}, types::{AddressedEncryptedPayload, ContentData},
}; };
@ -37,21 +37,28 @@ pub trait MlsContext {
} }
} }
fn invite_user<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( fn invite_user<DS: DeliveryService>(
&self, &self,
ctx: &mut ClientCtx<DS, RS, CS>, ds: &mut DS,
account_id: &AccountId, account_id: &AccountId,
welcome: &MlsMessageOut, welcome: &MlsMessageOut,
) -> Result<(), ChatError>; ) -> Result<(), ChatError>;
} }
pub struct GroupV1Convo<Ctx: MlsContext> { pub struct GroupV1Convo<MlsCtx, DS, KP> {
ctx: Rc<RefCell<Ctx>>, ctx: Rc<RefCell<MlsCtx>>,
ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>,
pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility pub(crate) mls_group: MlsGroup, // TODO: (!) Fix Visibility
convo_id: String, convo_id: String,
} }
impl<Ctx: MlsContext> std::fmt::Debug for GroupV1Convo<Ctx> { impl<MlsCtx, DS, KP> std::fmt::Debug for GroupV1Convo<MlsCtx, DS, KP>
where
MlsCtx: MlsContext,
DS: DeliveryService,
KP: KeyPackageProvider,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GroupV1Convo") f.debug_struct("GroupV1Convo")
.field("name", &self.ctx.borrow().ident().friendly_name()) .field("name", &self.ctx.borrow().ident().friendly_name())
@ -61,8 +68,17 @@ impl<Ctx: MlsContext> std::fmt::Debug for GroupV1Convo<Ctx> {
} }
} }
impl<Ctx: MlsContext> GroupV1Convo<Ctx> { impl<MlsCtx, DS, KP> GroupV1Convo<MlsCtx, DS, KP>
pub fn new<DS: DeliveryService>(ctx: Rc<RefCell<Ctx>>, ds: &mut DS) -> Self { where
MlsCtx: MlsContext,
DS: DeliveryService,
KP: KeyPackageProvider,
{
pub fn new(
ctx: Rc<RefCell<MlsCtx>>,
ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>,
) -> Self {
let config = Self::mls_create_config(); let config = Self::mls_create_config();
let mls_group = { let mls_group = {
let ctx_ref = ctx.borrow(); let ctx_ref = ctx.borrow();
@ -75,7 +91,7 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
.unwrap() .unwrap()
}; };
let convo_id = hex::encode(mls_group.group_id().as_slice()); 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!( println!(
"@ Create Convo: {}. {}. d:{} dc:{}", "@ Create Convo: {}. {}. d:{} dc:{}",
@ -86,14 +102,17 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
); );
Self { Self {
ctx, ctx,
ds,
keypkg_provider,
mls_group, mls_group,
convo_id, convo_id,
} }
} }
pub fn new_from_welcome<DS: DeliveryService>( pub fn new_from_welcome(
ctx: Rc<RefCell<Ctx>>, ctx: Rc<RefCell<MlsCtx>>,
ds: &mut DS, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>,
welcome: Welcome, welcome: Welcome,
) -> Self { ) -> Self {
let mls_group = { let mls_group = {
@ -109,7 +128,7 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
}; };
let convo_id = hex::encode(mls_group.group_id().as_slice()); 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!( println!(
"@ Welcome Convo: I:{}. {}. d:{} dc:{}", "@ Welcome Convo: I:{}. {}. d:{} dc:{}",
@ -121,14 +140,17 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
GroupV1Convo { GroupV1Convo {
ctx, ctx,
ds,
keypkg_provider,
mls_group, mls_group,
convo_id, convo_id,
} }
} }
pub fn load<DS: DeliveryService>( pub fn load(
ctx: Rc<RefCell<Ctx>>, ctx: Rc<RefCell<MlsCtx>>,
ds: &mut DS, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>,
convo_id: String, convo_id: String,
group_id: GroupId, group_id: GroupId,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
@ -138,16 +160,18 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
return Err(ChatError::NoConvo("mls group not found".into())); return Err(ChatError::NoConvo("mls group not found".into()));
}; };
Self::subscribe(ds, &convo_id)?; Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?;
Ok(GroupV1Convo { Ok(GroupV1Convo {
ctx, ctx,
ds,
keypkg_provider,
mls_group, mls_group,
convo_id, convo_id,
}) })
} }
fn subscribe<DS: DeliveryService>(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)) ds.subscribe(&Self::delivery_address_from_id(&convo_id))
.map_err(ChatError::generic)?; .map_err(ChatError::generic)?;
ds.subscribe(&Self::ctrl_delivery_address_from_id(&convo_id)) ds.subscribe(&Self::ctrl_delivery_address_from_id(&convo_id))
@ -191,15 +215,12 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
Self::ctrl_delivery_address_from_id(&self.convo_id) Self::ctrl_delivery_address_from_id(&self.convo_id)
} }
fn key_package_for_account<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( fn key_package_for_account(&self, ident: &AccountId) -> Result<KeyPackage, ChatError> {
&self, let retrieved_bytes = self
ctx: &mut ClientCtx<DS, RS, CS>, .keypkg_provider
ident: &AccountId, .borrow()
) -> Result<KeyPackage, ChatError> {
let retrieved_bytes = ctx
.contact_registry()
.retrieve(ident) .retrieve(ident)
.map_err(|e| ChatError::Generic(e.to_string()))?; .map_err(|e: KP::Error| ChatError::Generic(e.to_string()))?;
// dbg!(ctx.contact_registry()); // dbg!(ctx.contact_registry());
let Some(keypkg_bytes) = retrieved_bytes else { let Some(keypkg_bytes) = retrieved_bytes else {
@ -215,13 +236,23 @@ impl<Ctx: MlsContext> GroupV1Convo<Ctx> {
} }
} }
impl<Ctx: MlsContext> Id for GroupV1Convo<Ctx> { impl<MlsCtx, DS, KP> Id for GroupV1Convo<MlsCtx, DS, KP>
where
MlsCtx: MlsContext,
DS: DeliveryService,
KP: KeyPackageProvider,
{
fn id(&self) -> ConversationId<'_> { fn id(&self) -> ConversationId<'_> {
&self.convo_id &self.convo_id
} }
} }
impl<Ctx: MlsContext> Convo for GroupV1Convo<Ctx> { impl<MlsCtx, DS, KP> Convo for GroupV1Convo<MlsCtx, DS, KP>
where
MlsCtx: MlsContext,
DS: DeliveryService,
KP: KeyPackageProvider,
{
fn send_message( fn send_message(
&mut self, &mut self,
content: &[u8], content: &[u8],
@ -314,14 +345,13 @@ impl<Ctx: MlsContext> Convo for GroupV1Convo<Ctx> {
} }
} }
impl<Ctx: MlsContext, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore> impl<MlsCtx, DS, KP> GroupConvo<DS, KP> for GroupV1Convo<MlsCtx, DS, KP>
GroupConvo<DS, RS, CS> for GroupV1Convo<Ctx> where
MlsCtx: MlsContext,
DS: DeliveryService,
KP: KeyPackageProvider,
{ {
fn add_member( fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> {
&mut self,
ctx: &mut ClientCtx<DS, RS, CS>,
members: &[&AccountId],
) -> Result<(), ChatError> {
// add_members returns: // add_members returns:
// commit — the Commit message Alice broadcasts to all members // commit — the Commit message Alice broadcasts to all members
// welcome — the Welcome message sent privately to each new joiner // welcome — the Welcome message sent privately to each new joiner
@ -341,7 +371,7 @@ impl<Ctx: MlsContext, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore
let keypkgs = members let keypkgs = members
.iter() .iter()
// .map(|ident| self.key_package_for_account(ctx, ident)) // .map(|ident| self.key_package_for_account(ctx, ident))
.map(|ident| self.key_package_for_account(ctx, ident)) .map(|ident| self.key_package_for_account(ident))
.collect::<Result<Vec<_>, ChatError>>()?; .collect::<Result<Vec<_>, ChatError>>()?;
let (commit, welcome, _group_info) = self let (commit, welcome, _group_info) = self
@ -353,7 +383,7 @@ impl<Ctx: MlsContext, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore
// TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users // TODO: (P3) Evaluate privacy/performance implications of an aggregated Welcome for multiple users
for account_id in members { for account_id in members {
ctx_ref.invite_user(ctx, account_id, &welcome)?; ctx_ref.invite_user(&mut *self.ds.borrow_mut(), account_id, &welcome)?;
} }
let encrypted_payload = EncryptedPayload { let encrypted_payload = EncryptedPayload {
@ -370,8 +400,20 @@ impl<Ctx: MlsContext, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore
// TODO: (P1) Make GroupConvos agnostic to framing so its less error prone and more // 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()); let env = addr_enc_payload.into_envelope(self.convo_id.clone());
ctx.ds() self.ds
.borrow_mut()
.publish(env) .publish(env)
.map_err(|e| ChatError::Generic(format!("Publish: {e}"))) .map_err(|e| ChatError::Generic(format!("Publish: {e}")))
} }
fn send_content(&mut self, content: &[u8]) -> 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(())
}
} }

View File

@ -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: DeliveryService, RS: KeyPackageProvider, CS: ChatStore> {
ds: DS,
contact_registry: RS,
convo_store: Rc<RefCell<CS>>, // TODO: (P2) Remove Rc/Refcell
}
impl<'a, DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore> ClientCtx<DS, RS, CS> {
pub fn new(ds: DS, contact_registry: RS, convo_store: Rc<RefCell<CS>>) -> 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<DS, RS, CS> {
pub fn contact_registry_mut(&'a mut self) -> &'a mut RS {
&mut self.contact_registry
}
}

View File

@ -12,15 +12,13 @@ use storage::ConversationMeta;
use crate::AddressedEnvelope; use crate::AddressedEnvelope;
use crate::ChatError; use crate::ChatError;
use crate::DeliveryService; use crate::DeliveryService;
use crate::RegistrationService;
use crate::account::LogosAccount; use crate::account::LogosAccount;
use crate::conversation::GroupConvo; use crate::conversation::GroupConvo;
use crate::conversation::group_v1::MlsContext; use crate::conversation::group_v1::MlsContext;
use crate::conversation::{GroupV1Convo, IdentityProvider}; use crate::conversation::{GroupV1Convo, IdentityProvider};
use crate::ctx::ClientCtx;
use crate::types::AccountId; use crate::types::AccountId;
use crate::utils::{blake2b_hex, hash_size}; use crate::utils::{blake2b_hex, hash_size};
use crate::RegistrationService;
use crate::service_traits::KeyPackageProvider;
pub struct PqMlsContext { pub struct PqMlsContext {
ident_provider: LogosAccount, ident_provider: LogosAccount,
provider: LibcruxProvider, provider: LibcruxProvider,
@ -37,9 +35,9 @@ impl MlsContext for PqMlsContext {
&self.provider &self.provider
} }
fn invite_user<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( fn invite_user<DS: DeliveryService>(
&self, &self,
ctx: &mut ClientCtx<DS, RS, CS>, ds: &mut DS,
account_id: &AccountId, account_id: &AccountId,
welcome: &MlsMessageOut, welcome: &MlsMessageOut,
) -> Result<(), ChatError> { ) -> Result<(), ChatError> {
@ -62,7 +60,7 @@ impl MlsContext for PqMlsContext {
data: envelope.encode_to_vec(), data: envelope.encode_to_vec(),
}; };
ctx.ds().publish(outbound_msg).map_err(ChatError::generic)?; ds.publish(outbound_msg).map_err(ChatError::generic)?;
Ok(()) Ok(())
} }
} }
@ -81,17 +79,33 @@ impl InboxProtocolParams {
type ProtocolParams = InboxProtocolParams; type ProtocolParams = InboxProtocolParams;
pub struct InboxV2 { pub struct InboxV2<DS, RS, CS> {
account_id: AccountId, account_id: AccountId,
ds: Rc<RefCell<DS>>,
reg_service: Rc<RefCell<RS>>,
store: Rc<RefCell<CS>>,
ctx: Rc<RefCell<PqMlsContext>>, ctx: Rc<RefCell<PqMlsContext>>,
} }
impl<'a> InboxV2 { impl<'a, DS, CS, RS> InboxV2<DS, RS, CS>
pub fn new_with_account(account: LogosAccount) -> Self { where
DS: DeliveryService,
RS: RegistrationService,
CS: ChatStore,
{
pub fn new(
account: LogosAccount,
ds: Rc<RefCell<DS>>,
reg_service: Rc<RefCell<RS>>,
store: Rc<RefCell<CS>>,
) -> Self {
let account_id = account.account_id().clone(); let account_id = account.account_id().clone();
let provider = LibcruxProvider::new().unwrap(); let provider = LibcruxProvider::new().unwrap();
Self { Self {
account_id, account_id,
ds,
reg_service,
store,
ctx: Rc::new(RefCell::new(PqMlsContext { ctx: Rc::new(RefCell::new(PqMlsContext {
ident_provider: account, ident_provider: account,
provider, provider,
@ -103,18 +117,19 @@ impl<'a> InboxV2 {
&self.account_id &self.account_id
} }
pub fn register<DS: DeliveryService, RS: RegistrationService, CS: ChatStore>( /// Submit MlsKeypackage to registration service
&mut self, pub fn register(&mut self) -> Result<(), ChatError> {
ctx: &mut ClientCtx<DS, RS, CS>, let keypackage_bytes = self.create_keypackage()?.tls_serialize_detached()?;
) -> Result<(), ChatError> {
let keypackage = self.create_keypackage()?;
let bytes = keypackage.tls_serialize_detached()?; // TODO: (P3) Each keypackage can only be used once either enable...
// "LastResort" package or publish multiple
ctx.contact_registry_mut() self.reg_service
.register(&self.ctx.borrow().ident_provider.friendly_name(), bytes) .borrow_mut()
.map_err(ChatError::generic)?; //TODO: (P1) create an address scheme instead of using names .register(
Ok(()) &self.ctx.borrow().ident_provider.friendly_name(),
keypackage_bytes,
)
.map_err(ChatError::generic)
} }
pub fn delivery_address(&self) -> String { pub fn delivery_address(&self) -> String {
@ -125,19 +140,12 @@ impl<'a> InboxV2 {
ProtocolParams::conversation_id_for_account_id(&self.account_id) ProtocolParams::conversation_id_for_account_id(&self.account_id)
} }
pub fn create_group_v1<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( pub fn create_group_v1(&self) -> Result<GroupV1Convo<PqMlsContext, DS, RS>, ChatError> {
&self, let convo = GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone());
ctx: &mut ClientCtx<DS, RS, CS>,
) -> Result<GroupV1Convo<PqMlsContext>, ChatError> {
let convo = GroupV1Convo::new(self.assemble_ctx(), ctx.ds());
Ok(convo) Ok(convo)
} }
pub fn handle_frame<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( pub fn handle_frame(&self, payload_bytes: &[u8]) -> Result<(), ChatError> {
&self,
ctx: &mut ClientCtx<DS, RS, CS>,
payload_bytes: &[u8],
) -> Result<(), ChatError> {
let inbox_frame = InboxV2Frame::decode(payload_bytes)?; let inbox_frame = InboxV2Frame::decode(payload_bytes)?;
let Some(payload) = inbox_frame.payload else { let Some(payload) = inbox_frame.payload else {
@ -146,20 +154,12 @@ impl<'a> InboxV2 {
match payload { match payload {
InviteType::GroupV1(group_v1_heavy_invite) => { 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<RefCell<PqMlsContext>> { fn persist_convo(&self, convo: impl GroupConvo<DS, RS>) -> Result<(), ChatError> {
self.ctx.clone()
}
fn persist_convo<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>(
&self,
ctx: &'a ClientCtx<DS, RS, CS>,
convo: impl GroupConvo<DS, RS, CS>,
) -> Result<(), ChatError> {
// TODO: (P2) Remove remote_convo_id this is an implementation detail specific to PrivateV1 // TODO: (P2) Remove remote_convo_id this is an implementation detail specific to PrivateV1
// TODO: (P3) Implement From<Convo> for ConversationMeta // TODO: (P3) Implement From<Convo> for ConversationMeta
let meta = ConversationMeta { let meta = ConversationMeta {
@ -167,16 +167,12 @@ impl<'a> InboxV2 {
remote_convo_id: "0".into(), remote_convo_id: "0".into(),
kind: storage::ConversationKind::GroupV1, kind: storage::ConversationKind::GroupV1,
}; };
ctx.store().save_conversation(&meta)?; self.store.borrow_mut().save_conversation(&meta)?;
// TODO: (P1) Persist state // TODO: (P1) Persist state
Ok(()) Ok(())
} }
fn handle_heavy_invite<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( fn handle_heavy_invite(&self, invite: GroupV1HeavyInvite) -> Result<(), ChatError> {
&self,
ctx: &mut ClientCtx<DS, RS, CS>,
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 { 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); let convo = GroupV1Convo::new_from_welcome(
self.persist_convo(ctx, convo) self.ctx.clone(),
self.ds.clone(),
self.reg_service.clone(),
welcome,
);
self.persist_convo(convo)
} }
fn create_keypackage(&self) -> Result<KeyPackage, ChatError> { fn create_keypackage(&self) -> Result<KeyPackage, ChatError> {
@ -211,14 +212,19 @@ impl<'a> InboxV2 {
Ok(a.key_package().clone()) Ok(a.key_package().clone())
} }
pub fn load_mls_convo<DS: DeliveryService, RS: KeyPackageProvider, CS: ChatStore>( pub fn load_mls_convo(
&self, &self,
ctx: &mut ClientCtx<DS, RS, CS>,
convo_id: String, convo_id: String,
) -> Result<GroupV1Convo<PqMlsContext>, ChatError> { ) -> Result<GroupV1Convo<PqMlsContext, DS, RS>, ChatError> {
let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?; let group_id_bytes = hex::decode(&convo_id).map_err(ChatError::generic)?;
let group_id = GroupId::from_slice(&group_id_bytes); 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) Ok(convo)
} }

View File

@ -2,12 +2,11 @@ mod account;
mod context; mod context;
mod conversation; mod conversation;
mod crypto; mod crypto;
mod ctx;
mod errors; mod errors;
mod service_traits;
mod inbox; mod inbox;
mod inbox_v2; mod inbox_v2;
mod proto; mod proto;
mod service_traits;
mod types; mod types;
mod utils; mod utils;

View File

@ -10,7 +10,7 @@ use storage::{ConversationMeta, ConversationStore, IdentityStore};
use storage::{EphemeralKeyStore, RatchetStore}; use storage::{EphemeralKeyStore, RatchetStore};
use crate::{ use crate::{
AccountId, AddressedEnvelope, DeliveryService, RegistrationService, KeyPackageProvider, AccountId, AddressedEnvelope, DeliveryService, RegistrationService,
utils::{blake2b_hex, hash_size::Testing}, 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; type Error = String;
fn register(&mut self, identity: &str, key_bundle: Vec<u8>) -> Result<(), Self::Error> {
self.registry
.lock()
.unwrap()
.insert(identity.to_string(), key_bundle);
Ok(())
}
fn retrieve(&self, identity: &AccountId) -> Result<Option<Vec<u8>>, Self::Error> { fn retrieve(&self, identity: &AccountId) -> Result<Option<Vec<u8>>, Self::Error> {
Ok(self.registry.lock().unwrap().get(identity.as_str()).cloned()) Ok(self
} .registry
} .lock()
.unwrap()
impl RegistrationService for EphemeralRegistry { .get(identity.as_str())
fn register(&mut self, identity: &str, key_bundle: Vec<u8>) -> Result<(), Self::Error> { .cloned())
self.registry.lock().unwrap().insert(identity.to_string(), key_bundle);
Ok(())
} }
} }
@ -292,13 +297,3 @@ impl RatchetStore for MemStore {
todo!() 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<u8> {
// self.state.get(convo_id).unwrap().clone()
// }
// }

View File

@ -33,13 +33,12 @@ pub mod hash_size {
}; };
} }
use blake2::digest::consts::{U4, U8, U18}; use blake2::digest::consts::{U4, U8};
hash_sizes! { hash_sizes! {
/// Generic hash size for tests and debug /// Generic hash size for tests and debug
Testing => U4, Testing => U4,
/// Account ID hash length /// Account ID hash length
AccountId => U8, AccountId => U8,
ConversationId => U18,
} }
} }

View File

@ -1,25 +1,7 @@
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; use components::{EphemeralRegistry, LocalBroadcaster, MemStore};
use libchat::{ChatStorage, ContentData, Context, ConversationId, GroupConvo, hex_trunc}; use libchat::{ContentData, Context, GroupConvo, hex_trunc};
type TestContext = Context<LocalBroadcaster, EphemeralRegistry, ChatStorage>;
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 // Simple client Functionality for testing
struct Client { struct Client {
@ -39,12 +21,16 @@ impl Client {
} }
fn process_messages(&mut self) { 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(); let res = self.handle_payload(&data).unwrap();
if let Some(cb) = &self.on_content { if let Some(cb) = &self.on_content {
match res { if let Some(content_data) = res {
Some(content_data) => cb(content_data), cb(content_data);
None => continue,
} }
} }
} }
@ -53,7 +39,7 @@ impl Client {
fn convo( fn convo(
&mut self, &mut self,
convo_id: &str, convo_id: &str,
) -> Box<dyn GroupConvo<LocalBroadcaster, EphemeralRegistry, MemStore>> { ) -> Box<dyn GroupConvo<LocalBroadcaster, EphemeralRegistry>> {
// TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync // TODO: (P1) Convos are being copied somewhere, which means hanging on to a reference causes state desync
self.get_convo(convo_id).unwrap() self.get_convo(convo_id).unwrap()
} }
@ -117,24 +103,16 @@ fn create_group() {
clients[SARO] clients[SARO]
.convo(convo_id) .convo(convo_id)
.send_content( .send_content(b"ok who broke the group chat again")
&mut clients[SARO].client_ctx(),
b"ok who broke the group chat again",
)
.unwrap(); .unwrap();
// clients[SARO].process_messages();
process(&mut clients); process(&mut clients);
clients[RAYA] clients[RAYA]
.convo(convo_id) .convo(convo_id)
.send_content( .send_content(b"it was literally working five minutes ago")
&mut clients[RAYA].client_ctx(),
b"it was literally working five minutes ago",
)
.unwrap(); .unwrap();
// clients[SARO].process_messages();
process(&mut clients); process(&mut clients);
let pax_ctx = Context::new_with_name("pax", ds, rs, MemStore::new()).unwrap(); 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(); let pax_id = clients[PAX].account_id().clone();
clients[SARO] clients[SARO]
.convo(convo_id) .convo(convo_id)
.add_member(&mut clients[SARO].client_ctx(), &[&pax_id]) .add_member(&[&pax_id])
.unwrap(); .unwrap();
// clients[SARO].process_messages();
process(&mut clients); process(&mut clients);
clients[PAX] clients[PAX]
.convo(convo_id) .convo(convo_id)
.send_content( .send_content(b"ngl the key rotation is cooked")
&mut clients[PAX].client_ctx(),
b"ngl the key rotation is cooked",
)
.unwrap(); .unwrap();
// clients[SARO].process_messages();
process(&mut clients); process(&mut clients);
clients[SARO] clients[SARO]
.convo(convo_id) .convo(convo_id)
.send_content( .send_content(b"bro we literally just added you to the group ")
&mut clients[SARO].client_ctx(),
b"bro we literally just added you to the group ",
)
.unwrap(); .unwrap();
process(&mut clients); process(&mut clients);
// process(&mut clients);
} }

View File

@ -1,11 +1,9 @@
use std::ops::{Deref, DerefMut}; use libchat::{Context, Introduction};
use libchat::{AddressedEnvelope, Context, Introduction};
use sqlite::{ChatStorage, StorageConfig}; use sqlite::{ChatStorage, StorageConfig};
use storage::{ConversationStore, IdentityStore}; use storage::{ConversationStore, IdentityStore};
use tempfile::tempdir; use tempfile::tempdir;
use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; use components::{EphemeralRegistry, LocalBroadcaster};
fn send_and_verify( fn send_and_verify(
sender: &mut Context<LocalBroadcaster, EphemeralRegistry, ChatStorage>, sender: &mut Context<LocalBroadcaster, EphemeralRegistry, ChatStorage>,