1pub(crate) mod commit;
15mod commit_delay;
16pub(crate) mod config;
17pub(crate) mod conversation_guard;
18mod credential;
19mod duplicate;
20#[cfg(test)]
21mod durability;
22mod error;
23pub(crate) mod group_info;
24mod id;
25mod immutable_conversation;
26pub(crate) mod merge;
27mod orphan_welcome;
28mod own_commit;
29pub(crate) mod pending_conversation;
30mod persistence;
31pub(crate) mod proposal;
32mod renew;
33pub(crate) mod welcome;
34mod wipe;
35
36use std::{
37 borrow::Borrow,
38 collections::{HashMap, HashSet},
39 ops::Deref,
40 sync::Arc,
41};
42
43use itertools::Itertools as _;
44use log::trace;
45use mls_crypto_provider::MlsCryptoProvider;
46use openmls::{
47 group::MlsGroup,
48 prelude::{LeafNodeIndex, Proposal},
49};
50use openmls_traits::{OpenMlsCryptoProvider, types::SignatureScheme};
51
52use self::config::MlsConversationConfiguration;
53pub use self::{
54 conversation_guard::ConversationGuard,
55 error::{Error, Result},
56 id::{ConversationId, ConversationIdRef},
57 immutable_conversation::ImmutableConversation,
58};
59use super::credential::Credential;
60use crate::{
61 Ciphersuite, ClientId, ClientIdRef, CredentialType, E2eiConversationState, LeafError, MlsError, RecursiveError,
62 UserId, WireIdentity,
63 mls::{HasSessionAndCrypto, Session, credential::ext::CredentialExt as _},
64};
65
66#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
69#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
70pub(crate) trait ConversationWithMls<'a> {
71 type Context: HasSessionAndCrypto;
74
75 type Conversation: Deref<Target = MlsConversation> + Send;
76
77 async fn context(&self) -> Result<Self::Context>;
78
79 async fn conversation(&'a self) -> Self::Conversation;
80
81 async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
82 self.context()
83 .await?
84 .crypto_provider()
85 .await
86 .map_err(RecursiveError::mls("getting mls provider"))
87 .map_err(Into::into)
88 }
89
90 async fn session(&self) -> Result<Session> {
91 self.context()
92 .await?
93 .session()
94 .await
95 .map_err(RecursiveError::mls("getting mls client"))
96 .map_err(Into::into)
97 }
98}
99
100#[expect(private_bounds)]
105#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
106#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
107pub trait Conversation<'a>: ConversationWithMls<'a> {
108 async fn epoch(&'a self) -> u64 {
110 self.conversation().await.group().epoch().as_u64()
111 }
112
113 async fn ciphersuite(&'a self) -> Ciphersuite {
115 self.conversation().await.ciphersuite()
116 }
117
118 async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
127 const EXPORTER_LABEL: &str = "exporter";
128 const EXPORTER_CONTEXT: &[u8] = &[];
129 let backend = self.crypto_provider().await?;
130 let inner = self.conversation().await;
131 inner
132 .group()
133 .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
134 .map_err(MlsError::wrap("exporting secret key"))
135 .map_err(Into::into)
136 }
137
138 async fn get_client_ids(&'a self) -> Vec<ClientId> {
143 let inner = self.conversation().await;
144 inner
145 .group()
146 .members()
147 .map(|kp| ClientId::from(kp.credential.identity().to_owned()))
148 .collect()
149 }
150
151 async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
154 let inner = self.conversation().await;
155 let ext_senders = inner
156 .group()
157 .group_context_extensions()
158 .external_senders()
159 .ok_or(Error::MissingExternalSenderExtension)?;
160 let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
161 let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
162 Ok(ext_sender_public_key)
163 }
164
165 async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
168 let backend = self.crypto_provider().await?;
169 let authentication_service = backend.authentication_service();
170 authentication_service.refresh_time_of_interest().await;
171 let inner = self.conversation().await;
172 let state = Session::compute_conversation_state(
173 inner.ciphersuite(),
174 inner.group.members_credentials(),
175 CredentialType::X509,
176 authentication_service.borrow().await.as_ref(),
177 )
178 .await;
179 Ok(state)
180 }
181
182 async fn get_device_identities(
186 &'a self,
187 device_ids: &[impl Borrow<ClientIdRef> + Sync],
188 ) -> Result<Vec<WireIdentity>> {
189 if device_ids.is_empty() {
190 return Err(Error::CallerError(
191 "This function accepts a list of IDs as a parameter, but that list was empty.",
192 ));
193 }
194 let mls_provider = self.crypto_provider().await?;
195 let auth_service = mls_provider.authentication_service();
196 auth_service.refresh_time_of_interest().await;
197 let auth_service = auth_service.borrow().await;
198 let env = auth_service.as_ref();
199 let conversation = self.conversation().await;
200 conversation
201 .members_with_key()
202 .into_iter()
203 .filter(|(id, _)| device_ids.iter().any(|client_id| client_id.borrow() == id))
204 .map(|(_, c)| {
205 c.extract_identity(conversation.ciphersuite(), env)
206 .map_err(RecursiveError::mls_credential("extracting identity"))
207 })
208 .collect::<Result<Vec<_>, _>>()
209 .map_err(Into::into)
210 }
211
212 async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
219 if user_ids.is_empty() {
220 return Err(Error::CallerError(
221 "This function accepts a list of IDs as a parameter, but that list was empty.",
222 ));
223 }
224 let mls_provider = self.crypto_provider().await?;
225 let auth_service = mls_provider.authentication_service();
226 auth_service.refresh_time_of_interest().await;
227 let auth_service = auth_service.borrow().await;
228 let env = auth_service.as_ref();
229 let conversation = self.conversation().await;
230 let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
231
232 conversation
233 .members_with_key()
234 .iter()
235 .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
236 .filter(|(uid, _)| user_ids.contains(uid))
237 .map(|(uid, c)| {
238 let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
239 let identity = c
240 .extract_identity(conversation.ciphersuite(), env)
241 .map_err(RecursiveError::mls_credential("extracting identity"))?;
242 Ok((uid, identity))
243 })
244 .process_results(|iter| iter.into_group_map())
245 }
246
247 async fn generate_history_secret(&'a self) -> Result<crate::HistorySecret> {
253 let ciphersuite = self.ciphersuite().await;
254 crate::ephemeral::generate_history_secret(ciphersuite)
255 .await
256 .map_err(RecursiveError::root("generating history secret"))
257 .map_err(Into::into)
258 }
259
260 async fn is_history_sharing_enabled(&'a self) -> bool {
263 self.get_client_ids()
264 .await
265 .iter()
266 .any(|client_id| client_id.starts_with(crate::ephemeral::HISTORY_CLIENT_ID_PREFIX.as_bytes()))
267 }
268}
269
270impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
271
272#[derive(Debug)]
278#[allow(dead_code)]
279pub struct MlsConversation {
280 pub(crate) id: ConversationId,
281 pub(crate) parent_id: Option<ConversationId>,
282 pub(crate) group: MlsGroup,
283 configuration: MlsConversationConfiguration,
284}
285
286impl MlsConversation {
287 pub async fn create(
299 id: ConversationId,
300 author_client: &Session,
301 creator_credential_type: CredentialType,
302 configuration: MlsConversationConfiguration,
303 backend: &MlsCryptoProvider,
304 ) -> Result<Self> {
305 let (ciphersuite, credential_type) = (configuration.ciphersuite, creator_credential_type);
306 let credential = author_client
307 .find_most_recent_or_create_basic_credential(ciphersuite, credential_type)
308 .await
309 .map_err(RecursiveError::mls_client("getting or creating credential"))?;
310
311 let group = MlsGroup::new_with_group_id(
312 backend,
313 &credential.signature_key_pair,
314 &configuration.as_openmls_default_configuration()?,
315 openmls::prelude::GroupId::from_slice(id.as_ref()),
316 credential.to_mls_credential_with_key(),
317 )
318 .await
319 .map_err(MlsError::wrap("creating group with id"))?;
320
321 let mut conversation = Self {
322 id,
323 group,
324 parent_id: None,
325 configuration,
326 };
327
328 conversation
329 .persist_group_when_changed(&backend.keystore(), true)
330 .await?;
331
332 Ok(conversation)
333 }
334
335 pub(crate) async fn from_mls_group(
337 group: MlsGroup,
338 configuration: MlsConversationConfiguration,
339 backend: &MlsCryptoProvider,
340 ) -> Result<Self> {
341 let id = ConversationId::from(group.group_id().as_slice());
342
343 let mut conversation = Self {
344 id,
345 group,
346 configuration,
347 parent_id: None,
348 };
349
350 conversation
351 .persist_group_when_changed(&backend.keystore(), true)
352 .await?;
353
354 Ok(conversation)
355 }
356
357 pub fn id(&self) -> &ConversationId {
359 &self.id
360 }
361
362 pub(crate) fn group(&self) -> &MlsGroup {
363 &self.group
364 }
365
366 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
368 let pending_removals = self.pending_removals();
369 let existing_clients = self
370 .group
371 .members()
372 .filter_map(|kp| {
373 if !pending_removals.contains(&kp.index) {
374 Some(kp.credential.identity().to_owned().into())
375 } else {
376 trace!(client_index:% = kp.index; "Client is pending removal");
377 None
378 }
379 })
380 .collect::<HashSet<_>>();
381 existing_clients.into_iter().collect()
382 }
383
384 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
386 self.group
387 .pending_proposals()
388 .filter_map(|proposal| match proposal.proposal() {
389 Proposal::Remove(remove) => Some(remove.removed()),
390 _ => None,
391 })
392 .collect::<Vec<_>>()
393 }
394
395 pub(crate) fn ciphersuite(&self) -> Ciphersuite {
396 self.configuration.ciphersuite
397 }
398
399 pub(crate) fn signature_scheme(&self) -> SignatureScheme {
400 self.ciphersuite().signature_algorithm()
401 }
402
403 pub(crate) async fn find_current_credential(&self, client: &Session) -> Result<Arc<Credential>> {
404 let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
405 let sc = self.ciphersuite().signature_algorithm();
406 let ct = self
407 .own_credential_type()
408 .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
409
410 client
411 .find_credential_by_public_key(sc, ct, own_leaf.signature_key())
412 .await
413 .map_err(RecursiveError::mls_client("finding current credential"))
414 .map_err(Into::into)
415 }
416
417 pub(crate) async fn find_most_recent_credential(&self, client: &Session) -> Result<Arc<Credential>> {
418 let sc = self.ciphersuite().signature_algorithm();
419 let ct = self
420 .own_credential_type()
421 .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
422
423 client
424 .find_most_recent_credential(sc, ct)
425 .await
426 .map_err(RecursiveError::mls_client("finding most recent credential"))
427 .map_err(Into::into)
428 }
429}
430
431#[cfg(test)]
432pub mod test_utils {
433 use openmls::prelude::SignaturePublicKey;
434
435 use super::*;
436
437 impl MlsConversation {
438 pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
439 self.group
440 .members()
441 .map(|m| m.signature_key)
442 .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
443 }
444
445 pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
446 self.group.members().map(|m| m.encryption_key)
447 }
448
449 pub fn extensions(&self) -> &openmls::prelude::Extensions {
450 self.group.export_group_context().extensions()
451 }
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::test_utils::*;
459
460 #[apply(all_cred_cipher)]
461 pub async fn create_self_conversation_should_succeed(case: TestContext) {
462 let [alice] = case.sessions().await;
463 Box::pin(async move {
464 let conversation = case.create_conversation([&alice]).await;
465 assert_eq!(1, conversation.member_count().await);
466 let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
467 assert!(alice_can_send_message.is_ok());
468 })
469 .await;
470 }
471
472 #[apply(all_cred_cipher)]
473 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
474 let [alice, bob] = case.sessions().await;
475 Box::pin(async move {
476 let conversation = case.create_conversation([&alice, &bob]).await;
477 assert_eq!(2, conversation.member_count().await);
478 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
479 })
480 .await;
481 }
482
483 #[apply(all_cred_cipher)]
484 pub async fn create_many_people_conversation(case: TestContext) {
485 const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
486 let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
487 Box::pin(async move {
488 let alice = &alice_and_friends[0];
489 let conversation = case.create_conversation([alice]).await;
490
491 let bob_and_friends = &alice_and_friends[1..];
492 let conversation = conversation.invite_notify(bob_and_friends).await;
493
494 assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
495 assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
496 })
497 .await;
498 }
499
500 mod wire_identity_getters {
501 use super::Error;
502 use crate::{
503 ClientId, CredentialType, DeviceStatus, E2eiConversationState, mls::conversation::Conversation,
504 test_utils::*,
505 };
506
507 async fn all_identities_check<'a, C, const N: usize>(
508 conversation: &'a C,
509 user_ids: &[String; N],
510 expected_sizes: [usize; N],
511 ) where
512 C: Conversation<'a> + Sync,
513 {
514 let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
515 assert_eq!(all_identities.len(), N);
516 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
517 let alice_identities = all_identities.get(user_id).unwrap();
518 assert_eq!(alice_identities.len(), expected_size);
519 }
520 let not_found = conversation
522 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
523 .await
524 .unwrap();
525 assert!(not_found.is_empty());
526
527 let invalid = conversation.get_user_identities(&[]).await;
529 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
530 }
531
532 async fn check_identities_device_status<'a, C, const N: usize>(
533 conversation: &'a C,
534 client_ids: &[ClientId; N],
535 name_status: &[(impl ToString, DeviceStatus); N],
536 ) where
537 C: Conversation<'a> + Sync,
538 {
539 let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
540
541 for (user_name, status) in name_status.iter() {
542 let client_identity = identities.remove(
543 identities
544 .iter()
545 .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
546 .unwrap(),
547 );
548 assert_eq!(client_identity.status, *status);
549 }
550 assert!(identities.is_empty());
551
552 assert_eq!(
553 conversation.e2ei_conversation_state().await.unwrap(),
554 E2eiConversationState::NotVerified
555 );
556 }
557
558 #[macro_rules_attribute::apply(smol_macros::test)]
559 async fn should_read_device_identities() {
560 let case = TestContext::default_x509();
561
562 let [alice_android, alice_ios] = case.sessions().await;
563 Box::pin(async move {
564 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
565
566 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
567
568 let mut android_ids = conversation
569 .guard()
570 .await
571 .get_device_identities(&[android_id.clone(), ios_id.clone()])
572 .await
573 .unwrap();
574 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
575 assert_eq!(android_ids.len(), 2);
576 let mut ios_ids = conversation
577 .guard_of(&alice_ios)
578 .await
579 .get_device_identities(&[android_id.clone(), ios_id.clone()])
580 .await
581 .unwrap();
582 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
583 assert_eq!(ios_ids.len(), 2);
584
585 assert_eq!(android_ids, ios_ids);
586
587 let android_identities = conversation
588 .guard()
589 .await
590 .get_device_identities(&[android_id])
591 .await
592 .unwrap();
593 let android_id = android_identities.first().unwrap();
594 assert_eq!(
595 android_id.client_id.as_bytes(),
596 alice_android.transaction.client_id().await.unwrap().0.as_slice()
597 );
598
599 let ios_identities = conversation
600 .guard()
601 .await
602 .get_device_identities(&[ios_id])
603 .await
604 .unwrap();
605 let ios_id = ios_identities.first().unwrap();
606 assert_eq!(
607 ios_id.client_id.as_bytes(),
608 alice_ios.transaction.client_id().await.unwrap().0.as_slice()
609 );
610
611 let empty_slice: &[ClientId] = &[];
612 let invalid = conversation.guard().await.get_device_identities(empty_slice).await;
613 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
614 })
615 .await
616 }
617
618 #[macro_rules_attribute::apply(smol_macros::test)]
619 async fn should_read_revoked_device_cross_signed() {
620 let case = TestContext::default_x509();
621 let alice_user_id = uuid::Uuid::new_v4();
622 let bob_user_id = uuid::Uuid::new_v4();
623 let rupert_user_id = uuid::Uuid::new_v4();
624 let john_user_id = uuid::Uuid::new_v4();
625 let dilbert_user_id = uuid::Uuid::new_v4();
626
627 let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
628 let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
629 let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
630 let [john_client_id] = case.x509_client_ids_for_user(&john_user_id);
631 let [dilbert_client_id] = case.x509_client_ids_for_user(&dilbert_user_id);
632
633 let sessions = case
634 .sessions_x509_cross_signed_with_client_ids_and_revocation(
635 [alice_client_id, bob_client_id, rupert_client_id],
636 [john_client_id, dilbert_client_id],
637 &[dilbert_user_id.to_string(), rupert_user_id.to_string()],
638 )
639 .await;
640
641 Box::pin(async move {
642 let ([alice, bob, rupert], [john, dilbert]) = &sessions;
643 let mut sessions = sessions.0.iter().chain(sessions.1.iter());
644 let conversation = case.create_conversation(&mut sessions).await;
645
646 let (alice_id, bob_id, rupert_id, john_id, dilbert_id) = (
647 alice.get_client_id().await,
648 bob.get_client_id().await,
649 rupert.get_client_id().await,
650 john.get_client_id().await,
651 dilbert.get_client_id().await,
652 );
653
654 let client_ids = [alice_id, bob_id, rupert_id, john_id, dilbert_id];
655 let name_status = [
656 (alice_user_id, DeviceStatus::Valid),
657 (bob_user_id, DeviceStatus::Valid),
658 (rupert_user_id, DeviceStatus::Revoked),
659 (john_user_id, DeviceStatus::Valid),
660 (dilbert_user_id, DeviceStatus::Revoked),
661 ];
662 for _ in 0..2 {
664 for session in sessions.clone() {
665 let conversation = conversation.guard_of(session).await;
666 check_identities_device_status(&conversation, &client_ids, &name_status).await;
667 }
668 }
669 })
670 .await
671 }
672
673 #[macro_rules_attribute::apply(smol_macros::test)]
674 async fn should_read_revoked_device() {
675 let case = TestContext::default_x509();
676 let rupert_user_id = uuid::Uuid::new_v4();
677 let bob_user_id = uuid::Uuid::new_v4();
678 let alice_user_id = uuid::Uuid::new_v4();
679
680 let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
681 let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
682 let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
683
684 let sessions = case
685 .sessions_x509_with_client_ids_and_revocation(
686 [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
687 &[rupert_user_id.to_string()],
688 )
689 .await;
690
691 Box::pin(async move {
692 let [alice, bob, rupert] = &sessions;
693 let conversation = case.create_conversation(&sessions).await;
694
695 let (alice_id, bob_id, rupert_id) = (
696 alice.get_client_id().await,
697 bob.get_client_id().await,
698 rupert.get_client_id().await,
699 );
700
701 let client_ids = [alice_id, bob_id, rupert_id];
702 let name_status = [
703 (alice_user_id, DeviceStatus::Valid),
704 (bob_user_id, DeviceStatus::Valid),
705 (rupert_user_id, DeviceStatus::Revoked),
706 ];
707
708 for _ in 0..2 {
710 for session in sessions.iter() {
711 let conversation = conversation.guard_of(session).await;
712 check_identities_device_status(&conversation, &client_ids, &name_status).await;
713 }
714 }
715 })
716 .await
717 }
718
719 #[macro_rules_attribute::apply(smol_macros::test)]
720 async fn should_not_fail_when_basic() {
721 let case = TestContext::default();
722
723 let [alice_android, alice_ios] = case.sessions().await;
724 Box::pin(async move {
725 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
726
727 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
728
729 let mut android_ids = conversation
730 .guard()
731 .await
732 .get_device_identities(&[android_id.clone(), ios_id.clone()])
733 .await
734 .unwrap();
735 android_ids.sort();
736
737 let mut ios_ids = conversation
738 .guard_of(&alice_ios)
739 .await
740 .get_device_identities(&[android_id, ios_id])
741 .await
742 .unwrap();
743 ios_ids.sort();
744
745 assert_eq!(ios_ids.len(), 2);
746 assert_eq!(ios_ids, android_ids);
747
748 assert!(ios_ids.iter().all(|i| {
749 matches!(i.credential_type, CredentialType::Basic)
750 && matches!(i.status, DeviceStatus::Valid)
751 && i.x509_identity.is_none()
752 && !i.thumbprint.is_empty()
753 && !i.client_id.is_empty()
754 }));
755 })
756 .await
757 }
758
759 #[macro_rules_attribute::apply(smol_macros::test)]
762 async fn should_read_users_cross_signed() {
763 let case = TestContext::default_x509();
764 let [alice_1_id, alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
765 let [federated_alice_1_id, federated_alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
766 let [bob_id, federated_bob_id] = case.x509_client_ids();
767
768 let ([alice_1, alice_2, bob], [federated_alice_1, federated_alice_2, federated_bob]) = case
769 .sessions_x509_cross_signed_with_client_ids(
770 [alice_1_id, alice_2_id, bob_id],
771 [federated_alice_1_id, federated_alice_2_id, federated_bob_id],
772 )
773 .await;
774 Box::pin(async move {
775 let sessions = [
776 &alice_1,
777 &alice_2,
778 &bob,
779 &federated_bob,
780 &federated_alice_1,
781 &federated_alice_2,
782 ];
783 let conversation = case.create_conversation(sessions).await;
784
785 let nb_members = conversation.member_count().await;
786 assert_eq!(nb_members, 6);
787 let conversation_guard = conversation.guard().await;
788
789 assert_eq!(alice_1.get_user_id().await, alice_2.get_user_id().await);
790
791 let alicem_user_id = federated_alice_2.get_user_id().await;
792 let bobt_user_id = federated_bob.get_user_id().await;
793
794 let alice_user_id = alice_1.get_user_id().await;
796 let alice_identities = conversation_guard
797 .get_user_identities(std::slice::from_ref(&alice_user_id))
798 .await
799 .unwrap();
800 assert_eq!(alice_identities.len(), 1);
801 let identities = alice_identities.get(&alice_user_id).unwrap();
802 assert_eq!(identities.len(), 2);
803
804 let bob_user_id = bob.get_user_id().await;
806 let bob_identities = conversation_guard
807 .get_user_identities(std::slice::from_ref(&bob_user_id))
808 .await
809 .unwrap();
810 assert_eq!(bob_identities.len(), 1);
811 let identities = bob_identities.get(&bob_user_id).unwrap();
812 assert_eq!(identities.len(), 1);
813
814 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
816 let expected_sizes = [2, 1, 2, 1];
817
818 for session in sessions {
819 all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
820 }
821 })
822 .await
823 }
824
825 #[macro_rules_attribute::apply(smol_macros::test)]
826 async fn should_read_users() {
827 let case = TestContext::default_x509();
828 let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
829 let [bob_android] = case.x509_client_ids();
830
831 let sessions = case
832 .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
833 .await;
834
835 Box::pin(async move {
836 let conversation = case.create_conversation(&sessions).await;
837
838 let nb_members = conversation.member_count().await;
839 assert_eq!(nb_members, 3);
840
841 let [alice_android, alice_ios, bob_android] = &sessions;
842 assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
843
844 let alice_user_id = alice_android.get_user_id().await;
846 let alice_identities = conversation
847 .guard()
848 .await
849 .get_user_identities(std::slice::from_ref(&alice_user_id))
850 .await
851 .unwrap();
852 assert_eq!(alice_identities.len(), 1);
853 let identities = alice_identities.get(&alice_user_id).unwrap();
854 assert_eq!(identities.len(), 2);
855
856 let bob_user_id = bob_android.get_user_id().await;
858 let bob_identities = conversation
859 .guard()
860 .await
861 .get_user_identities(std::slice::from_ref(&bob_user_id))
862 .await
863 .unwrap();
864 assert_eq!(bob_identities.len(), 1);
865 let identities = bob_identities.get(&bob_user_id).unwrap();
866 assert_eq!(identities.len(), 1);
867
868 let user_ids = [alice_user_id, bob_user_id];
869 let expected_sizes = [2, 1];
870
871 for session in &sessions {
872 all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
873 }
874 })
875 .await
876 }
877
878 #[macro_rules_attribute::apply(smol_macros::test)]
879 async fn should_exchange_messages_cross_signed() {
880 let case = TestContext::default_x509();
881 let sessions = case.sessions_x509_cross_signed::<3, 3>().await;
882 Box::pin(async move {
883 let sessions = sessions.0.iter().chain(sessions.1.iter());
884 let conversation = case.create_conversation(sessions.clone()).await;
885
886 assert_eq!(conversation.member_count().await, 6);
887
888 assert!(conversation.is_functional_and_contains(sessions).await);
889 })
890 .await;
891 }
892 }
893
894 mod export_secret {
895 use openmls::prelude::ExportSecretError;
896
897 use super::*;
898 use crate::MlsErrorKind;
899
900 #[apply(all_cred_cipher)]
901 pub async fn can_export_secret_key(case: TestContext) {
902 let [alice] = case.sessions().await;
903 Box::pin(async move {
904 let conversation = case.create_conversation([&alice]).await;
905
906 let key_length = 128;
907 let result = conversation.guard().await.export_secret_key(key_length).await;
908 assert!(result.is_ok());
909 assert_eq!(result.unwrap().len(), key_length);
910 })
911 .await
912 }
913
914 #[apply(all_cred_cipher)]
915 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
916 let [alice] = case.sessions().await;
917 Box::pin(async move {
918 let conversation = case.create_conversation([&alice]).await;
919
920 let result = conversation.guard().await.export_secret_key(usize::MAX).await;
921 let error = result.unwrap_err();
922 assert!(innermost_source_matches!(
923 error,
924 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
925 ));
926 })
927 .await
928 }
929 }
930
931 mod get_client_ids {
932 use super::*;
933
934 #[apply(all_cred_cipher)]
935 pub async fn can_get_client_ids(case: TestContext) {
936 let [alice, bob] = case.sessions().await;
937 Box::pin(async move {
938 let conversation = case.create_conversation([&alice]).await;
939
940 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
941
942 let conversation = conversation.invite_notify([&bob]).await;
943
944 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
945 })
946 .await
947 }
948 }
949
950 mod external_sender {
951 use super::*;
952
953 #[apply(all_cred_cipher)]
954 pub async fn should_fetch_ext_sender(mut case: TestContext) {
955 let [alice, external_sender] = case.sessions().await;
956 Box::pin(async move {
957 let conversation = case
958 .create_conversation_with_external_sender(&external_sender, [&alice])
959 .await;
960
961 let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
962 assert!(!alice_ext_sender.is_empty());
963 assert_eq!(
964 alice_ext_sender,
965 external_sender.client_signature_key(&case).await.as_slice().to_vec()
966 );
967 })
968 .await
969 }
970 }
971}