Make conversations generic over IdentityProvider

This commit is contained in:
Jazz Turner-Baggs 2026-05-02 09:39:48 -07:00
parent e8ffc9d48e
commit 6ba8bbc9ce
No known key found for this signature in database
19 changed files with 418 additions and 264 deletions

11
Cargo.lock generated
View File

@ -378,6 +378,7 @@ dependencies = [
"chat-sqlite 0.1.0", "chat-sqlite 0.1.0",
"components", "components",
"libchat 0.1.0", "libchat 0.1.0",
"logos-account",
"tempfile", "tempfile",
"thiserror", "thiserror",
] ]
@ -1386,6 +1387,7 @@ dependencies = [
"chat-sqlite 0.1.0", "chat-sqlite 0.1.0",
"components", "components",
"libchat 0.1.0", "libchat 0.1.0",
"logos-account",
"storage 0.1.0", "storage 0.1.0",
"tempfile", "tempfile",
] ]
@ -1481,6 +1483,7 @@ dependencies = [
"hex", "hex",
"openmls", "openmls",
"openmls_libcrux_crypto 0.3.1", "openmls_libcrux_crypto 0.3.1",
"openmls_memory_storage 0.5.0",
"openmls_traits 0.5.0", "openmls_traits 0.5.0",
"prost", "prost",
"rand_core 0.6.4", "rand_core 0.6.4",
@ -1821,6 +1824,14 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "logos-account"
version = "0.1.0"
dependencies = [
"crypto 0.1.0",
"libchat 0.1.0",
]
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.12.5" version = "0.12.5"

View File

@ -4,6 +4,7 @@ resolver = "3"
members = [ members = [
"bin/chat-cli", "bin/chat-cli",
"core/account",
"core/conversations", "core/conversations",
"core/crypto", "core/crypto",
"core/double-ratchets", "core/double-ratchets",
@ -16,6 +17,7 @@ members = [
] ]
default-members = [ default-members = [
"core/account",
"core/sqlite", "core/sqlite",
"core/conversations", "core/conversations",
"core/crypto", "core/crypto",
@ -32,6 +34,7 @@ chat-sqlite = { path = "core/sqlite" }
components = { path = "extensions/components" } components = { path = "extensions/components" }
crypto = { path = "core/crypto" } crypto = { path = "core/crypto" }
libchat = { path = "core/conversations" } libchat = { path = "core/conversations" }
logos-account = { path = "core/account" }
storage = { path = "core/storage" } storage = { path = "core/storage" }
# External Workspace dependency declarations (sorted) # External Workspace dependency declarations (sorted)

14
core/account/Cargo.toml Normal file
View File

@ -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)

View File

@ -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<String>) -> 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)
}
}

5
core/account/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
#[cfg(feature = "dev")]
mod account;
#[cfg(feature = "dev")]
pub use account::TestLogosAccount;

View File

@ -20,6 +20,7 @@ double-ratchets = { path = "../double-ratchets" }
hex = "0.4.3" hex = "0.4.3"
openmls = { version = "0.8.1", features = ["libcrux-provider"] } openmls = { version = "0.8.1", features = ["libcrux-provider"] }
openmls_libcrux_crypto = "0.3.1" openmls_libcrux_crypto = "0.3.1"
openmls_memory_storage = "0.5.0"
openmls_traits = "0.5.0" openmls_traits = "0.5.0"
prost = "0.14.1" prost = "0.14.1"
rand_core = { version = "0.6" } rand_core = { version = "0.6" }

View File

@ -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<String>) -> 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<Vec<u8>, 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
}
}

View File

