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