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!(