@ -2,10 +2,9 @@ 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::conversation::{Convo, GroupConvo}; use crate::conversation::{Convo, GroupConvo};
use crate::{DeliveryService, RegistrationService}; use crate::{DeliveryService, IdentityProvider, RegistrationService};
use crate::{ use crate::{
conversation::{Conversation, Id, PrivateV1Convo}, conversation::{Conversation, Id, PrivateV1Convo},
errors::ChatError, errors::ChatError,
@ -22,16 +21,22 @@ pub use crate::inbox::Introduction;
// This is the main entry point to the conversations api. // This is the main entry point to the conversations api.
// 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<
IP: IdentityProvider,
DS: DeliveryService,
RS: RegistrationService,
CS: ChatStore,
> {
identity: Rc<Identity>, identity: Rc<Identity>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
store: Rc<RefCell<CS>>, store: Rc<RefCell<CS>>,
inbox: Inbox<CS>, inbox: Inbox<CS>,
pq_inbox: InboxV2<DS, RS, CS>, pq_inbox: InboxV2<IP, DS, RS, CS>,
} }
impl<DS, RS, CS> Context<DS, RS, CS> impl<IP, DS, RS, CS> Context<IP, DS, RS, CS>
where where
IP: IdentityProvider + 'static,
DS: DeliveryService + 'static, DS: DeliveryService + 'static,
RS: RegistrationService + 'static, RS: RegistrationService + 'static,
CS: ChatStore + 'static, CS: ChatStore + 'static,
@ -42,6 +47,7 @@ where
/// Otherwise, a new identity will be created with the given name and saved. /// Otherwise, a new identity will be created with the given name and saved.
pub fn new_from_store( pub fn new_from_store(
name: impl Into<String>, name: impl Into<String>,
account: IP,
delivery: DS, delivery: DS,
registration: RS, registration: RS,
store: CS, store: CS,
@ -65,12 +71,7 @@ where
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( let pq_inbox = InboxV2::new(account, ds.clone(), contact_registry.clone(), store.clone());
LogosAccount::new_test(name),
ds.clone(),
contact_registry.clone(),
store.clone(),
);
// Subscribe // Subscribe
ds.borrow_mut() ds.borrow_mut()
@ -91,6 +92,7 @@ where
/// Uses in-memory SQLite database. Each call creates a new isolated database. /// Uses in-memory SQLite database. Each call creates a new isolated database.
pub fn new_with_name( pub fn new_with_name(
name: impl Into<String>, name: impl Into<String>,
account: IP,
delivery: DS, delivery: DS,
registration: RS, registration: RS,
chat_store: CS, chat_store: CS,
@ -110,12 +112,8 @@ where
let identity = Rc::new(identity); let identity = Rc::new(identity);
let inbox = Inbox::new(store.clone(), Rc::clone(&identity)); let inbox = Inbox::new(store.clone(), Rc::clone(&identity));
let mut pq_inbox = InboxV2::new( let mut pq_inbox =
LogosAccount::new_test(name), InboxV2::new(account, ds.clone(), contact_registry.clone(), store.clone());
ds.clone(),
contact_registry.clone(),
store.clone(),
);
// TODO: (P2) Initialize Account in Context or upper client. // TODO: (P2) Initialize Account in Context or upper client.
pq_inbox.register()?; pq_inbox.register()?;

View File

@ -12,7 +12,7 @@ use std::sync::Arc;
use storage::{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;
pub use privatev1::PrivateV1Convo; pub use privatev1::PrivateV1Convo;
pub type ConversationId<'a> = &'a str; pub type ConversationId<'a> = &'a str;

View File

@ -7,114 +7,72 @@ use std::rc::Rc;
use blake2::{Blake2b, Digest, digest::consts::U6}; use blake2::{Blake2b, Digest, digest::consts::U6};
use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload}; use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload};
use crypto::Ed25519VerifyingKey;
use openmls::prelude::tls_codec::Deserialize; use openmls::prelude::tls_codec::Deserialize;
use openmls::prelude::*; use openmls::prelude::*;
use openmls_libcrux_crypto::Provider as LibcruxProvider;
use openmls_traits::signatures::Signer as OpenMlsSigner;
use storage::ConversationKind; use storage::ConversationKind;
use crate::inbox_v2::{MlsIdentityProvider, MlsProvider};
use crate::types::AccountId; use crate::types::AccountId;
use crate::{ use crate::{
DeliveryService, DeliveryService,
conversation::{ChatError, ConversationId, Convo, GroupConvo, Id}, conversation::{ChatError, ConversationId, Convo, GroupConvo, Id},
service_traits::KeyPackageProvider, service_traits::{IdentityProvider, KeyPackageProvider},
types::{AddressedEncryptedPayload, ContentData}, types::{AddressedEncryptedPayload, ContentData},
}; };
/// Provides the identity information needed to participate in an MLS group. pub struct GroupV1Convo<IP: IdentityProvider, MP, DS, KP> {
/// identity_provider: Rc<RefCell<MlsIdentityProvider<IP>>>,
/// Implementors must also implement [`OpenMlsSigner`] so they can sign MLS mls_provider: Rc<RefCell<MP>>,
/// 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<DS: DeliveryService>(
&self,
ds: &mut DS,
account_id: &AccountId,
welcome: &MlsMessageOut,
) -> Result<(), ChatError>;
}
pub struct GroupV1Convo<MlsCtx, DS, KP> {
ctx: Rc<RefCell<MlsCtx>>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>, keypkg_provider: Rc<RefCell<KP>>,
mls_group: MlsGroup, mls_group: MlsGroup,
convo_id: String, convo_id: String,
} }
impl<MlsCtx, DS, KP> std::fmt::Debug for GroupV1Convo<MlsCtx, DS, KP> impl<IP, MP, DS, KP> std::fmt::Debug for GroupV1Convo<IP, MP, DS, KP>
where where
MlsCtx: MlsContext, IP: IdentityProvider,
MP: MlsProvider,
DS: DeliveryService, DS: DeliveryService,
KP: KeyPackageProvider, 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.identity_provider.borrow().friendly_name())
.field("convo_id", &self.convo_id) .field("convo_id", &self.convo_id)
.field("mls_epoch", &self.mls_group.epoch()) .field("mls_epoch", &self.mls_group.epoch())
.finish_non_exhaustive() .finish_non_exhaustive()
} }
} }
impl<MlsCtx, DS, KP> GroupV1Convo<MlsCtx, DS, KP> impl<IP, MP, DS, KP> GroupV1Convo<IP, MP, DS, KP>
where where
MlsCtx: MlsContext, IP: IdentityProvider,
MP: MlsProvider,
DS: DeliveryService, DS: DeliveryService,
KP: KeyPackageProvider, KP: KeyPackageProvider,
{ {
// Create a new conversation with the creator as the only participant. // Create a new conversation with the creator as the only participant.
pub fn new( pub fn new(
ctx: Rc<RefCell<MlsCtx>>, identity_provider: Rc<RefCell<MlsIdentityProvider<IP>>>,
mls_provider: Rc<RefCell<MP>>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>, keypkg_provider: Rc<RefCell<KP>>,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let config = Self::mls_create_config(); let config = Self::mls_create_config();
let mls_group = { let mls_group = {
let ctx_ref = ctx.borrow(); let mls_provider_ref = mls_provider.borrow();
MlsGroup::new( let signer = identity_provider.borrow();
ctx_ref.provider(), let credential = signer.get_credential();
ctx_ref.ident(),
&config, MlsGroup::new(&*mls_provider_ref, &*signer, &config, credential).unwrap()
ctx_ref.get_credential(),
)
.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(&mut ds.borrow_mut(), &convo_id)?; Self::subscribe(&mut ds.borrow_mut(), &convo_id)?;
Ok(Self { Ok(Self {
ctx, identity_provider,
mls_provider,
ds, ds,
keypkg_provider, keypkg_provider,
mls_group, mls_group,
@ -124,15 +82,14 @@ where
// Constructs a new conversation upon receiving a MlsWelcome message. // Constructs a new conversation upon receiving a MlsWelcome message.
pub fn new_from_welcome( pub fn new_from_welcome(
ctx: Rc<RefCell<MlsCtx>>, identity_provider: Rc<RefCell<MlsIdentityProvider<IP>>>,
mls_provider: Rc<RefCell<MP>>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>, keypkg_provider: Rc<RefCell<KP>>,
welcome: Welcome, welcome: Welcome,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
let mls_group = { let mls_group = {
let ctx_borrow = ctx.borrow(); let provider = &*mls_provider.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() .unwrap()
.build() .build()
@ -145,7 +102,8 @@ where
Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?; Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?;
Ok(Self { Ok(Self {
ctx, identity_provider,
mls_provider,
ds, ds,
keypkg_provider, keypkg_provider,
mls_group, mls_group,
@ -154,20 +112,22 @@ where
} }
pub fn load( pub fn load(
ctx: Rc<RefCell<MlsCtx>>, identity_provider: Rc<RefCell<MlsIdentityProvider<IP>>>,
mls_provider: Rc<RefCell<MP>>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
keypkg_provider: Rc<RefCell<KP>>, keypkg_provider: Rc<RefCell<KP>>,
convo_id: String, convo_id: String,
group_id: GroupId, group_id: GroupId,
) -> Result<Self, ChatError> { ) -> Result<Self, ChatError> {
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)? .map_err(ChatError::generic)?
.ok_or_else(|| 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)?; Self::subscribe(&mut *ds.borrow_mut(), &convo_id)?;
Ok(GroupV1Convo { Ok(GroupV1Convo {
ctx, identity_provider,
mls_provider,
ds, ds,
keypkg_provider, keypkg_provider,
mls_group, mls_group,
@ -233,17 +193,16 @@ where
}; };
let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?; let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?;
let keypkg = key_package_in.validate( let keypkg =
self.ctx.borrow().provider().crypto(), key_package_in.validate(self.mls_provider.borrow().crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version
ProtocolVersion::Mls10,
)?; //TODO: P3 - Hardcoded Protocol Version
Ok(keypkg) Ok(keypkg)
} }
} }
impl<MlsCtx, DS, KP> Id for GroupV1Convo<MlsCtx, DS, KP> impl<IP, MP, DS, KP> Id for GroupV1Convo<IP, MP, DS, KP>
where where
MlsCtx: MlsContext, IP: IdentityProvider,
MP: MlsProvider,
DS: DeliveryService, DS: DeliveryService,
KP: KeyPackageProvider, KP: KeyPackageProvider,
{ {
@ -252,9 +211,10 @@ where
} }
} }
impl<MlsCtx, DS, KP> Convo for GroupV1Convo<MlsCtx, DS, KP> impl<IP, MP, DS, KP> Convo for GroupV1Convo<IP, MP, DS, KP>
where where
MlsCtx: MlsContext, IP: IdentityProvider,
MP: MlsProvider,
DS: DeliveryService, DS: DeliveryService,
KP: KeyPackageProvider, KP: KeyPackageProvider,
{ {
@ -262,11 +222,13 @@ where
&mut self, &mut self,
content: &[u8], content: &[u8],
) -> Result<Vec<AddressedEncryptedPayload>, ChatError> { ) -> Result<Vec<AddressedEncryptedPayload>, ChatError> {
let ctx_ref = self.ctx.borrow();
let provider = ctx_ref.provider();
let mls_message_out = self let mls_message_out = self
.mls_group .mls_group
.create_message(provider, ctx_ref.ident(), content) .create_message(
&*self.mls_provider.borrow(),
&*self.identity_provider.borrow(),
content,
)
.unwrap(); .unwrap();
let a = AddressedEncryptedPayload { let a = AddressedEncryptedPayload {
@ -302,8 +264,7 @@ where
.try_into_protocol_message() .try_into_protocol_message()
.map_err(ChatError::generic)?; .map_err(ChatError::generic)?;
let ctx_borrow = self.ctx.borrow(); let provider = &*self.mls_provider.borrow();
let provider = ctx_borrow.provider();
if protocol_message.epoch() < self.mls_group.epoch() { if protocol_message.epoch() < self.mls_group.epoch() {
// TODO: (P1) Add logging for messages arriving from past epoch. // TODO: (P1) Add logging for messages arriving from past epoch.
@ -344,9 +305,10 @@ where
} }
} }
impl<MlsCtx, DS, KP> GroupConvo<DS, KP> for GroupV1Convo<MlsCtx, DS, KP> impl<IP, MP, DS, KP> GroupConvo<DS, KP> for GroupV1Convo<IP, MP, DS, KP>
where where
MlsCtx: MlsContext, IP: IdentityProvider,
MP: MlsProvider,
DS: DeliveryService, DS: DeliveryService,
KP: KeyPackageProvider, KP: KeyPackageProvider,
{ {
@ -355,8 +317,8 @@ where
// welcome — the Welcome message sent privately to each new joiner // welcome — the Welcome message sent privately to each new joiner
// _group_info — used for external joins; ignore for now // _group_info — used for external joins; ignore for now
fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> { fn add_member(&mut self, members: &[&AccountId]) -> Result<(), ChatError> {
let ctx_ref = self.ctx.borrow(); let identity_provider = &*self.identity_provider.borrow();
let provider = ctx_ref.provider(); let mls_provider = &*self.mls_provider.borrow();
if members.len() > 50 { if members.len() > 50 {
// This is a temporary limit that originates from the the De-MLS epoch time. // 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 let (commit, welcome, _group_info) = self
.mls_group .mls_group
.add_members(provider, ctx_ref.ident(), keypkgs.iter().as_slice()) .add_members(mls_provider, identity_provider, keypkgs.iter().as_slice())
.unwrap(); .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 // 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(&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 { let encrypted_payload = EncryptedPayload {

View File

@ -1,10 +1,14 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use chat_proto::logoschat::envelope::EnvelopeV1; use chat_proto::logoschat::envelope::EnvelopeV1;
use openmls::prelude::tls_codec::Serialize; use openmls::prelude::tls_codec::Serialize;
use openmls::prelude::*; 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 prost::{Message, Oneof};
use storage::ChatStore; use storage::ChatStore;
use storage::ConversationMeta; use storage::ConversationMeta;
@ -12,29 +16,95 @@ use storage::ConversationMeta;
use crate::AddressedEnvelope; use crate::AddressedEnvelope;
use crate::ChatError; use crate::ChatError;
use crate::DeliveryService; use crate::DeliveryService;
use crate::IdentityProvider;
use crate::RegistrationService; use crate::RegistrationService;
use crate::account::LogosAccount; use crate::conversation::{GroupConvo, GroupV1Convo};
use crate::conversation::GroupConvo;
use crate::conversation::group_v1::MlsContext;
use crate::conversation::{GroupV1Convo, IdentityProvider};
use crate::types::AccountId; use crate::types::AccountId;
use crate::utils::{blake2b_hex, hash_size}; use crate::utils::{blake2b_hex, hash_size};
pub struct PqMlsContext {
ident_provider: LogosAccount, // Define unique Identifiers derivations used in InboxV2
provider: LibcruxProvider, fn delivery_address_for(account_id: &AccountId) -> String {
blake2b_hex::<hash_size::AccountId>(&["InboxV2|", "delivery_address|", account_id.as_str()])
} }
impl MlsContext for PqMlsContext { fn conversation_id_for(account_id: &AccountId) -> String {
type IDENT = LogosAccount; blake2b_hex::<hash_size::ConvoId>(&["InboxV2|", "conversation_id|", account_id.as_str()])
}
fn ident(&self) -> &LogosAccount { pub struct MlsIdentityProvider<T: IdentityProvider>(T);
&self.ident_provider
impl<T: IdentityProvider> MlsIdentityProvider<T> {
pub fn get_credential(&self) -> CredentialWithKey {
CredentialWithKey {
credential: BasicCredential::new(self.friendly_name().into()).into(),
signature_key: self.public_key().as_ref().into(),
}
}
}
impl<T: IdentityProvider> Deref for MlsIdentityProvider<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: IdentityProvider> IdentityProvider for MlsIdentityProvider<T> {
fn account_id(&self) -> &AccountId {
self.0.account_id()
} }
fn provider(&self) -> &LibcruxProvider { fn friendly_name(&self) -> String {
&self.provider 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<T: IdentityProvider> Signer for MlsIdentityProvider<T> {
fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, 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<DS: DeliveryService>(
&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<Self, CryptoError> {
let crypto = LibcruxCryptoProvider::new()?;
let storage = MemoryStorage::default();
Ok(Self { crypto, storage })
}
}
impl MlsProvider for MlsEphemeralPqProvider {
fn invite_user<DS: DeliveryService>( fn invite_user<DS: DeliveryService>(
&self, &self,
ds: &mut DS, ds: &mut DS,
@ -65,49 +135,62 @@ impl MlsContext for PqMlsContext {
} }
} }
// Define unique Identifiers derivations used in InboxV2 impl OpenMlsProvider for MlsEphemeralPqProvider {
fn delivery_address_for(account_id: &AccountId) -> String { type CryptoProvider = LibcruxCryptoProvider;
blake2b_hex::<hash_size::AccountId>(&["InboxV2|", "delivery_address|", account_id.as_str()]) type RandProvider = LibcruxCryptoProvider;
} type StorageProvider = openmls_memory_storage::MemoryStorage;
fn conversation_id_for(account_id: &AccountId) -> String { fn storage(&self) -> &Self::StorageProvider {
blake2b_hex::<hash_size::ConvoId>(&["InboxV2|", "conversation_id|", account_id.as_str()]) &self.storage
}
fn crypto(&self) -> &Self::CryptoProvider {
&self.crypto
}
fn rand(&self) -> &Self::RandProvider {
&self.crypto
}
} }
/// An PQ focused Conversation initializer. /// An PQ focused Conversation initializer.
/// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols /// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols
/// such as MLS. /// such as MLS.
pub struct InboxV2<DS, RS, CS> { pub struct InboxV2<IP, DS, RS, CS>
where
IP: IdentityProvider,
{
account_id: AccountId, account_id: AccountId,
account: Rc<RefCell<MlsIdentityProvider<IP>>>,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
reg_service: Rc<RefCell<RS>>, reg_service: Rc<RefCell<RS>>,
store: Rc<RefCell<CS>>, store: Rc<RefCell<CS>>,
ctx: Rc<RefCell<PqMlsContext>>, mls_provider: Rc<RefCell<MlsEphemeralPqProvider>>,
} }
impl<DS, CS, RS> InboxV2<DS, RS, CS> impl<IP, DS, CS, RS> InboxV2<IP, DS, RS, CS>
where where
IP: IdentityProvider,
DS: DeliveryService, DS: DeliveryService,
RS: RegistrationService, RS: RegistrationService,
CS: ChatStore, CS: ChatStore,
{ {
pub fn new( pub fn new(
account: LogosAccount, account: IP,
ds: Rc<RefCell<DS>>, ds: Rc<RefCell<DS>>,
reg_service: Rc<RefCell<RS>>, reg_service: Rc<RefCell<RS>>,
store: Rc<RefCell<CS>>, store: Rc<RefCell<CS>>,
) -> Self { ) -> Self {
// Avoid referencing a temporary value by caching it.
let account_id = account.account_id().clone(); let account_id = account.account_id().clone();
let provider = LibcruxProvider::new().unwrap(); let provider = MlsEphemeralPqProvider::new().unwrap();
Self { Self {
account_id, account_id,
account: Rc::new(RefCell::new(MlsIdentityProvider(account))),
ds, ds,
reg_service, reg_service,
store, store,
ctx: Rc::new(RefCell::new(PqMlsContext { mls_provider: Rc::new(RefCell::new(provider)),
ident_provider: account,
provider,
})),
} }
} }
@ -123,10 +206,7 @@ where
// "LastResort" package or publish multiple // "LastResort" package or publish multiple
self.reg_service self.reg_service
.borrow_mut() .borrow_mut()
.register( .register(&self.account.borrow().friendly_name(), keypackage_bytes)
&self.ctx.borrow().ident_provider.friendly_name(),
keypackage_bytes,
)
.map_err(ChatError::generic) .map_err(ChatError::generic)
} }
@ -138,8 +218,15 @@ where
conversation_id_for(&self.account_id) conversation_id_for(&self.account_id)
} }
pub fn create_group_v1(&self) -> Result<GroupV1Convo<PqMlsContext, DS, RS>, ChatError> { pub fn create_group_v1(
GroupV1Convo::new(self.ctx.clone(), self.ds.clone(), self.reg_service.clone()) &self,
) -> Result<GroupV1Convo<IP, MlsEphemeralPqProvider, DS, RS>, 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> { pub fn handle_frame(&self, payload_bytes: &[u8]) -> Result<(), ChatError> {
@ -180,7 +267,8 @@ where
}; };
let convo = GroupV1Convo::new_from_welcome( let convo = GroupV1Convo::new_from_welcome(
self.ctx.clone(), self.account.clone(),
self.mls_provider.clone(),
self.ds.clone(), self.ds.clone(),
self.reg_service.clone(), self.reg_service.clone(),
welcome, welcome,
@ -189,20 +277,21 @@ where
} }
fn create_keypackage(&self) -> Result<KeyPackage, ChatError> { fn create_keypackage(&self) -> Result<KeyPackage, ChatError> {
let ctx_borrow = self.ctx.borrow();
let capabilities = Capabilities::builder() let capabilities = Capabilities::builder()
.ciphersuites(vec![ .ciphersuites(vec![
Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519,
]) ])
.extensions(vec![ExtensionType::ApplicationId]) .extensions(vec![ExtensionType::ApplicationId])
.build(); .build();
let signer = self.account.borrow();
let a = KeyPackage::builder() let a = KeyPackage::builder()
.leaf_node_capabilities(capabilities) .leaf_node_capabilities(capabilities)
.build( .build(
Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519, Ciphersuite::MLS_256_XWING_CHACHA20POLY1305_SHA256_Ed25519,
ctx_borrow.provider(), &*self.mls_provider.borrow(),
ctx_borrow.ident(), &*signer,
ctx_borrow.get_credential(), signer.get_credential(),
) )
.expect("Failed to build KeyPackage"); .expect("Failed to build KeyPackage");
@ -212,11 +301,12 @@ where
pub fn load_mls_convo( pub fn load_mls_convo(
&self, &self,
convo_id: String, convo_id: String,
) -> Result<GroupV1Convo<PqMlsContext, DS, RS>, ChatError> { ) -> Result<GroupV1Convo<IP, MlsEphemeralPqProvider, 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( let convo = GroupV1Convo::load(
self.ctx.clone(), self.account.clone(),
self.mls_provider.clone(),
self.ds.clone(), self.ds.clone(),
self.reg_service.clone(), self.reg_service.clone(),
convo_id, convo_id,

View File

@ -1,4 +1,3 @@
mod account;
mod context; mod context;
mod conversation; mod conversation;
mod crypto; mod crypto;
@ -10,12 +9,11 @@ mod service_traits;
mod types; mod types;
mod utils; mod utils;
pub use account::LogosAccount;
pub use chat_sqlite::ChatStorage; pub use chat_sqlite::ChatStorage;
pub use chat_sqlite::StorageConfig; pub use chat_sqlite::StorageConfig;
pub use context::{Context, ConversationId, ConversationIdOwned, Introduction}; pub use context::{Context, ConversationId, ConversationIdOwned, Introduction};
pub use conversation::GroupConvo; pub use conversation::GroupConvo;
pub use errors::ChatError; 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 types::{AccountId, AddressedEnvelope, ContentData};
pub use utils::hex_trunc; pub use utils::hex_trunc;

View File

@ -3,6 +3,8 @@
/// different implementations. /// different implementations.
use std::{fmt::Debug, fmt::Display}; use std::{fmt::Debug, fmt::Display};
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
use crate::types::{AccountId, AddressedEnvelope}; use crate::types::{AccountId, AddressedEnvelope};
/// A Delivery service is responsible for payload transport. /// A Delivery service is responsible for payload transport.
@ -39,3 +41,12 @@ impl<T: RegistrationService> KeyPackageProvider for T {
RegistrationService::retrieve(self, identity) 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;
}

View File

@ -8,9 +8,10 @@ edition = "2024"
[dev-dependencies] [dev-dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
chat-sqlite = { workspace = true }
components = { workspace = true } components = { workspace = true }
libchat = { workspace = true } libchat = { workspace = true }
chat-sqlite = { workspace = true } logos-account = { workspace = true, features = ["dev"]}
storage = { workspace = true } storage = { workspace = true }
# External dependencies (sorted) # External dependencies (sorted)

View File

@ -2,16 +2,16 @@ use std::ops::{Deref, DerefMut};
use components::{EphemeralRegistry, LocalBroadcaster, MemStore}; use components::{EphemeralRegistry, LocalBroadcaster, MemStore};
use libchat::{ContentData, Context, GroupConvo, hex_trunc}; use libchat::{ContentData, Context, GroupConvo, hex_trunc};
use logos_account::TestLogosAccount;
// Simple client Functionality for testing // Simple client Functionality for testing
struct Client { struct Client {
inner: Context<LocalBroadcaster, EphemeralRegistry, MemStore>, inner: Context<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, MemStore>,
on_content: Option<Box<dyn Fn(ContentData)>>, on_content: Option<Box<dyn Fn(ContentData)>>,
} }
impl Client { impl Client {
fn init( fn init(
ctx: Context<LocalBroadcaster, EphemeralRegistry, MemStore>, ctx: Context<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, MemStore>,
cb: Option<impl Fn(ContentData) + 'static>, cb: Option<impl Fn(ContentData) + 'static>,
) -> Self { ) -> Self {
Client { Client {
@ -46,7 +46,7 @@ impl Client {
} }
impl Deref for Client { impl Deref for Client {
type Target = Context<LocalBroadcaster, EphemeralRegistry, MemStore>; type Target = Context<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, MemStore>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.inner &self.inner
@ -80,9 +80,25 @@ fn create_group() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::new(); let rs = EphemeralRegistry::new();
let saro_ctx = let saro_account = TestLogosAccount::new("saro");
Context::new_with_name("saro", ds.new_consumer(), rs.clone(), MemStore::new()).unwrap(); let saro_ctx = Context::new_with_name(
let raya_ctx = Context::new_with_name("raya", ds.clone(), rs.clone(), MemStore::new()).unwrap(); "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![ let mut clients = vec![
Client::init(saro_ctx, Some(pretty_print(" Saro "))), Client::init(saro_ctx, Some(pretty_print(" Saro "))),
@ -115,7 +131,8 @@ fn create_group() {
process(&mut clients); 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")))); clients.push(Client::init(pax_ctx, Some(pretty_print(" Pax"))));
const PAX: usize = 2; const PAX: usize = 2;

View File

@ -1,13 +1,13 @@
use chat_sqlite::{ChatStorage, StorageConfig}; use chat_sqlite::{ChatStorage, StorageConfig};
use components::{EphemeralRegistry, LocalBroadcaster};
use libchat::{Context, Introduction}; use libchat::{Context, Introduction};
use logos_account::TestLogosAccount;
use storage::{ConversationStore, IdentityStore}; use storage::{ConversationStore, IdentityStore};
use tempfile::tempdir; use tempfile::tempdir;
use components::{EphemeralRegistry, LocalBroadcaster};
fn send_and_verify( fn send_and_verify(
sender: &mut Context<LocalBroadcaster, EphemeralRegistry, ChatStorage>, sender: &mut Context<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, ChatStorage>,
receiver: &mut Context<LocalBroadcaster, EphemeralRegistry, ChatStorage>, receiver: &mut Context<TestLogosAccount, LocalBroadcaster, EphemeralRegistry, ChatStorage>,
convo_id: &str, convo_id: &str,
content: &[u8], content: &[u8],
) { ) {
@ -26,9 +26,18 @@ fn ctx_integration() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::new(); let rs = EphemeralRegistry::new();
let mut saro = let saro_account = TestLogosAccount::new("saro");
Context::new_with_name("saro", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); let raya_account = TestLogosAccount::new("raya");
let mut raya = Context::new_with_name("raya", ds, rs, ChatStorage::in_memory()).unwrap(); 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 // Raya creates intro bundle and sends to Saro
let bundle = raya.create_intro_bundle().unwrap(); let bundle = raya.create_intro_bundle().unwrap();
@ -64,13 +73,14 @@ fn identity_persistence() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::new(); let rs = EphemeralRegistry::new();
let store1 = ChatStorage::new(StorageConfig::InMemory).unwrap(); 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 pubkey1 = ctx1.identity().public_key();
let name1 = ctx1.installation_name().to_string(); let name1 = ctx1.installation_name().to_string();
// For persistence tests with file-based storage, we'd need a shared db. // For persistence tests with file-based storage, we'd need a shared db.
// With in-memory, we just verify the identity was created. // 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)); assert!(!pubkey1.as_bytes().iter().all(|&b| b == 0));
} }
@ -83,14 +93,15 @@ fn open_persists_new_identity() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::new(); let rs = EphemeralRegistry::new();
let store = ChatStorage::new(StorageConfig::File(db_path.clone())).unwrap(); 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(); let pubkey = ctx.identity().public_key();
drop(ctx); drop(ctx);
let store = ChatStorage::new(StorageConfig::File(db_path)).unwrap(); let store = ChatStorage::new(StorageConfig::File(db_path)).unwrap();
let persisted = store.load_identity().unwrap().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); assert_eq!(persisted.public_key(), pubkey);
} }
@ -98,19 +109,28 @@ fn open_persists_new_identity() {
fn conversation_metadata_persistence() { fn conversation_metadata_persistence() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::new(); let rs = EphemeralRegistry::new();
let mut alice = let account_saro = TestLogosAccount::new("saro");
Context::new_with_name("alice", ds.clone(), rs.clone(), ChatStorage::in_memory()).unwrap(); let mut saro = Context::new_with_name(
let mut bob = Context::new_with_name("bob", ds, rs, ChatStorage::in_memory()).unwrap(); "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 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 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); 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.len(), 1);
assert_eq!(convos[0].kind.as_str(), "private_v1"); assert_eq!(convos[0].kind.as_str(), "private_v1");
} }
@ -119,45 +139,56 @@ fn conversation_metadata_persistence() {
fn conversation_full_flow() { fn conversation_full_flow() {
let ds = LocalBroadcaster::new(); let ds = LocalBroadcaster::new();
let rs = EphemeralRegistry::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 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 payload = payloads.first().unwrap();
let content = alice.handle_payload(&payload.data).unwrap().unwrap(); let content = saro.handle_payload(&payload.data).unwrap().unwrap();
let alice_convo_id = content.conversation_id; 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(); 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(); let payload = payloads.first().unwrap();
alice.handle_payload(&payload.data).unwrap().unwrap(); saro.handle_payload(&payload.data).unwrap().unwrap();
// Verify conversation list // Verify conversation list
let convo_ids = alice.list_conversations().unwrap(); let convo_ids = saro.list_conversations().unwrap();
assert_eq!(convo_ids.len(), 1); assert_eq!(convo_ids.len(), 1);
// Continue exchanging messages // 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 payload = payloads.first().unwrap();
let content = alice let content = saro
.handle_payload(&payload.data) .handle_payload(&payload.data)
.expect("should decrypt") .expect("should decrypt")
.expect("should have content"); .expect("should have content");
assert_eq!(content.data, b"more messages"); assert_eq!(content.data, b"more messages");
// Alice can also send back // saro can also send back
let payloads = alice.send_content(&alice_convo_id, b"alice reply").unwrap(); let payloads = saro.send_content(&saro_convo_id, b"saro reply").unwrap();
let payload = payloads.first().unwrap(); let payload = payloads.first().unwrap();
let content = bob let content = raya
.handle_payload(&payload.data) .handle_payload(&payload.data)
.unwrap() .unwrap()
.expect("bob should receive"); .expect("raya should receive");
assert_eq!(content.data, b"alice reply"); assert_eq!(content.data, b"saro reply");
} }

View File

@ -9,8 +9,9 @@ crate-type = ["rlib"]
[dependencies] [dependencies]
# Workspace dependencies (sorted) # Workspace dependencies (sorted)
chat-sqlite = { workspace = true } chat-sqlite = { workspace = true }
components = { workspace = true} components = { workspace = true }
libchat = { workspace = true } libchat = { workspace = true }
logos-account = { workspace = true, features = ["dev"] }
# External dependencies (sorted) # External dependencies (sorted)
thiserror = "2" thiserror = "2"

View File

@ -1,23 +1,31 @@
use libchat::{ use libchat::{
AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned, AddressedEnvelope, ChatError, ChatStorage, ContentData, Context, ConversationIdOwned,
DeliveryService, Introduction, StorageConfig, DeliveryService, IdentityProvider, Introduction, StorageConfig,
}; };
use logos_account::TestLogosAccount;
use components::EphemeralRegistry; use components::EphemeralRegistry;
use crate::errors::ClientError; use crate::errors::ClientError;
pub struct ChatClient<D: DeliveryService + 'static> { pub struct ChatClient<D>
ctx: Context<D, EphemeralRegistry, ChatStorage>, where
D: DeliveryService + 'static,
{
ctx: Context<TestLogosAccount, D, EphemeralRegistry, ChatStorage>,
} }
impl<D: DeliveryService> ChatClient<D> { impl<D> ChatClient<D>
where
D: DeliveryService + 'static,
{
/// Create an in-memory, ephemeral client. Identity is lost on drop. /// Create an in-memory, ephemeral client. Identity is lost on drop.
pub fn new(name: impl Into<String>, delivery: D) -> Self { pub fn new(name: impl Into<String> + Clone, delivery: D) -> Self {
let account = TestLogosAccount::new(name.clone());
let registry = EphemeralRegistry::new(); let registry = EphemeralRegistry::new();
let store = ChatStorage::in_memory(); let store = ChatStorage::in_memory();
Self { 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<D: DeliveryService> ChatClient<D> {
/// If an identity already exists in storage it is loaded; otherwise a new /// If an identity already exists in storage it is loaded; otherwise a new
/// one is created and saved. /// one is created and saved.
pub fn open( pub fn open(
name: impl Into<String>, identity: TestLogosAccount,
config: StorageConfig, config: StorageConfig,
delivery: D, delivery: D,
) -> Result<Self, ClientError<D::Error>> { ) -> Result<Self, ClientError<D::Error>> {
let store = ChatStorage::new(config).map_err(ChatError::from)?; let store = ChatStorage::new(config).map_err(ChatError::from)?;
let registry = EphemeralRegistry::new(); 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 }) Ok(Self { ctx })
} }

View File

@ -1,6 +1,7 @@
use client::{ use client::{
ChatClient, ContentData, ConversationIdOwned, Cursor, InProcessDelivery, StorageConfig, ChatClient, ContentData, ConversationIdOwned, Cursor, InProcessDelivery, StorageConfig,
}; };
use logos_account::TestLogosAccount;
use std::sync::Arc; use std::sync::Arc;
fn receive(receiver: &mut ChatClient<InProcessDelivery>, cursor: &mut Cursor) -> ContentData { fn receive(receiver: &mut ChatClient<InProcessDelivery>, 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 db_path = dir.path().join("test.db").to_string_lossy().to_string();
let config = StorageConfig::File(db_path); 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(); let name1 = client1.installation_name().to_string();
drop(client1); 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(); let name2 = client2.installation_name().to_string();
assert_eq!( assert_eq!(