diff --git a/core/conversations/src/lib.rs b/core/conversations/src/lib.rs index b701b12..edc093e 100644 --- a/core/conversations/src/lib.rs +++ b/core/conversations/src/lib.rs @@ -31,4 +31,4 @@ pub use service_traits::{DeliveryService, RegistrationService, WakeupService}; pub use shared_traits::{IdentId, IdentIdRef, IdentityProvider}; pub use storage::ConversationKind; pub use types::AddressedEnvelope; -pub use utils::hex_trunc; +pub use utils::{hex_trunc, trunc}; diff --git a/core/conversations/src/utils.rs b/core/conversations/src/utils.rs index 3ed1059..4dde6a4 100644 --- a/core/conversations/src/utils.rs +++ b/core/conversations/src/utils.rs @@ -71,3 +71,12 @@ pub fn hex_trunc(data: &[u8]) -> String { ) } } + +#[allow(unused)] +pub fn trunc(data: &str) -> String { + if data.len() <= 8 { + data.to_string() + } else { + format!("{}..{}", &data[..4], &data[data.len() - 4..]) + } +} diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index de97a73..ec34895 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -11,10 +11,13 @@ crate-type = ["rlib"] chat-sqlite = { workspace = true } components = { workspace = true } crossbeam-channel = { workspace = true } +crypto = { workspace = true } libchat = { workspace = true } logos-account = { workspace = true, features = ["dev"]} +shared-traits = { workspace = true } # External dependencies (sorted) +hex = "0.4.3" parking_lot = "0.12" thiserror = "2" tracing = "0.1" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4337bf9..95b99aa 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -7,13 +7,13 @@ use libchat::{ ChatError, ChatStorage, ConversationId, ConvoOutcome, Core, DeliveryService, IdentId, IdentIdRef, InboxOutcome, Introduction, PayloadOutcome, RegistrationService, StorageConfig, }; -use logos_account::TestLogosAccount; use parking_lot::Mutex; +use crate::delegate::{self, DelegateCredential, DelegateSigner}; use crate::errors::ClientError; use crate::event::Event; -type ClientCore = Core<(TestLogosAccount, T, R, ThreadedWakeupService, ChatStorage)>; +type ClientCore = Core<(DelegateSigner, T, R, ThreadedWakeupService, ChatStorage)>; type AccountAddressRef<'a> = &'a str; type LocalSignerId = IdentId; @@ -53,11 +53,12 @@ impl ChatClient { /// Create an in-memory, ephemeral client. Identity is lost on drop. pub fn new(name: impl Into, mut transport: T) -> (Self, Receiver) { let inbound = transport.inbound(); - let ident = TestLogosAccount::new(name); + let delegate = DelegateSigner::random(); + let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded(); let wakeup_service = ThreadedWakeupService::new(wakeup_tx); let core = Core::new_with_name( - ident, + delegate, transport, EphemeralRegistry::new(), wakeup_service, @@ -78,11 +79,11 @@ impl ChatClient { ) -> Result<(Self, Receiver), ClientError> { let store = ChatStorage::new(config).map_err(ChatError::from)?; let inbound = transport.inbound(); - let ident = TestLogosAccount::new(name); + let delegate = DelegateSigner::random(); let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded(); let wakeup_service = ThreadedWakeupService::new(wakeup_tx); let core = Core::new_from_store( - ident, + delegate, transport, EphemeralRegistry::new(), wakeup_service, @@ -96,7 +97,7 @@ impl ChatClient { impl ChatClient where - T: DeliveryService + Send + 'static, + T: Transport + Send + 'static, R: RegistrationService + Send + 'static, { /// Open or create a persistent client with a caller-supplied registration @@ -118,14 +119,35 @@ where { let store = ChatStorage::new(config).map_err(ChatError::from)?; let inbound = transport.inbound(); - let ident = TestLogosAccount::new(name); + let delegate = DelegateSigner::random(); let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded(); let wakeup_service = ThreadedWakeupService::new(wakeup_tx); - let mut core = Core::new_from_store(ident, transport, registry, wakeup_service, store)?; + let mut core = Core::new_from_store(delegate, transport, registry, wakeup_service, store)?; core.register_keypackage()?; Ok(Self::spawn(core, inbound, wakeup_rx)) } + /// Create a client with ephemeral storage with the provided Transport and RegistrationService. + pub fn new_ephemeral( + delegate: DelegateSigner, + mut transport: T, + reg: R, + ) -> (Self, Receiver) { + let inbound = transport.inbound(); + + let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded(); + let wakeup_service = ThreadedWakeupService::new(wakeup_tx); + let core = Core::new_with_name( + delegate, + transport, + reg, + wakeup_service, + ChatStorage::in_memory(), + ) + .unwrap(); + Self::spawn(core, inbound, wakeup_rx) + } + fn spawn( core: ClientCore, inbound: Receiver>, @@ -290,9 +312,15 @@ fn events_from_inbound(result: PayloadOutcome) -> Vec { fn convo_events(outcome: ConvoOutcome) -> Vec { let ConvoOutcome { convo_id, content } = outcome; content - .map(|c| Event::MessageReceived { - convo_id: Arc::from(convo_id), - content: c.bytes, + .map(|c| { + let data = hex::decode(c.encoded_credential).unwrap(); + let delegate_cred = DelegateCredential::from(data); + println!("{:?}", delegate_cred); + + Event::MessageReceived { + convo_id: Arc::from(convo_id), + content: c.bytes, + } }) .into_iter() .collect() diff --git a/crates/client/src/delegate.rs b/crates/client/src/delegate.rs new file mode 100644 index 0000000..e061bed --- /dev/null +++ b/crates/client/src/delegate.rs @@ -0,0 +1,151 @@ +use crypto::{Ed25519SigningKey, Ed25519VerifyingKey}; +use libchat::{IdentId, IdentityProvider}; + +use crate::ClientError; + +type AccountAddr = String; + +pub struct DelegateSigner { + signing_key: Ed25519SigningKey, + verifying_key: Ed25519VerifyingKey, + identifier: IdentId, + account_addr: Option, +} + +impl DelegateSigner { + pub fn random() -> Self { + let signing_key = Ed25519SigningKey::generate(); + let verifying_key = signing_key.verifying_key(); + let identifier = DelegateCredential::unassociated(&verifying_key).into(); + Self { + signing_key, + verifying_key, + identifier, + account_addr: None, + } + } + + pub fn associate(&mut self, account_addr: AccountAddr) { + self.identifier = + DelegateCredential::associated(&self.verifying_key, account_addr.as_str()).into(); + self.account_addr = Some(account_addr); + } + + pub fn account_addr(&self) -> Option<&str> { + self.account_addr.as_deref() + } +} + +impl IdentityProvider for DelegateSigner { + fn id(&self) -> libchat::IdentIdRef<'_> { + &self.identifier + } + + fn display_name(&self) -> String { + trunc(self.identifier.as_str()) + } + + fn sign(&self, payload: &[u8]) -> crypto::Ed25519Signature { + self.signing_key.sign(payload) + } + + fn public_key(&self) -> &Ed25519VerifyingKey { + &self.verifying_key + } +} + +/// Represents the senders information for received frames. +#[derive(Debug)] +pub struct DelegateCredential { + delegate_id: Ed25519VerifyingKey, + account_addr: Option, +} + +impl DelegateCredential { + const TAG_DELEGATE_ID: u8 = 0x01; + const TAG_ACCOUNT_ADDR: u8 = 0x02; + + pub fn unassociated(delegate: &Ed25519VerifyingKey) -> Self { + Self { + delegate_id: delegate.clone(), + account_addr: None, + } + } + + pub fn associated(delegate: &Ed25519VerifyingKey, account: &str) -> Self { + Self { + delegate_id: delegate.clone(), + account_addr: Some(account.to_string()), + } + } + + pub fn to_vec(self) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&[0x23, 0x23]); + let key_bytes = self.delegate_id.as_ref(); + data.extend_from_slice(&[Self::TAG_DELEGATE_ID, key_bytes.len() as u8]); + data.extend_from_slice(key_bytes); + if let Some(addr) = self.account_addr { + let addr_bytes = addr.as_bytes(); + data.extend_from_slice(&[Self::TAG_ACCOUNT_ADDR, addr_bytes.len() as u8]); + data.extend_from_slice(addr_bytes); + } + data + } +} + +impl From for Vec { + fn from(value: DelegateCredential) -> Self { + value.to_vec() + } +} + +impl From> for DelegateCredential { + fn from(value: Vec) -> Self { + assert_eq!(&value[..2], &[0x23, 0x23], "invalid magic bytes"); + let mut delegate_id = None; + let mut account_addr = None; + let mut i = 2; + while i + 2 <= value.len() { + let tag = value[i]; + let len = value[i + 1] as usize; + i += 2; + let v = &value[i..i + len]; + i += len; + match tag { + DelegateCredential::TAG_DELEGATE_ID => { + let bytes: &[u8; 32] = v.try_into().expect("invalid delegate_id length"); + delegate_id = Some( + Ed25519VerifyingKey::from_bytes(bytes).expect("invalid verifying key"), + ); + } + DelegateCredential::TAG_ACCOUNT_ADDR => { + account_addr = + Some(String::from_utf8(v.to_vec()).expect("invalid account_addr utf8")); + } + _ => {} + } + } + Self { + delegate_id: delegate_id.expect("missing delegate_id TLV field"), + account_addr, + } + } +} + +impl From for IdentId { + fn from(value: DelegateCredential) -> Self { + IdentId::new(hex::encode(value.to_vec())) + } +} + +impl TryFrom for DelegateCredential { + type Error = ClientError; + + fn try_from(value: IdentId) -> Result { + Ok(hex::decode(value.as_str()) + .map_err(|e| ClientError::BadlyFormedCredential)? + .into()) + } +} + diff --git a/crates/client/src/errors.rs b/crates/client/src/errors.rs index 23f352a..4420ccf 100644 --- a/crates/client/src/errors.rs +++ b/crates/client/src/errors.rs @@ -4,4 +4,6 @@ use libchat::ChatError; pub enum ClientError { #[error(transparent)] Chat(#[from] ChatError), + #[error("received credential could not be parsed")] + BadlyFormedCredential, }