Update Context to accept External Identity Provider. (#127)

* rename .account_id() to .id()

* Create logos-traits crate

* Remove AccountId references

* external IdentityProvider for Context

* Fix compile errors from merge

* Update logos-traits to shared-traits

* format fixes

* warnings cleanup

* clippy fix

* Remove rebase artifact
This commit is contained in:
Jazz Turner-Baggs
2026-06-10 06:59:04 -07:00
committed by GitHub
parent 0e72fdf483
commit a610117e81
27 changed files with 297 additions and 222 deletions
-63
View File
@@ -1,63 +0,0 @@
use crypto::{Ed25519Signature, Ed25519SigningKey, Ed25519VerifyingKey};
use openmls::prelude::SignatureScheme;
use openmls_traits::signatures::Signer;
use crate::{AccountId, IdentityProvider};
/// Logos Account represents a single account across
/// multiple installations and services.
///
/// Deprecated!
pub struct LogosAccount {
id: AccountId,
signing_key: Ed25519SigningKey,
verifying_key: Ed25519VerifyingKey,
}
impl LogosAccount {
/// Create a test LogosAccount. The `AccountId` is derived from the
/// generated Ed25519 verifying key (hex-encoded) so signatures over the
/// id can be verified by anyone holding the id alone.
/// The supplied `_display_name` is currently ignored — id is the key.
/// This should only be used during MLS integration. Not suitable for production use.
/// TODO: (P1) Remove once implementation is ready.
pub fn new_test(_display_name: impl Into<String>) -> Self {
let signing_key = Ed25519SigningKey::generate();
let verifying_key = signing_key.verifying_key();
let id = AccountId::new(hex::encode(verifying_key.as_ref()));
Self {
id,
signing_key,
verifying_key,
}
}
}
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 account_id(&self) -> &AccountId {
&self.id
}
fn display_name(&self) -> String {
self.id.to_string()
}
fn sign(&self, payload: &[u8]) -> Ed25519Signature {
self.signing_key.sign(payload)
}
fn public_key(&self) -> &Ed25519VerifyingKey {
&self.verifying_key
}
}
+3 -4
View File
@@ -1,14 +1,13 @@
pub mod group_v1;
mod privatev1;
pub use crate::errors::ChatError;
use crate::outcomes::ConvoOutcome;
use crate::proto::EncryptedPayload;
use crate::service_context::{ExternalServices, ServiceContext};
use crate::types::AccountId;
pub use crate::errors::ChatError;
pub use group_v1::GroupV1Convo;
pub use privatev1::PrivateV1Convo;
use shared_traits::IdentIdRef;
pub type ConversationId = String;
@@ -34,6 +33,6 @@ pub(crate) trait GroupConvo<S: ExternalServices>: Convo<S> {
fn add_member(
&mut self,
cx: &mut ServiceContext<S>,
members: &[&AccountId],
members: &[IdentIdRef],
) -> Result<(), ChatError>;
}
@@ -8,10 +8,11 @@ use chat_proto::logoschat::reliability::ReliablePayload;
use openmls::prelude::tls_codec::Deserialize;
use openmls::prelude::*;
use prost::Message as _;
use shared_traits::IdentIdRef;
use crate::inbox_v2::MlsProvider;
use crate::service_context::{ExternalServices, ServiceContext};
use crate::types::AccountId;
use crate::{
DeliveryService, IdentityProvider,
conversation::{ChatError, Convo, GroupConvo},
@@ -140,7 +141,7 @@ impl GroupV1Convo {
fn key_package_for_account(
&self,
ident: &AccountId,
ident: IdentIdRef,
provider: &impl MlsProvider,
keypkg_provider: &impl KeyPackageProvider,
) -> Result<KeyPackage, ChatError> {
@@ -171,7 +172,7 @@ impl GroupV1Convo {
content: &[u8],
cx: &ServiceContext<S>,
) -> Result<Vec<AddressedEncryptedPayload>, ChatError> {
let sender_id = cx.mls_identity.account_id().as_str();
let sender_id = cx.mls_identity.id().as_str();
let reliable = cx.causal.on_send(&self.convo_id, sender_id, content);
let wire = reliable.encode_to_vec();
@@ -274,7 +275,7 @@ impl<S: ExternalServices> GroupConvo<S> for GroupV1Convo {
fn add_member(
&mut self,
cx: &mut ServiceContext<S>,
members: &[&AccountId],
members: &[IdentIdRef],
) -> Result<(), ChatError> {
if members.len() > 50 {
// This is a temporary limit that originates from the the De-MLS epoch time.
+19 -28
View File
@@ -1,4 +1,3 @@
use crate::account::LogosAccount;
use crate::causal_history::{CausalHistoryStore, MissingMessage};
use crate::service_context::{ExternalServices, ServiceContext};
use crate::{DeliveryService, IdentityProvider, RegistrationService};
@@ -9,10 +8,10 @@ use crate::{
inbox_v2::{InboxV2, MlsEphemeralPqProvider, MlsIdentityProvider},
outcomes::{ConvoOutcome, InboxOutcome, PayloadOutcome},
proto::{EncryptedPayload, EnvelopeV1, Message},
types::AccountId,
};
use crypto::{Identity, PublicKey};
use openmls::prelude::GroupId;
use shared_traits::IdentIdRef;
use storage::{ChatStore, ConversationKind, ConversationStore};
pub use crate::conversation::ConversationId;
@@ -32,8 +31,9 @@ pub struct Core<S: ExternalServices> {
// Constructors live on the `(DS, RS, CS)` form: `S` can't be inferred backwards
// through `S::DS`, so the bundle is built from the three args here.
impl<DS, RS, CS> Core<(DS, RS, CS)>
impl<IP, DS, RS, CS> Core<(IP, DS, RS, CS)>
where
IP: IdentityProvider + 'static,
DS: DeliveryService + 'static,
RS: RegistrationService + 'static,
CS: ChatStore + 'static,
@@ -43,42 +43,34 @@ where
/// If an identity exists in storage, it will be restored.
/// Otherwise, a new identity will be created with the given name and saved.
pub fn new_from_store(
name: impl Into<String>,
ident: IP,
delivery: DS,
registration: RS,
mut store: CS,
) -> Result<Self, ChatError> {
let name = name.into();
// Load or create identity
let identity = if let Some(identity) = store.load_identity()? {
identity
} else {
let identity = Identity::new(&name);
let identity = Identity::new(ident.id().as_str().to_string());
store.save_identity(&identity)?;
identity
};
Self::assemble(name, identity, delivery, registration, store)
Self::assemble(ident, identity, delivery, registration, store)
}
/// Creates a new in-memory `Core` (for testing).
///
/// Uses in-memory SQLite database. Each call creates a new isolated database.
pub fn new_with_name(
name: impl Into<String>,
ident: IP,
delivery: DS,
registration: RS,
mut store: CS,
store: CS,
) -> Result<Self, ChatError> {
let name = name.into();
let identity = Identity::new(&name);
store
.save_identity(&identity)
.expect("in-memory storage should not fail");
let identity = Identity::new(ident.id().as_str().to_string());
let mut core = Self::assemble(ident, identity, delivery, registration, store)?;
let mut core = Self::assemble(name, identity, delivery, registration, store)?;
// TODO: (P2) Initialize Account in Core or upper client.
core.register_keypackage()?;
Ok(core)
}
@@ -86,19 +78,18 @@ where
/// Builds the inbox/account/MLS/causal state, subscribes both inbound
/// addresses, and assembles the service bundle — shared by both constructors.
fn assemble(
name: String,
ident: IP,
identity: Identity,
mut delivery: DS,
registration: RS,
store: CS,
) -> Result<Self, ChatError> {
let inbox = Inbox::new(&identity);
let account = LogosAccount::new_test(name);
let account_id = account.account_id().clone();
let mls_identity = MlsIdentityProvider::new(account);
let ident_id = ident.id().clone();
let mls_identity = MlsIdentityProvider::new(ident);
let mls_provider = MlsEphemeralPqProvider::new().map_err(ChatError::generic)?;
let causal = CausalHistoryStore::new();
let pq_inbox = InboxV2::new(account_id);
let pq_inbox = InboxV2::new(ident_id);
// Subscribe to inbound addresses for both conversation stacks.
delivery
@@ -124,7 +115,7 @@ where
}
}
impl<S: ExternalServices + 'static> Core<S> {
impl<'a, S: ExternalServices + 'static> Core<S> {
pub fn ds(&mut self) -> &mut S::DS {
&mut self.services.ds
}
@@ -138,8 +129,8 @@ impl<S: ExternalServices + 'static> Core<S> {
}
/// Returns the unique identifier associated with the account
pub fn account_id(&self) -> &AccountId {
self.pq_inbox.account_id()
pub fn ident_id(&'a self) -> IdentIdRef<'a> {
self.pq_inbox.ident_id()
}
/// Submit the local account's MLS KeyPackage to the registration service.
@@ -179,7 +170,7 @@ impl<S: ExternalServices + 'static> Core<S> {
pub fn create_group_convo(
&mut self,
participants: &[&AccountId],
participants: &[IdentIdRef],
) -> Result<ConversationId, ChatError> {
// TODO: (P1) Ensure errors are handled properly. This is a high chance for
// desynchronized state: MlsGroup persistence, conversation persistence, and
@@ -200,7 +191,7 @@ impl<S: ExternalServices + 'static> Core<S> {
pub fn group_add_member(
&mut self,
convo_id: &str,
members: &[&AccountId],
members: &[IdentIdRef],
) -> Result<(), ChatError> {
let mut convo = self.load_group_convo(convo_id)?;
convo.add_member(&mut self.services, members)
+44 -2
View File
@@ -270,18 +270,60 @@ impl Inbox {
#[cfg(test)]
mod tests {
use super::*;
use chat_sqlite::{ChatStorage, StorageConfig};
use crypto::{Ed25519SigningKey, Ed25519VerifyingKey};
use shared_traits::{IdentId, IdentityProvider};
struct Identity {
name: IdentId,
key: Ed25519SigningKey,
verify: Ed25519VerifyingKey,
}
impl Identity {
pub fn new(name: impl Into<String>) -> Self {
let key = Ed25519SigningKey::generate();
let verify = key.verifying_key();
Identity {
name: IdentId::new(name.into()),
key,
verify,
}
}
}
impl IdentityProvider for Identity {
fn id(&self) -> shared_traits::IdentIdRef<'_> {
&self.name
}
fn display_name(&self) -> String {
self.name.to_string()
}
fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature {
self.key.sign(payload)
}
fn public_key(&self) -> &crypto::Ed25519VerifyingKey {
&self.verify
}
}
#[test]
fn test_invite_privatev1_roundtrip() {
let saro_storage = ChatStorage::new(StorageConfig::InMemory).unwrap();
let raya_storage = ChatStorage::new(StorageConfig::InMemory).unwrap();
let mut saro_cx = ServiceContext::for_test("saro", saro_storage).unwrap();
let saro_account = Identity::new("saro");
let raya_account = Identity::new("raya");
let mut saro_cx = ServiceContext::for_test(saro_account, saro_storage).unwrap();
let saro_inbox = Inbox::new(&saro_cx.identity);
let mut raya_cx = ServiceContext::for_test("raya", raya_storage).unwrap();
let mut raya_cx = ServiceContext::for_test(raya_account, raya_storage).unwrap();
let raya_inbox = Inbox::new(&raya_cx.identity);
let bundle = raya_inbox.create_intro_bundle(&mut raya_cx).unwrap();
+14 -13
View File
@@ -3,6 +3,8 @@ mod mls_provider;
pub use identity::MlsIdentityProvider;
pub(crate) use mls_provider::MlsEphemeralPqProvider;
use shared_traits::IdentId;
use shared_traits::IdentIdRef;
use chat_proto::logoschat::envelope::EnvelopeV1;
use openmls::prelude::tls_codec::Serialize;
@@ -18,16 +20,15 @@ use crate::conversation::ConversationId;
use crate::conversation::GroupV1Convo;
use crate::outcomes::{ConversationClass, InboxOutcome, NewConversation};
use crate::service_context::{ExternalServices, ServiceContext};
use crate::types::AccountId;
use crate::utils::{blake2b_hex, hash_size};
// Define unique Identifiers derivations used in InboxV2
fn delivery_address_for(account_id: &AccountId) -> String {
blake2b_hex::<hash_size::AccountId>(&["InboxV2|", "delivery_address|", account_id.as_str()])
fn delivery_address_for(ident_id: IdentIdRef) -> String {
blake2b_hex::<hash_size::DeliveryAddr>(&["InboxV2|", "delivery_address|", ident_id.as_str()])
}
fn conversation_id_for(account_id: &AccountId) -> String {
blake2b_hex::<hash_size::ConvoId>(&["InboxV2|", "conversation_id|", account_id.as_str()])
fn conversation_id_for(ident_id: IdentIdRef) -> String {
blake2b_hex::<hash_size::ConvoId>(&["InboxV2|", "conversation_id|", ident_id.as_str()])
}
/// An Extension trait which extends OpenMlsProvider to add required functionality
@@ -36,7 +37,7 @@ pub trait MlsProvider: OpenMlsProvider {
fn invite_user<DS: DeliveryService>(
&self,
ds: &mut DS,
account_id: &AccountId,
ident_id: IdentIdRef,
welcome: &MlsMessageOut,
) -> Result<(), ChatError>;
}
@@ -46,16 +47,16 @@ pub trait MlsProvider: OpenMlsProvider {
/// such as MLS.
pub struct InboxV2 {
// Account_id field is an owned value, so it can be returned via reference.
account_id: AccountId,
ident_id: IdentId,
}
impl InboxV2 {
pub fn new(account_id: AccountId) -> Self {
Self { account_id }
pub fn new(ident_id: IdentId) -> Self {
Self { ident_id }
}
pub fn account_id(&self) -> &AccountId {
&self.account_id
pub fn ident_id(&self) -> IdentIdRef<'_> {
&self.ident_id
}
/// Submit MlsKeypackage to registration service
@@ -73,11 +74,11 @@ impl InboxV2 {
}
pub fn delivery_address(&self) -> String {
delivery_address_for(&self.account_id)
delivery_address_for(&self.ident_id)
}
pub fn id(&self) -> String {
conversation_id_for(&self.account_id)
conversation_id_for(&self.ident_id)
}
pub fn handle_frame<S: ExternalServices>(
+5 -4
View File
@@ -5,8 +5,9 @@ use openmls_traits::{
signatures::{Signer, SignerError},
types::SignatureScheme,
};
use shared_traits::IdentIdRef;
use crate::{AccountId, IdentityProvider};
use crate::IdentityProvider;
/// A Wrapper for an IdentityProvider which provides MLS specific functionality
///
@@ -22,7 +23,7 @@ impl<T: IdentityProvider> MlsIdentityProvider<T> {
pub fn get_credential(&self) -> CredentialWithKey {
CredentialWithKey {
credential: BasicCredential::new(self.account_id().as_str().as_bytes().to_vec()).into(),
credential: BasicCredential::new(self.id().as_str().as_bytes().to_vec()).into(),
signature_key: self.public_key().as_ref().into(),
}
}
@@ -37,8 +38,8 @@ impl<T: IdentityProvider> Deref for MlsIdentityProvider<T> {
}
impl<T: IdentityProvider> IdentityProvider for MlsIdentityProvider<T> {
fn account_id(&self) -> &AccountId {
self.0.account_id()
fn id(&self) -> IdentIdRef<'_> {
self.0.id()
}
fn display_name(&self) -> String {
@@ -4,8 +4,9 @@ use openmls_memory_storage::MemoryStorage;
use openmls_traits::OpenMlsProvider;
use openmls_traits::types::CryptoError;
use prost::Message;
use shared_traits::IdentIdRef;
use crate::{AccountId, ChatError, DeliveryService};
use crate::{ChatError, DeliveryService};
use super::{
AddressedEnvelope, EnvelopeV1, GroupV1HeavyInvite, InboxV2Frame, InviteType, MlsProvider,
@@ -31,7 +32,7 @@ impl MlsProvider for MlsEphemeralPqProvider {
fn invite_user<DS: DeliveryService>(
&self,
ds: &mut DS,
account_id: &AccountId,
ident_id: IdentIdRef,
welcome: &MlsMessageOut,
) -> Result<(), ChatError> {
let invite = GroupV1HeavyInvite {
@@ -43,13 +44,13 @@ impl MlsProvider for MlsEphemeralPqProvider {
};
let envelope = EnvelopeV1 {
conversation_hint: conversation_id_for(account_id),
conversation_hint: conversation_id_for(ident_id),
salt: 0,
payload: frame.encode_to_vec().into(),
};
let outbound_msg = AddressedEnvelope {
delivery_address: delivery_address_for(account_id),
delivery_address: delivery_address_for(ident_id),
data: envelope.encode_to_vec(),
};
+3 -4
View File
@@ -1,4 +1,3 @@
mod account;
mod causal_history;
mod conversation;
mod core;
@@ -13,7 +12,6 @@ mod service_traits;
mod types;
mod utils;
pub use account::LogosAccount;
pub use causal_history::{Frontier, MissingMessage};
pub use chat_sqlite::ChatStorage;
pub use chat_sqlite::StorageConfig;
@@ -23,7 +21,8 @@ pub use outcomes::{
Content, ConversationClass, ConvoOutcome, InboxOutcome, NewConversation, PayloadOutcome,
};
pub use service_context::ExternalServices;
pub use service_traits::{DeliveryService, IdentityProvider, RegistrationService};
pub use service_traits::{DeliveryService, RegistrationService};
pub use shared_traits::IdentityProvider;
pub use storage::ConversationKind;
pub use types::{AccountId, AddressedEnvelope};
pub use types::AddressedEnvelope;
pub use utils::hex_trunc;
+10 -7
View File
@@ -3,7 +3,7 @@
use crypto::Identity;
use storage::ChatStore;
use crate::account::LogosAccount;
use crate::IdentityProvider;
use crate::causal_history::CausalHistoryStore;
use crate::inbox_v2::{MlsEphemeralPqProvider, MlsIdentityProvider};
use crate::{DeliveryService, RegistrationService};
@@ -11,17 +11,20 @@ use crate::{DeliveryService, RegistrationService};
/// Bundles the external service types (`DS`, `RS`, `CS`) behind one `S`. The
/// `(DS, RS, CS)` tuple impl lets them still be supplied separately.
pub trait ExternalServices {
type IP: IdentityProvider;
type DS: DeliveryService;
type RS: RegistrationService;
type CS: ChatStore;
}
impl<DS, RS, CS> ExternalServices for (DS, RS, CS)
impl<IP, DS, RS, CS> ExternalServices for (IP, DS, RS, CS)
where
IP: IdentityProvider,
DS: DeliveryService,
RS: RegistrationService,
CS: ChatStore,
{
type IP = IP;
type DS = DS;
type RS = RS;
type CS = CS;
@@ -32,7 +35,7 @@ pub(crate) struct ServiceContext<S: ExternalServices> {
pub(crate) ds: S::DS,
pub(crate) registry: S::RS,
pub(crate) store: S::CS,
pub(crate) mls_identity: MlsIdentityProvider<LogosAccount>,
pub(crate) mls_identity: MlsIdentityProvider<S::IP>,
pub(crate) mls_provider: MlsEphemeralPqProvider,
pub(crate) causal: CausalHistoryStore,
pub(crate) identity: Identity,
@@ -80,15 +83,15 @@ mod test_support {
}
}
impl<CS: ChatStore> ServiceContext<(NoopDelivery, NoopRegistration, CS)> {
impl<IP: IdentityProvider, CS: ChatStore> ServiceContext<(IP, NoopDelivery, NoopRegistration, CS)> {
/// Builds a context around a real store, stubbing other services.
pub(crate) fn for_test(name: &str, store: CS) -> Result<Self, ChatError> {
let account = LogosAccount::new_test(name);
pub(crate) fn for_test(ident: IP, store: CS) -> Result<Self, ChatError> {
let name = ident.id().as_str().to_string();
Ok(Self {
ds: NoopDelivery,
registry: NoopRegistration,
store,
mls_identity: MlsIdentityProvider::new(account),
mls_identity: MlsIdentityProvider::new(ident),
mls_provider: MlsEphemeralPqProvider::new().map_err(ChatError::generic)?,
causal: CausalHistoryStore::new(),
identity: Identity::new(name),
+2 -14
View File
@@ -1,11 +1,10 @@
/// Service traits define the functionality which must be externally supplied by
/// platform clients. Platforms can alter the behaviour of the chat core by supplying
/// different implementations.
use shared_traits::IdentityProvider;
use std::{fmt::Debug, fmt::Display};
use crypto::{Ed25519Signature, Ed25519VerifyingKey};
use crate::types::{AccountId, AddressedEnvelope};
use crate::types::AddressedEnvelope;
/// A Delivery service is responsible for payload transport.
/// This interface allows Conversations to send payloads on the wire as well as
@@ -50,14 +49,3 @@ impl<T: RegistrationService> KeyPackageProvider for T {
RegistrationService::retrieve(self, device_id)
}
}
/// Represents an external Identity
/// Implement this to provide an Authentication model for users/installations
pub trait IdentityProvider {
fn account_id(&self) -> &AccountId;
// Display name is not garenteed to be consistent. It should only be used to
// provded a more readable identifier for the account.
fn display_name(&self) -> String;
fn sign(&self, payload: &[u8]) -> Ed25519Signature;
fn public_key(&self) -> &Ed25519VerifyingKey;
}
+1 -29
View File
@@ -1,4 +1,4 @@
use std::fmt::{self, Debug};
use std::fmt::Debug;
use crate::proto::{self, Message};
@@ -66,31 +66,3 @@ impl AddressedEncryptedPayload {
)
}
}
/// This represents an Identifier for an account.
/// Its a thin wrapper around a string, but providers extra functionality,
/// and ensures type consistency
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AccountId(String);
impl AccountId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AccountId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl AsRef<str> for AccountId {
fn as_ref(&self) -> &str {
&self.0
}
}
+3 -4
View File
@@ -11,7 +11,7 @@ pub fn timestamp_millis() -> i64 {
/// Track hash sizes in use across the crate.
pub mod hash_size {
use blake2::digest::{
consts::{U32, U64},
consts::{U4, U6, U32, U64},
generic_array::ArrayLength,
typenum::{IsLessOrEqual, NonZero},
};
@@ -34,12 +34,11 @@ pub mod hash_size {
};
}
use blake2::digest::consts::{U6, U8};
hash_sizes! {
/// Account ID hash length
AccountId => U8,
/// Conversation ID hash length
ConvoId => U6,
/// Delivery Address length
DeliveryAddr => U4,
/// Causal history message ID hash length (256-bit, collision-resistant)
MessageId => U32,
}