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