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