mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-06-27 19:49:31 +00:00
Add group description
This commit is contained in:
parent
97eacc01a7
commit
afbb75d294
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -2104,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3707,7 +3707,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4993,7 +4993,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5051,7 +5051,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5661,7 +5661,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6359,7 +6359,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
mod direct_v1;
|
||||
mod group_cent_info;
|
||||
pub mod group_v1;
|
||||
mod group_v2;
|
||||
pub mod mls_extensions;
|
||||
mod privatev1;
|
||||
|
||||
pub use crate::errors::ChatError;
|
||||
use crate::outcomes::ConvoOutcome;
|
||||
use crate::proto::EncryptedPayload;
|
||||
use crate::service_context::{ExternalServices, ServiceContext};
|
||||
use crate::types::ConvoMetadata;
|
||||
pub use direct_v1::DirectV1Convo;
|
||||
pub use group_cent_info::GroupCentInfoConvo;
|
||||
pub use group_v1::GroupV1Convo;
|
||||
pub use group_v2::GroupV2Convo;
|
||||
pub use privatev1::PrivateV1Convo;
|
||||
@ -42,6 +46,8 @@ pub(crate) trait GroupConvo<S: ExternalServices>: Convo<S> + std::fmt::Debug + S
|
||||
cx: &mut ServiceContext<S>,
|
||||
members: &[IdentIdRef],
|
||||
) -> Result<(), ChatError>;
|
||||
|
||||
fn metadata(&self) -> ConvoMetadata;
|
||||
}
|
||||
|
||||
pub(crate) trait Identified {
|
||||
|
||||
371
core/conversations/src/conversation/group_cent_info.rs
Normal file
371
core/conversations/src/conversation/group_cent_info.rs
Normal file
@ -0,0 +1,371 @@
|
||||
/// GroupV1 is a conversationType which provides effecient handling of multiple participants
|
||||
/// Properties:
|
||||
/// - Harvest Now Decrypt Later (HNDL) protection provided by XWING
|
||||
/// - Multiple
|
||||
use blake2::{Blake2b, Digest, digest::consts::U6};
|
||||
use chat_proto::logoschat::encryption::{EncryptedPayload, Plaintext, encrypted_payload};
|
||||
use chat_proto::logoschat::reliability::ReliablePayload;
|
||||
use openmls::prelude::tls_codec::Deserialize;
|
||||
use openmls::prelude::*;
|
||||
use prost::Message as _;
|
||||
use shared_traits::IdentIdRef;
|
||||
use std::collections::VecDeque;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::account_directory::{AccountDirectory, resolve_device_ids};
|
||||
use crate::conversation::ConversationIdRef;
|
||||
use crate::inbox_v2::MlsProvider;
|
||||
use crate::service_context::{ExternalServices, ServiceContext};
|
||||
use crate::types::ConvoMetadata;
|
||||
use crate::utils::{blake2b_hex, hash_size};
|
||||
use crate::{
|
||||
DeliveryService, IdentityProvider,
|
||||
conversation::{ChatError, Convo, GroupConvo, Identified},
|
||||
outcomes::{Content, ConvoOutcome},
|
||||
service_traits::KeyPackageProvider,
|
||||
types::AddressedEncryptedPayload,
|
||||
};
|
||||
|
||||
use super::mls_extensions::{
|
||||
ConvoMetaInfo, GROUP_METADATA_EXTENSION_TYPE, capabilities_with_group_metadata,
|
||||
};
|
||||
|
||||
const OUTBOUND_HASH_CACHE_SIZE: usize = 25;
|
||||
|
||||
pub struct GroupCentInfoConvo {
|
||||
mls_group: MlsGroup,
|
||||
convo_id: String,
|
||||
// Cache outbound message Id's to filter out re-entrant messages
|
||||
outbound_msgs: VecDeque<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GroupCentInfoConvo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GroupCentInfoConvo")
|
||||
.field("convo_id", &self.convo_id)
|
||||
.field("mls_epoch", &self.mls_group.epoch())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupCentInfoConvo {
|
||||
// Create a new conversation with the creator as the only participant.
|
||||
pub fn new<S: ExternalServices>(
|
||||
cx: &mut ServiceContext<S>,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
) -> Result<Self, ChatError> {
|
||||
let config = Self::mls_create_config(cx, name, desc);
|
||||
let mls_group = MlsGroup::new(
|
||||
&cx.mls_provider,
|
||||
&cx.mls_identity,
|
||||
&config,
|
||||
cx.mls_identity.get_credential(),
|
||||
)
|
||||
.unwrap();
|
||||
let convo_id = hex::encode(mls_group.group_id().as_slice());
|
||||
Self::subscribe(&mut cx.ds, &convo_id)?;
|
||||
|
||||
Ok(Self {
|
||||
mls_group,
|
||||
convo_id,
|
||||
outbound_msgs: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
// Constructs a new conversation upon receiving a MlsWelcome message.
|
||||
pub fn new_from_welcome<S: ExternalServices>(
|
||||
cx: &mut ServiceContext<S>,
|
||||
welcome: Welcome,
|
||||
) -> Result<Self, ChatError> {
|
||||
let mls_group =
|
||||
StagedWelcome::build_from_welcome(&cx.mls_provider, &Self::mls_join_config(), welcome)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
.into_group(&cx.mls_provider)
|
||||
.unwrap();
|
||||
|
||||
let convo_id = hex::encode(mls_group.group_id().as_slice());
|
||||
Self::subscribe(&mut cx.ds, &convo_id)?;
|
||||
|
||||
Ok(Self {
|
||||
mls_group,
|
||||
convo_id,
|
||||
outbound_msgs: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
// Configure the delivery service to listen for the required delivery addresses.
|
||||
fn subscribe(ds: &mut impl DeliveryService, convo_id: &str) -> Result<(), ChatError> {
|
||||
ds.subscribe(&Self::delivery_address_from_id(convo_id))
|
||||
.map_err(ChatError::generic)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mls_create_config<S: ExternalServices>(
|
||||
cx: &mut ServiceContext<S>,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
) -> MlsGroupCreateConfig {
|
||||
let meta = ConvoMetaInfo::new(name, cx.mls_identity.id(), desc);
|
||||
|
||||
let extensions = Extensions::from_vec(vec![Extension::Unknown(
|
||||
GROUP_METADATA_EXTENSION_TYPE,
|
||||
UnknownExtension(meta.to_extension_bytes()),
|
||||
)])
|
||||
.expect("failed to create extensions");
|
||||
|
||||
MlsGroupCreateConfig::builder()
|
||||
.ciphersuite(cx.mls_provider.crypto().supported_ciphersuites()[0])
|
||||
.capabilities(capabilities_with_group_metadata())
|
||||
.use_ratchet_tree_extension(true) // Embed the ratchet tree in the Welcome so joiners can build the group
|
||||
.with_group_context_extensions(extensions)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn mls_join_config() -> MlsGroupJoinConfig {
|
||||
MlsGroupJoinConfig::builder().build()
|
||||
}
|
||||
|
||||
fn delivery_address_from_id(convo_id: &str) -> String {
|
||||
let hash = Blake2b::<U6>::new()
|
||||
.chain_update("delivery_addr|")
|
||||
.chain_update(convo_id)
|
||||
.finalize();
|
||||
hex::encode(hash)
|
||||
}
|
||||
|
||||
fn delivery_address(&self) -> String {
|
||||
Self::delivery_address_from_id(&self.convo_id)
|
||||
}
|
||||
|
||||
/// Resolve an account to a KeyPackage for *every* device it authorizes.
|
||||
///
|
||||
/// First resolves the account to its device ids through the account
|
||||
/// directory ([`resolve_device_ids`]), then fetches each device's
|
||||
/// KeyPackage. When the account never published a bundle, resolution falls
|
||||
/// back to a single device id equal to the account id — the pre-directory
|
||||
/// behaviour — so single-device accounts are unaffected.
|
||||
fn key_packages_for_account(
|
||||
&self,
|
||||
ident: IdentIdRef,
|
||||
provider: &impl MlsProvider,
|
||||
registry: &(impl KeyPackageProvider + AccountDirectory),
|
||||
) -> Result<Vec<KeyPackage>, ChatError> {
|
||||
let device_ids =
|
||||
resolve_device_ids(registry, ident).map_err(|e| ChatError::Generic(e.to_string()))?;
|
||||
|
||||
let mut keypackages = Vec::with_capacity(device_ids.len());
|
||||
for device_id in &device_ids {
|
||||
let retrieved = registry
|
||||
.retrieve(device_id)
|
||||
.map_err(|e| ChatError::Generic(e.to_string()))?;
|
||||
let Some(keypkg_bytes) = retrieved else {
|
||||
return Err(ChatError::Protocol(format!(
|
||||
"no keypackage for device {device_id} of account {ident}"
|
||||
)));
|
||||
};
|
||||
|
||||
let key_package_in = KeyPackageIn::tls_deserialize(&mut keypkg_bytes.as_slice())?;
|
||||
let keypkg = key_package_in.validate(provider.crypto(), ProtocolVersion::Mls10)?; //TODO: P3 - Hardcoded Protocol Version
|
||||
keypackages.push(keypkg);
|
||||
}
|
||||
Ok(keypackages)
|
||||
}
|
||||
|
||||
fn send_message<S: ExternalServices>(
|
||||
&mut self,
|
||||
content: &[u8],
|
||||
cx: &mut ServiceContext<S>,
|
||||
) -> Result<(), ChatError> {
|
||||
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();
|
||||
|
||||
let mls_message_out = self
|
||||
.mls_group
|
||||
.create_message(&cx.mls_provider, &cx.mls_identity, &wire)
|
||||
.unwrap();
|
||||
|
||||
let msg_bytes = mls_message_out.to_bytes().unwrap();
|
||||
self.send_payload(cx, msg_bytes)
|
||||
}
|
||||
|
||||
// Publish outboubound payloads to the DeliveryService
|
||||
fn send_payload<S: ExternalServices>(
|
||||
&mut self,
|
||||
cx: &mut ServiceContext<S>,
|
||||
msg_bytes: Vec<u8>,
|
||||
) -> Result<(), ChatError> {
|
||||
// Hash and Cache to detect inbound messages
|
||||
let msg_hash = blake2b_hex::<hash_size::MessageId>(&[&msg_bytes]);
|
||||
self.outbound_msgs.push_back(msg_hash);
|
||||
if self.outbound_msgs.len() > OUTBOUND_HASH_CACHE_SIZE {
|
||||
let _ = self.outbound_msgs.remove(0);
|
||||
}
|
||||
|
||||
// Wrap in Payload frames
|
||||
let aep = AddressedEncryptedPayload {
|
||||
delivery_address: self.delivery_address(),
|
||||
data: EncryptedPayload {
|
||||
encryption: Some(encrypted_payload::Encryption::Plaintext(Plaintext {
|
||||
payload: msg_bytes.into(),
|
||||
})),
|
||||
},
|
||||
};
|
||||
let env = aep.into_envelope(self.convo_id.clone());
|
||||
|
||||
// Send via DS
|
||||
cx.ds
|
||||
.publish(env)
|
||||
.map_err(|e| ChatError::Delivery(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Identified for GroupCentInfoConvo {
|
||||
fn id(&self) -> ConversationIdRef<'_> {
|
||||
&self.convo_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ExternalServices> Convo<S> for GroupCentInfoConvo {
|
||||
fn send_content(
|
||||
&mut self,
|
||||
cx: &mut ServiceContext<S>,
|
||||
content: &[u8],
|
||||
) -> Result<(), ChatError> {
|
||||
self.send_message(content, cx)
|
||||
}
|
||||
|
||||
fn handle_frame(
|
||||
&mut self,
|
||||
cx: &mut ServiceContext<S>,
|
||||
encoded_payload: EncryptedPayload,
|
||||
) -> Result<ConvoOutcome, ChatError> {
|
||||
let bytes = match encoded_payload.encryption {
|
||||
Some(encrypted_payload::Encryption::Plaintext(pt)) => pt.payload,
|
||||
_ => {
|
||||
return Err(ChatError::ProtocolExpectation(
|
||||
"None",
|
||||
"Some(Encryption::Plaintext)".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Bail early if we sent this message
|
||||
let msg_hash = blake2b_hex::<hash_size::MessageId>(&[bytes.as_ref()]);
|
||||
if self.outbound_msgs.contains(&msg_hash) {
|
||||
debug!("Dropping message, sent from self");
|
||||
return Ok(ConvoOutcome::empty(self.convo_id.to_string()));
|
||||
}
|
||||
|
||||
let mls_message: MlsMessageIn =
|
||||
MlsMessageIn::tls_deserialize_exact_bytes(&bytes).map_err(ChatError::generic)?;
|
||||
|
||||
let protocol_message: ProtocolMessage = mls_message
|
||||
.try_into_protocol_message()
|
||||
.map_err(ChatError::generic)?;
|
||||
|
||||
if protocol_message.epoch() < self.mls_group.epoch() {
|
||||
// TODO: (P1) Add logging for messages arriving from past epoch.
|
||||
return Ok(ConvoOutcome::empty(self.id().to_string()));
|
||||
}
|
||||
|
||||
let processed = self
|
||||
.mls_group
|
||||
.process_message(&cx.mls_provider, protocol_message)
|
||||
.map_err(ChatError::generic)?;
|
||||
|
||||
let cred_bytes = processed.credential().serialized_content().to_vec();
|
||||
|
||||
let content = match processed.into_content() {
|
||||
ProcessedMessageContent::ApplicationMessage(msg) => {
|
||||
let reliable = ReliablePayload::decode(msg.into_bytes().as_slice())?;
|
||||
cx.causal.on_receive(&self.convo_id, &reliable);
|
||||
Some(Content {
|
||||
bytes: reliable.content.to_vec(),
|
||||
encoded_credential: cred_bytes,
|
||||
})
|
||||
}
|
||||
ProcessedMessageContent::StagedCommitMessage(commit) => {
|
||||
self.mls_group
|
||||
.merge_staged_commit(&cx.mls_provider, *commit)
|
||||
.map_err(ChatError::generic)?;
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
// TODO: (P2) Log unknown message type
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(ConvoOutcome {
|
||||
convo_id: self.id().to_string(),
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
fn wakeup(&mut self, _: &mut ServiceContext<S>) -> Result<(), ChatError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ExternalServices> GroupConvo<S> for GroupCentInfoConvo {
|
||||
// add_members returns:
|
||||
// commit — the Commit message Alice broadcasts to all members
|
||||
// welcome — the Welcome message sent privately to each new joiner
|
||||
// _group_info — used for external joins; ignore for now
|
||||
fn add_member(
|
||||
&mut self,
|
||||
cx: &mut ServiceContext<S>,
|
||||
members: &[IdentIdRef],
|
||||
) -> Result<(), ChatError> {
|
||||
if members.len() > 50 {
|
||||
// This is a temporary limit that originates from the the De-MLS epoch time.
|
||||
return Err(ChatError::Protocol(
|
||||
"Cannot add more than 50 Members at a time".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Resolve each account to a KeyPackage per authorized device and flatten
|
||||
// them into one list — every device of every invitee becomes an MLS
|
||||
// leaf, so all of a user's installations join the group.
|
||||
let mut keypkgs = Vec::with_capacity(members.len());
|
||||
for ident in members {
|
||||
keypkgs.extend(self.key_packages_for_account(ident, &cx.mls_provider, &cx.registry)?);
|
||||
}
|
||||
|
||||
let (commit, welcome, _group_info) = self
|
||||
.mls_group
|
||||
.add_members(
|
||||
&cx.mls_provider,
|
||||
&cx.mls_identity,
|
||||
keypkgs.iter().as_slice(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.mls_group
|
||||
.merge_pending_commit(&cx.mls_provider)
|
||||
.unwrap();
|
||||
|
||||
for ident in members {
|
||||
crate::inbox_v2::invite_user_group_cent_info(&mut cx.ds, ident, &welcome)?;
|
||||
}
|
||||
|
||||
self.send_payload(cx, commit.to_bytes()?)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ConvoMetadata {
|
||||
let res = self.mls_group.extensions().iter().find_map(|ext| {
|
||||
if let Extension::Unknown(ext_type, UnknownExtension(bytes)) = ext {
|
||||
if *ext_type == GROUP_METADATA_EXTENSION_TYPE {
|
||||
return ConvoMetaInfo::from_extension_bytes(bytes).ok();
|
||||
};
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
res.map(Into::into).unwrap_or_else(ConvoMetadata::empty)
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ use crate::conversation::ConversationIdRef;
|
||||
use crate::inbox_v2::MlsProvider;
|
||||
use crate::service_context::{ExternalServices, ServiceContext};
|
||||
|
||||
use crate::types::ConvoMetadata;
|
||||
use crate::utils::{blake2b_hex, hash_size};
|
||||
use crate::{
|
||||
DeliveryService, IdentityProvider,
|
||||
@ -353,4 +354,8 @@ impl<S: ExternalServices> GroupConvo<S> for GroupV1Convo {
|
||||
|
||||
self.send_payload(cx, commit.to_bytes()?)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ConvoMetadata {
|
||||
ConvoMetadata::empty()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// DeMLS and Libchat have different execution models, trait definitions and ownership/lifetimes of objects.
|
||||
// The easies path is to do a Spike to see what it would take, gather the friction points and then iterate.
|
||||
|
||||
use crate::types::AddressedEncryptedPayload;
|
||||
use crate::types::{AddressedEncryptedPayload, ConvoMetadata};
|
||||
use crate::{Content, WakeupService};
|
||||
use alloy::signers::local::PrivateKeySigner;
|
||||
use blake2::{Blake2b, Digest, digest::consts::U6};
|
||||
@ -312,6 +312,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metadata(&self) -> ConvoMetadata {
|
||||
ConvoMetadata::empty()
|
||||
}
|
||||
|
||||
// fn conversation_state(&self) -> Result<ConversationState, ChatError> {
|
||||
// Ok(self
|
||||
// .conversation
|
||||
|
||||
104
core/conversations/src/conversation/mls_extensions.rs
Normal file
104
core/conversations/src/conversation/mls_extensions.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use openmls::{
|
||||
extensions::ExtensionType,
|
||||
prelude::{
|
||||
Capabilities,
|
||||
tls_codec::{Deserialize, Error as TlsError, Serialize, Size, VLByteSlice, VLBytes},
|
||||
},
|
||||
};
|
||||
use shared_traits::{IdentId, IdentIdRef};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::types::ConvoMetadata;
|
||||
|
||||
pub const GROUP_METADATA_EXTENSION_TYPE: u16 = 0xFF01;
|
||||
|
||||
pub fn capabilities_with_group_metadata() -> Capabilities {
|
||||
Capabilities::new(
|
||||
None, // default protocol versions
|
||||
None, // default ciphersuites
|
||||
Some(&[ExtensionType::Unknown(GROUP_METADATA_EXTENSION_TYPE)]),
|
||||
None, // default proposal types
|
||||
None, // default credential types
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConvoMetaInfo {
|
||||
version: u16,
|
||||
name: String,
|
||||
owner: IdentId,
|
||||
desc: String,
|
||||
}
|
||||
|
||||
impl ConvoMetaInfo {
|
||||
pub fn new(name: impl Into<String>, owner: IdentIdRef, desc: impl Into<String>) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
name: name.into(),
|
||||
owner: owner.clone(),
|
||||
desc: desc.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_extension_bytes(&self) -> Vec<u8> {
|
||||
// TLS presentation-language encoding — matches the wire format used by
|
||||
// the rest of the MLS stack, so no extra serializer is pulled in.
|
||||
self.tls_serialize_detached().expect("serialization failed")
|
||||
}
|
||||
|
||||
pub fn from_extension_bytes(bytes: &[u8]) -> Result<Self, TlsError> {
|
||||
Self::tls_deserialize(&mut &bytes[..])
|
||||
}
|
||||
}
|
||||
|
||||
// Each field is encoded as a variable-length opaque (`opaque <V>`); `IdentId`
|
||||
// and `String` aren't `tls_codec` types, so we encode/decode their UTF-8 bytes.
|
||||
impl Size for ConvoMetaInfo {
|
||||
fn tls_serialized_len(&self) -> usize {
|
||||
self.version.tls_serialized_len()
|
||||
+ VLByteSlice(self.name.as_bytes()).tls_serialized_len()
|
||||
+ VLByteSlice(self.owner.as_str().as_bytes()).tls_serialized_len()
|
||||
+ VLByteSlice(self.desc.as_bytes()).tls_serialized_len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ConvoMetaInfo {
|
||||
fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, TlsError> {
|
||||
let mut written = self.version.tls_serialize(writer)?;
|
||||
written += VLByteSlice(self.name.as_bytes()).tls_serialize(writer)?;
|
||||
written += VLByteSlice(self.owner.as_str().as_bytes()).tls_serialize(writer)?;
|
||||
written += VLByteSlice(self.desc.as_bytes()).tls_serialize(writer)?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for ConvoMetaInfo {
|
||||
fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, TlsError> {
|
||||
let version = u16::tls_deserialize(bytes)?;
|
||||
let name = vl_string(bytes)?;
|
||||
let owner = IdentId::new(vl_string(bytes)?);
|
||||
let desc = vl_string(bytes)?;
|
||||
Ok(Self {
|
||||
version,
|
||||
name,
|
||||
owner,
|
||||
desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn vl_string<R: Read>(bytes: &mut R) -> Result<String, TlsError> {
|
||||
let raw = VLBytes::tls_deserialize(bytes)?;
|
||||
String::from_utf8(raw.into())
|
||||
.map_err(|_| TlsError::DecodingError("invalid utf-8 in ConvoMetaInfo".into()))
|
||||
}
|
||||
|
||||
impl From<ConvoMetaInfo> for ConvoMetadata {
|
||||
fn from(value: ConvoMetaInfo) -> Self {
|
||||
Self {
|
||||
owner: value.owner.to_string(),
|
||||
name: value.name,
|
||||
desc: value.desc,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
use crate::causal_history::{CausalHistoryStore, MissingMessage};
|
||||
use crate::conversation::GroupCentInfoConvo;
|
||||
use crate::conversation::{
|
||||
ConversationIdRef, DirectV1Convo, GroupV1Convo, GroupV2Convo, Identified, PrivateV1Convo,
|
||||
};
|
||||
use crate::service_context::{ExternalServices, ServiceContext};
|
||||
use crate::types::ConvoMetadata;
|
||||
use crate::{DeliveryService, IdentityProvider, RegistrationService, WakeupService};
|
||||
use crate::{
|
||||
conversation::{Convo, GroupConvo},
|
||||
@ -277,6 +279,24 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
||||
Ok(convo_id)
|
||||
}
|
||||
|
||||
pub fn create_group_cent_info(
|
||||
&mut self,
|
||||
participants: &[IdentIdRef],
|
||||
name: &str,
|
||||
desc: &str,
|
||||
) -> Result<ConversationId, ChatError> {
|
||||
// TODO: (P1) Ensure errors are handled properly. This is a high chance for
|
||||
// desynchronized state: MlsGroup persistence, conversation persistence, and
|
||||
// invite delivery all happen separately.
|
||||
let mut convo = GroupCentInfoConvo::new(&mut self.services, name, desc)?;
|
||||
convo.add_member(&mut self.services, participants)?;
|
||||
let convo_id = convo.id().to_string();
|
||||
|
||||
self.register_convo(ConvoTypeOwned::Group(Box::new(convo)))?;
|
||||
|
||||
Ok(convo_id)
|
||||
}
|
||||
|
||||
/// Add members to an existing group conversation.
|
||||
pub fn group_add_member(
|
||||
&mut self,
|
||||
@ -496,6 +516,13 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
|
||||
.load_conversation(convo_id)?
|
||||
.ok_or_else(|| ChatError::NoConvo(convo_id.into()))
|
||||
}
|
||||
|
||||
pub fn convo_metadata(&self, convo_id: ConversationIdRef) -> ConvoMetadata {
|
||||
match self.cached_convos.get(convo_id) {
|
||||
Some(ConvoTypeOwned::Group(group_convo)) => group_convo.metadata(),
|
||||
_ => ConvoMetadata::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConvoTypeOwned<S: ExternalServices> {
|
||||
|
||||
@ -17,10 +17,12 @@ pub(crate) use mls_provider::MlsEphemeralPqProvider;
|
||||
use crate::ChatError;
|
||||
use crate::DeliveryService;
|
||||
use crate::RegistrationService;
|
||||
use crate::conversation::GroupCentInfoConvo;
|
||||
use crate::conversation::GroupConvo;
|
||||
use crate::conversation::GroupV1Convo;
|
||||
use crate::conversation::GroupV2Convo;
|
||||
use crate::conversation::Identified as _;
|
||||
use crate::conversation::mls_extensions::GROUP_METADATA_EXTENSION_TYPE;
|
||||
use crate::service_context::{ExternalServices, ServiceContext};
|
||||
use crate::utils::{blake2b_hex, hash_size};
|
||||
use crate::{
|
||||
@ -75,6 +77,30 @@ pub fn invite_user_v2<DS: DeliveryService>(
|
||||
.map_err(ChatError::generic)
|
||||
}
|
||||
|
||||
pub fn invite_user_group_cent_info<DS: DeliveryService>(
|
||||
ds: &mut DS,
|
||||
ident_id: IdentIdRef,
|
||||
welcome: &MlsMessageOut,
|
||||
) -> Result<(), ChatError> {
|
||||
let frame = InboxV2Frame {
|
||||
payload: Some(InviteType::GroupV3(welcome.to_bytes()?)),
|
||||
};
|
||||
|
||||
let envelope = EnvelopeV1 {
|
||||
conversation_hint: conversation_id_for(ident_id),
|
||||
salt: 0,
|
||||
payload: frame.encode_to_vec().into(),
|
||||
};
|
||||
|
||||
let outbound_msg = AddressedEnvelope {
|
||||
delivery_address: delivery_address_for(ident_id),
|
||||
data: envelope.encode_to_vec(),
|
||||
};
|
||||
|
||||
ds.publish(outbound_msg).map_err(ChatError::generic)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An PQ focused Conversation initializer.
|
||||
/// InboxV2 Incorporates an Account based identity system to support PQ based conversation protocols
|
||||
/// such as MLS.
|
||||
@ -144,6 +170,20 @@ impl InboxV2 {
|
||||
let convo = GroupV2Convo::new_from_welcome(service_ctx, &mw)?;
|
||||
Ok(Some(Box::new(convo)))
|
||||
}
|
||||
InviteType::GroupV3(welcome_bytes) => {
|
||||
let (msg_in, _remaining) =
|
||||
MlsMessageIn::tls_deserialize_bytes(welcome_bytes.as_slice())?;
|
||||
|
||||
let MlsMessageBodyIn::Welcome(welcome) = msg_in.extract() else {
|
||||
return Err(ChatError::ProtocolExpectation(
|
||||
"something else",
|
||||
"Welcome".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let convo = GroupCentInfoConvo::new_from_welcome(service_ctx, welcome)?;
|
||||
Ok(Some(Box::new(convo)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +229,10 @@ impl InboxV2 {
|
||||
) -> Result<KeyPackage, ChatError> {
|
||||
let capabilities = Capabilities::builder()
|
||||
.ciphersuites(vec![CIPHER_SUITE])
|
||||
.extensions(vec![ExtensionType::ApplicationId])
|
||||
.extensions(vec![
|
||||
ExtensionType::ApplicationId,
|
||||
ExtensionType::Unknown(GROUP_METADATA_EXTENSION_TYPE),
|
||||
])
|
||||
.build();
|
||||
let a = KeyPackage::builder()
|
||||
.leaf_node_capabilities(capabilities)
|
||||
@ -279,7 +322,7 @@ impl InboxV2 {
|
||||
|
||||
#[derive(Clone, PartialEq, Message)]
|
||||
pub struct InboxV2Frame {
|
||||
#[prost(oneof = "InviteType", tags = "1, 2")]
|
||||
#[prost(oneof = "InviteType", tags = "1, 2, 3")]
|
||||
pub payload: Option<InviteType>,
|
||||
}
|
||||
|
||||
@ -289,6 +332,8 @@ pub enum InviteType {
|
||||
GroupV1(GroupV1HeavyInvite),
|
||||
#[prost(bytes, tag = "2")]
|
||||
GroupV2(Vec<u8>),
|
||||
#[prost(bytes, tag = "3")]
|
||||
GroupV3(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Message)]
|
||||
|
||||
@ -66,3 +66,20 @@ impl AddressedEncryptedPayload {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConvoMetadata {
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
|
||||
impl ConvoMetadata {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
owner: String::new(),
|
||||
name: String::new(),
|
||||
desc: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
core/integration_tests_core/tests/test_group_cent_info.rs
Normal file
66
core/integration_tests_core/tests/test_group_cent_info.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use integration_tests_core::TestHarness;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn dev() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.with_test_writer()
|
||||
.try_init();
|
||||
|
||||
let mut harness = TestHarness::<3>::new(|_, _| {});
|
||||
|
||||
let raya_id = harness.raya().ident_id().clone();
|
||||
let pax_id = harness.pax().ident_id().clone();
|
||||
|
||||
const M_R1: &[u8; 12] = b"Hi From Raya";
|
||||
const M_P1: &[u8; 13] = b"Hey it's Pax!";
|
||||
|
||||
// Step: Saro Create Convo with Raya
|
||||
|
||||
let convo_id = harness
|
||||
.saro()
|
||||
.create_group_cent_info(
|
||||
&[&raya_id],
|
||||
"Saro<>Raya",
|
||||
"this is a DM Between Raya and Saro",
|
||||
)
|
||||
.expect("Saro invite Raya ");
|
||||
harness.process_until(|h| h.raya().list_conversations().unwrap().len() == 1);
|
||||
|
||||
// Step: Raya Send Content
|
||||
|
||||
println!("{:?}", harness.raya().convo_metadata(&convo_id));
|
||||
|
||||
harness
|
||||
.raya()
|
||||
.send_content(&convo_id, M_R1)
|
||||
.expect("Raya send Msg");
|
||||
|
||||
harness.process_until(|h| h.saro().received_messages().len() == 1);
|
||||
|
||||
// Step: Saro add Pax
|
||||
|
||||
harness
|
||||
.saro()
|
||||
.group_add_member(&convo_id, &[&pax_id])
|
||||
.expect("Saro invite pax");
|
||||
harness.process_until(|h| h.pax().list_conversations().unwrap().len() == 1);
|
||||
|
||||
// Step: Pax send Content
|
||||
|
||||
harness
|
||||
.pax()
|
||||
.send_content(&convo_id, M_P1)
|
||||
.expect("Pax send");
|
||||
harness.process(Duration::from_millis(500));
|
||||
|
||||
assert!(harness.saro().check(&convo_id, M_R1));
|
||||
assert!(harness.saro().check(&convo_id, M_P1));
|
||||
|
||||
assert!(!harness.raya().check(&convo_id, M_R1));
|
||||
assert!(harness.raya().check(&convo_id, M_P1));
|
||||
|
||||
assert!(!harness.pax().check(&convo_id, M_R1));
|
||||
assert!(!harness.pax().check(&convo_id, M_P1));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user