From d40e72be9b82ed1d75aa3c22c53ada4048f2c733 Mon Sep 17 00:00:00 2001 From: Jazz Turner-Baggs <473256+jazzz@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:53:44 +0700 Subject: [PATCH] Add Introductions (#22) --- conversations/src/context.rs | 13 ++--- conversations/src/errors.rs | 4 ++ conversations/src/inbox.rs | 2 + conversations/src/inbox/inbox.rs | 19 ++++++-- conversations/src/inbox/introduction.rs | 65 +++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 conversations/src/inbox/introduction.rs diff --git a/conversations/src/context.rs b/conversations/src/context.rs index 0c78331..393b6c3 100644 --- a/conversations/src/context.rs +++ b/conversations/src/context.rs @@ -6,9 +6,11 @@ use crate::{ conversation::{ConversationId, ConversationIdOwned, ConversationStore}, identity::Identity, inbox::Inbox, + proto, types::{ContentData, PayloadData}, }; +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 { @@ -30,17 +32,16 @@ impl Context { pub fn create_private_convo( &mut self, - remote_bundle: &PrekeyBundle, + remote_bundle: &Introduction, content: String, - ) -> ConversationIdOwned { - let (convo, _payloads) = self + ) -> (ConversationIdOwned, Vec) { + let (convo, payloads) = self .inbox .invite_to_private_convo(remote_bundle, content) .unwrap_or_else(|_| todo!("Log/Surface Error")); - self.store.insert_convo(convo) - - // TODO: Change return type to handle outbout packets. + let convo_id = self.store.insert_convo(convo); + (convo_id, payloads) } pub fn send_content(&mut self, _convo_id: ConversationId, _content: &[u8]) -> Vec { diff --git a/conversations/src/errors.rs b/conversations/src/errors.rs index b9bb8dc..d6c82b6 100644 --- a/conversations/src/errors.rs +++ b/conversations/src/errors.rs @@ -12,4 +12,8 @@ pub enum ChatError { BadBundleValue(String), #[error("handshake initiated with a unknown ephemeral key")] UnknownEphemeralKey(), + #[error("expected a different key length")] + InvalidKeyLength, + #[error("bytes provided to {0} failed")] + BadParsing(&'static str), } diff --git a/conversations/src/inbox.rs b/conversations/src/inbox.rs index 0322818..d669e0d 100644 --- a/conversations/src/inbox.rs +++ b/conversations/src/inbox.rs @@ -1,4 +1,6 @@ mod handshake; mod inbox; +mod introduction; pub use inbox::Inbox; +pub use introduction::Introduction; diff --git a/conversations/src/inbox/inbox.rs b/conversations/src/inbox/inbox.rs index bb919e2..73382d7 100644 --- a/conversations/src/inbox/inbox.rs +++ b/conversations/src/inbox/inbox.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use crypto::{PrekeyBundle, SecretKey}; +use crate::context::Introduction; use crate::conversation::{ChatError, ConversationId, Convo, ConvoFactory, Id, PrivateV1Convo}; use crate::crypto::{Blake2b128, CopyBytes, Digest, PublicKey, StaticSecret}; use crate::identity::Identity; @@ -66,13 +67,21 @@ impl Inbox { pub fn invite_to_private_convo( &self, - remote_bundle: &PrekeyBundle, + remote_bundle: &Introduction, initial_message: String, ) -> Result<(PrivateV1Convo, Vec), ChatError> { let mut rng = OsRng; + // TODO: Include signature in introduction bundle. Manaully fill for now + let pkb = PrekeyBundle { + identity_key: remote_bundle.installation_key, + signed_prekey: remote_bundle.ephemeral_key, + signature: [0u8; 64], + onetime_prekey: None, + }; + let (seed_key, ephemeral_pub) = - InboxHandshake::perform_as_initiator(&self.ident.secret(), remote_bundle, &mut rng); + InboxHandshake::perform_as_initiator(&self.ident.secret(), &pkb, &mut rng); let mut convo = PrivateV1Convo::new(seed_key); @@ -89,8 +98,8 @@ impl Inbox { let header = proto::InboxHeaderV1 { initiator_static: self.ident.public_key().copy_to_bytes(), initiator_ephemeral: ephemeral_pub.copy_to_bytes(), - responder_static: remote_bundle.identity_key.copy_to_bytes(), - responder_ephemeral: remote_bundle.signed_prekey.copy_to_bytes(), + responder_static: remote_bundle.installation_key.copy_to_bytes(), + responder_ephemeral: remote_bundle.ephemeral_key.copy_to_bytes(), }; let handshake = proto::InboxHandshakeV1 { @@ -226,7 +235,7 @@ mod tests { let bundle = raya_inbox.create_bundle(); let (_, payloads) = saro_inbox - .invite_to_private_convo(&bundle, "hello".into()) + .invite_to_private_convo(&bundle.into(), "hello".into()) .unwrap(); let encrypted_payload = payloads diff --git a/conversations/src/inbox/introduction.rs b/conversations/src/inbox/introduction.rs new file mode 100644 index 0000000..2966293 --- /dev/null +++ b/conversations/src/inbox/introduction.rs @@ -0,0 +1,65 @@ +use crypto::PrekeyBundle; +use x25519_dalek::PublicKey; + +use crate::errors::ChatError; + +/// Supplies remote participants with the required keys to use Inbox protocol +pub struct Introduction { + pub installation_key: PublicKey, + pub ephemeral_key: PublicKey, +} + +impl From for Introduction { + fn from(value: PrekeyBundle) -> Self { + Introduction { + installation_key: value.identity_key, + ephemeral_key: value.signed_prekey, + } + } +} + +impl Into> for Introduction { + fn into(self) -> Vec { + // TODO: avoid copies, via writing directly to slice + let link = format!( + "Bundle:{}:{}", + hex::encode(self.installation_key.as_bytes()), + hex::encode(self.ephemeral_key.as_bytes()), + ); + + link.into_bytes() + } +} + +impl TryFrom> for Introduction { + type Error = ChatError; + + fn try_from(value: Vec) -> Result { + let str_value = + String::from_utf8(value).map_err(|_| ChatError::BadParsing("Introduction"))?; + let parts: Vec<&str> = str_value.splitn(3, ':').collect(); + + if parts[0] != "Bundle" { + return Err(ChatError::BadBundleValue( + "not recognized as an introduction bundle".into(), + )); + } + + let installation_bytes: [u8; 32] = hex::decode(parts[1]) + .map_err(|_| ChatError::BadParsing("installation_key"))? + .try_into() + .map_err(|_| ChatError::InvalidKeyLength)?; + let installation_key = PublicKey::from(installation_bytes); + + let ephemeral_bytes: [u8; 32] = hex::decode(parts[1]) + .map_err(|_| ChatError::BadParsing("ephemeral_key"))? + .try_into() + .map_err(|_| ChatError::InvalidKeyLength)?; + let ephemeral_key = PublicKey::from(ephemeral_bytes); + + Ok(Introduction { + installation_key, + ephemeral_key, + }) + } +}