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 core_crypto_keystore::Database;
44use itertools::Itertools as _;
45use log::trace;
46use openmls::{
47 group::{MlsGroup, QueuedProposal},
48 prelude::{LeafNode, LeafNodeIndex, Proposal, Sender},
49};
50use openmls_traits::OpenMlsCryptoProvider;
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 mls_provider::MlsCryptoProvider,
65};
66
67#[cfg_attr(target_os = "unknown", async_trait::async_trait(?Send))]
70#[cfg_attr(not(target_os = "unknown"), async_trait::async_trait)]
71pub(crate) trait ConversationWithMls<'a> {
72 type Context: HasSessionAndCrypto;
75
76 type Conversation: Deref<Target = MlsConversation> + Send;
77
78 async fn context(&self) -> Result<Self::Context>;
79
80 async fn conversation(&'a self) -> Self::Conversation;
81
82 async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
83 self.context()
84 .await?
85 .crypto_provider()
86 .await
87 .map_err(RecursiveError::mls("getting mls provider"))
88 .map_err(Into::into)
89 }
90
91 async fn session(&self) -> Result<Session<Database>> {
92 self.context()
93 .await?
94 .session()
95 .await
96 .map_err(RecursiveError::mls("getting mls client"))
97 .map_err(Into::into)
98 }
99}
100
101#[expect(private_bounds)]
106#[cfg_attr(target_os = "unknown", async_trait::async_trait(?Send))]
107#[cfg_attr(not(target_os = "unknown"), async_trait::async_trait)]
108pub trait Conversation<'a>: ConversationWithMls<'a> {
109 async fn epoch(&'a self) -> u64 {
111 self.conversation().await.group().epoch().as_u64()
112 }
113
114 async fn ciphersuite(&'a self) -> Ciphersuite {
116 self.conversation().await.ciphersuite()
117 }
118
119 async fn credential_ref(&'a self) -> Result<CredentialRef> {
121 let inner = self.conversation().await;
122 let session = self.session().await?;
123 let credential = inner
124 .find_current_credential(&session)
125 .await
126 .map_err(|_| Error::IdentityInitializationError)?;
127 Ok(CredentialRef::from_credential(&credential))
128 }
129
130 async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
139 const EXPORTER_LABEL: &str = "exporter";
140 const EXPORTER_CONTEXT: &[u8] = &[];
141 let backend = self.crypto_provider().await?;
142 let inner = self.conversation().await;
143 inner
144 .group()
145 .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
146 .map_err(MlsError::wrap("exporting secret key"))
147 .map_err(Into::into)
148 }
149
150 async fn get_client_ids(&'a self) -> Vec<ClientId> {
155 let inner = self.conversation().await;
156 inner
157 .group()
158 .members()
159 .map(|kp| ClientId::from(kp.credential.identity().to_owned()))
160 .collect()
161 }
162
163 async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
166 let inner = self.conversation().await;
167 let ext_senders = inner
168 .group()
169 .group_context_extensions()
170 .external_senders()
171 .ok_or(Error::MissingExternalSenderExtension)?;
172 let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
173 let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
174 Ok(ext_sender_public_key)
175 }
176
177 async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
180 let backend = self.crypto_provider().await?;
181 let authentication_service = backend.authentication_service();
182 authentication_service.refresh_time_of_interest().await;
183 let inner = self.conversation().await;
184 let state = Session::<Database>::compute_conversation_state(
185 inner.ciphersuite(),
186 inner.group.members_credentials(),
187 CredentialType::X509,
188 authentication_service.borrow().await.as_ref(),
189 )
190 .await;
191 Ok(state)
192 }
193
194 async fn get_device_identities(
198 &'a self,
199 device_ids: &[impl Borrow<ClientIdRef> + Sync],
200 ) -> Result<Vec<WireIdentity>> {
201 if device_ids.is_empty() {
202 return Err(Error::CallerError(
203 "This function accepts a list of IDs as a parameter, but that list was empty.",
204 ));
205 }
206 let mls_provider = self.crypto_provider().await?;
207 let auth_service = mls_provider.authentication_service();
208 auth_service.refresh_time_of_interest().await;
209 let auth_service = auth_service.borrow().await;
210 let env = auth_service.as_ref();
211 let conversation = self.conversation().await;
212 conversation
213 .members_with_key()
214 .into_iter()
215 .filter(|(id, _)| device_ids.iter().any(|client_id| client_id.borrow() == id))
216 .map(|(_, c)| {
217 c.extract_identity(conversation.ciphersuite(), env)
218 .map_err(RecursiveError::mls_credential("extracting identity"))
219 })
220 .collect::<Result<Vec<_>, _>>()
221 .map_err(Into::into)
222 }
223
224 async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
231 if user_ids.is_empty() {
232 return Err(Error::CallerError(
233 "This function accepts a list of IDs as a parameter, but that list was empty.",
234 ));
235 }
236 let mls_provider = self.crypto_provider().await?;
237 let auth_service = mls_provider.authentication_service();
238 auth_service.refresh_time_of_interest().await;
239 let auth_service = auth_service.borrow().await;
240 let env = auth_service.as_ref();
241 let conversation = self.conversation().await;
242 let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
243
244 conversation
245 .members_with_key()
246 .iter()
247 .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
248 .filter(|(uid, _)| user_ids.contains(uid))
249 .map(|(uid, c)| {
250 let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
251 let identity = c
252 .extract_identity(conversation.ciphersuite(), env)
253 .map_err(RecursiveError::mls_credential("extracting identity"))?;
254 Ok((uid, identity))
255 })
256 .process_results(|iter| iter.into_group_map())
257 }
258
259 async fn generate_history_secret(&'a self) -> Result<crate::HistorySecret> {
265 let ciphersuite = self.ciphersuite().await;
266 crate::ephemeral::generate_history_secret(ciphersuite)
267 .await
268 .map_err(RecursiveError::root("generating history secret"))
269 .map_err(Into::into)
270 }
271
272 async fn is_history_sharing_enabled(&'a self) -> bool {
275 self.get_client_ids()
276 .await
277 .iter()
278 .any(|client_id| client_id.starts_with(crate::ephemeral::HISTORY_CLIENT_ID_PREFIX.as_bytes()))
279 }
280}
281
282impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
283
284#[derive(Debug)]
290pub struct MlsConversation {
291 pub(crate) id: ConversationId,
292 pub(crate) group: MlsGroup,
293 configuration: MlsConversationConfiguration,
294}
295
296impl MlsConversation {
297 pub async fn create(
299 id: ConversationId,
300 provider: &MlsCryptoProvider,
301 database: &Database,
302 credential_ref: &CredentialRef,
303 configuration: MlsConversationConfiguration,
304 ) -> Result<Self> {
305 let credential = credential_ref
306 .load(database)
307 .await
308 .map_err(RecursiveError::mls_credential_ref("getting credential"))?;
309
310 let group = MlsGroup::new_with_group_id(
311 provider,
312 &credential.signature_key_pair,
313 &configuration.as_openmls_default_configuration()?,
314 openmls::prelude::GroupId::from_slice(id.as_ref()),
315 credential.to_mls_credential_with_key(),
316 )
317 .await
318 .map_err(MlsError::wrap("creating group with id"))?;
319
320 let mut conversation = Self {
321 id,
322 group,
323 configuration,
324 };
325
326 conversation.persist_group_when_changed(database, true).await?;
327
328 Ok(conversation)
329 }
330
331 pub(crate) async fn from_mls_group(
333 group: MlsGroup,
334 configuration: MlsConversationConfiguration,
335 database: &Database,
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 };
344
345 conversation.persist_group_when_changed(database, true).await?;
346
347 Ok(conversation)
348 }
349
350 pub fn id(&self) -> &ConversationId {
352 &self.id
353 }
354
355 pub(crate) fn group(&self) -> &MlsGroup {
356 &self.group
357 }
358
359 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
361 let pending_removals = self.pending_removals();
362 let existing_clients = self
363 .group
364 .members()
365 .filter_map(|kp| {
366 if !pending_removals.contains(&kp.index) {
367 Some(kp.credential.identity().to_owned().into())
368 } else {
369 trace!(client_index:% = kp.index; "Client is pending removal");
370 None
371 }
372 })
373 .collect::<HashSet<_>>();
374 existing_clients.into_iter().collect()
375 }
376
377 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
379 self.group
380 .pending_proposals()
381 .filter_map(|proposal| match proposal.proposal() {
382 Proposal::Remove(remove) => Some(remove.removed()),
383 _ => None,
384 })
385 .collect::<Vec<_>>()
386 }
387
388 pub(crate) fn ciphersuite(&self) -> Ciphersuite {
389 self.configuration.ciphersuite
390 }
391
392 fn extract_own_updated_node_from_proposals<'a>(
393 own_index: &LeafNodeIndex,
394 pending_proposals: impl Iterator<Item = &'a QueuedProposal>,
395 ) -> Option<&'a LeafNode> {
396 pending_proposals
397 .filter_map(|proposal| {
398 if let Sender::Member(index) = proposal.sender()
399 && index == own_index
400 && let Proposal::Update(update_proposal) = proposal.proposal()
401 {
402 return Some(update_proposal.leaf_node());
403 }
404 None
405 })
406 .last()
407 }
408
409 async fn find_credential_for_leaf_node(
410 &self,
411 session: &Session<Database>,
412 leaf_node: &LeafNode,
413 ) -> Result<Arc<Credential>> {
414 let credential = session
415 .find_credential_by_public_key(leaf_node.signature_key())
416 .await
417 .map_err(RecursiveError::mls_client("finding current credential"))?;
418 Ok(credential)
419 }
420
421 pub(crate) async fn find_current_credential(&self, client: &Session<Database>) -> Result<Arc<Credential>> {
422 let own_leaf = Self::extract_own_updated_node_from_proposals(
425 &self.group().own_leaf_index(),
426 self.group().pending_proposals(),
427 )
428 .or_else(|| self.group.own_leaf())
429 .ok_or(LeafError::InternalMlsError)?;
430 self.find_credential_for_leaf_node(client, own_leaf).await
431 }
432}
433
434#[cfg(test)]
435pub mod test_utils {
436 use openmls::prelude::SignaturePublicKey;
437
438 use super::*;
439
440 impl MlsConversation {
441 pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
442 self.group
443 .members()
444 .map(|m| m.signature_key)
445 .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
446 }
447
448 pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
449 self.group.members().map(|m| m.encryption_key)
450 }
451
452 pub fn extensions(&self) -> &openmls::prelude::Extensions {
453 self.group.export_group_context().extensions()
454 }
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use crate::test_utils::*;
462
463 #[apply(all_cred_cipher)]
464 pub async fn create_self_conversation_should_succeed(case: TestContext) {
465 let [alice] = case.sessions().await;
466 Box::pin(async move {
467 let conversation = case.create_conversation([&alice]).await;
468 assert_eq!(1, conversation.member_count().await);
469 let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
470 assert!(alice_can_send_message.is_ok());
471 })
472 .await;
473 }
474
475 #[apply(all_cred_cipher)]
476 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
477 let [alice, bob] = case.sessions().await;
478 Box::pin(async move {
479 let conversation = case.create_conversation([&alice, &bob]).await;
480 assert_eq!(2, conversation.member_count().await);
481 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
482 })
483 .await;
484 }
485
486 #[apply(all_cred_cipher)]
487 pub async fn create_many_people_conversation(case: TestContext) {
488 const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
489 let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
490 Box::pin(async move {
491 let alice = &alice_and_friends[0];
492 let conversation = case.create_conversation([alice]).await;
493
494 let bob_and_friends = &alice_and_friends[1..];
495 let conversation = conversation.invite_notify(bob_and_friends).await;
496
497 assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
498 assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
499 })
500 .await;
501 }
502
503 mod wire_identity_getters {
504 use super::Error;
505 use crate::{
506 ClientId, CredentialType, DeviceStatus, E2eiConversationState, mls::conversation::Conversation,
507 test_utils::*,
508 };
509
510 async fn all_identities_check<'a, C, const N: usize>(
511 conversation: &'a C,
512 user_ids: &[String; N],
513 expected_sizes: [usize; N],
514 ) where
515 C: Conversation<'a> + Sync,
516 {
517 let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
518 assert_eq!(all_identities.len(), N);
519 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
520 let alice_identities = all_identities.get(user_id).unwrap();
521 assert_eq!(alice_identities.len(), expected_size);
522 }
523 let not_found = conversation
525 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
526 .await
527 .unwrap();
528 assert!(not_found.is_empty());
529
530 let invalid = conversation.get_user_identities(&[]).await;
532 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
533 }
534
535 async fn check_identities_device_status<'a, C, const N: usize>(
536 conversation: &'a C,
537 client_ids: &[ClientId; N],
538 name_status: &[(impl ToString, DeviceStatus); N],
539 ) where
540 C: Conversation<'a> + Sync,
541 {
542 let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
543
544 for (user_name, status) in name_status.iter() {
545 let client_identity = identities.remove(
546 identities
547 .iter()
548 .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
549 .unwrap(),
550 );
551 assert_eq!(client_identity.status, *status);
552 }
553 assert!(identities.is_empty());
554
555 assert_eq!(
556 conversation.e2ei_conversation_state().await.unwrap(),
557 E2eiConversationState::NotVerified
558 );
559 }
560
561 #[macro_rules_attribute::apply(smol_macros::test)]
562 async fn should_read_device_identities() {
563 let case = TestContext::default_x509();
564
565 let [alice_android, alice_ios] = case.sessions().await;
566 Box::pin(async move {
567 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
568
569 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
570
571 let mut android_ids = conversation
572 .guard()
573 .await
574 .get_device_identities(&[android_id.clone(), ios_id.clone()])
575 .await
576 .unwrap();
577 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
578 assert_eq!(android_ids.len(), 2);
579 let mut ios_ids = conversation
580 .guard_of(&alice_ios)
581 .await
582 .get_device_identities(&[android_id.clone(), ios_id.clone()])
583 .await
584 .unwrap();
585 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
586 assert_eq!(ios_ids.len(), 2);
587
588 assert_eq!(android_ids, ios_ids);
589
590 let android_identities = conversation
591 .guard()
592 .await
593 .get_device_identities(&[android_id])
594 .await
595 .unwrap();
596 let android_id = android_identities.first().unwrap();
597 assert_eq!(
598 android_id.client_id.as_bytes(),
599 alice_android.transaction.client_id().await.unwrap().0.as_slice()
600 );
601
602 let ios_identities = conversation
603 .guard()
604 .await
605 .get_device_identities(&[ios_id])
606 .await
607 .unwrap();
608 let ios_id = ios_identities.first().unwrap();
609 assert_eq!(
610 ios_id.client_id.as_bytes(),
611 alice_ios.transaction.client_id().await.unwrap().0.as_slice()
612 );
613
614 let empty_slice: &[ClientId] = &[];
615 let invalid = conversation.guard().await.get_device_identities(empty_slice).await;
616 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
617 })
618 .await
619 }
620
621 #[ignore]
623 #[macro_rules_attribute::apply(smol_macros::test)]
624 async fn should_read_revoked_device() {
625 let case = TestContext::default_x509();
626 let rupert_user_id = uuid::Uuid::new_v4();
627 let bob_user_id = uuid::Uuid::new_v4();
628 let alice_user_id = uuid::Uuid::new_v4();
629
630 let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
631 let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
632 let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
633
634 let sessions = case
635 .sessions_x509_with_client_ids_and_revocation(
636 [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
637 &[rupert_user_id.to_string()],
638 )
639 .await;
640
641 Box::pin(async move {
642 let [alice, bob, rupert] = &sessions;
643 let conversation = case.create_conversation(&sessions).await;
644
645 let (alice_id, bob_id, rupert_id) = (
646 alice.get_client_id().await,
647 bob.get_client_id().await,
648 rupert.get_client_id().await,
649 );
650
651 let client_ids = [alice_id, bob_id, rupert_id];
652 let name_status = [
653 (alice_user_id, DeviceStatus::Valid),
654 (bob_user_id, DeviceStatus::Valid),
655 (rupert_user_id, DeviceStatus::Revoked),
656 ];
657
658 for _ in 0..2 {
660 for session in sessions.iter() {
661 let conversation = conversation.guard_of(session).await;
662 check_identities_device_status(&conversation, &client_ids, &name_status).await;
663 }
664 }
665 })
666 .await
667 }
668
669 #[macro_rules_attribute::apply(smol_macros::test)]
670 async fn should_not_fail_when_basic() {
671 let case = TestContext::default();
672
673 let [alice_android, alice_ios] = case.sessions().await;
674 Box::pin(async move {
675 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
676
677 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
678
679 let mut android_ids = conversation
680 .guard()
681 .await
682 .get_device_identities(&[android_id.clone(), ios_id.clone()])
683 .await
684 .unwrap();
685 android_ids.sort();
686
687 let mut ios_ids = conversation
688 .guard_of(&alice_ios)
689 .await
690 .get_device_identities(&[android_id, ios_id])
691 .await
692 .unwrap();
693 ios_ids.sort();
694
695 assert_eq!(ios_ids.len(), 2);
696 assert_eq!(ios_ids, android_ids);
697
698 assert!(ios_ids.iter().all(|i| {
699 matches!(i.credential_type, CredentialType::Basic)
700 && matches!(i.status, DeviceStatus::Valid)
701 && i.x509_identity.is_none()
702 && !i.thumbprint.is_empty()
703 && !i.client_id.is_empty()
704 }));
705 })
706 .await
707 }
708
709 #[macro_rules_attribute::apply(smol_macros::test)]
710 async fn should_read_users() {
711 let case = TestContext::default_x509();
712 let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
713 let [bob_android] = case.x509_client_ids();
714
715 let sessions = case
716 .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
717 .await;
718
719 Box::pin(async move {
720 let conversation = case.create_conversation(&sessions).await;
721
722 let nb_members = conversation.member_count().await;
723 assert_eq!(nb_members, 3);
724
725 let [alice_android, alice_ios, bob_android] = &sessions;
726 assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
727
728 let alice_user_id = alice_android.get_user_id().await;
730 let alice_identities = conversation
731 .guard()
732 .await
733 .get_user_identities(std::slice::from_ref(&alice_user_id))
734 .await
735 .unwrap();
736 assert_eq!(alice_identities.len(), 1);
737 let identities = alice_identities.get(&alice_user_id).unwrap();
738 assert_eq!(identities.len(), 2);
739
740 let bob_user_id = bob_android.get_user_id().await;
742 let bob_identities = conversation
743 .guard()
744 .await
745 .get_user_identities(std::slice::from_ref(&bob_user_id))
746 .await
747 .unwrap();
748 assert_eq!(bob_identities.len(), 1);
749 let identities = bob_identities.get(&bob_user_id).unwrap();
750 assert_eq!(identities.len(), 1);
751
752 let user_ids = [alice_user_id, bob_user_id];
753 let expected_sizes = [2, 1];
754
755 for session in &sessions {
756 all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
757 }
758 })
759 .await
760 }
761 }
762
763 mod export_secret {
764 use openmls::prelude::ExportSecretError;
765
766 use super::*;
767 use crate::MlsErrorKind;
768
769 #[apply(all_cred_cipher)]
770 pub async fn can_export_secret_key(case: TestContext) {
771 let [alice] = case.sessions().await;
772 Box::pin(async move {
773 let conversation = case.create_conversation([&alice]).await;
774
775 let key_length = 128;
776 let result = conversation.guard().await.export_secret_key(key_length).await;
777 assert!(result.is_ok());
778 assert_eq!(result.unwrap().len(), key_length);
779 })
780 .await
781 }
782
783 #[apply(all_cred_cipher)]
784 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
785 let [alice] = case.sessions().await;
786 Box::pin(async move {
787 let conversation = case.create_conversation([&alice]).await;
788
789 let result = conversation.guard().await.export_secret_key(usize::MAX).await;
790 let error = result.unwrap_err();
791 assert!(innermost_source_matches!(
792 error,
793 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
794 ));
795 })
796 .await
797 }
798 }
799
800 mod get_client_ids {
801 use super::*;
802
803 #[apply(all_cred_cipher)]
804 pub async fn can_get_client_ids(case: TestContext) {
805 let [alice, bob] = case.sessions().await;
806 Box::pin(async move {
807 let conversation = case.create_conversation([&alice]).await;
808
809 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
810
811 let conversation = conversation.invite_notify([&bob]).await;
812
813 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
814 })
815 .await
816 }
817 }
818
819 mod external_sender {
820 use super::*;
821
822 #[apply(all_cred_cipher)]
823 pub async fn should_fetch_ext_sender(mut case: TestContext) {
824 let [alice, external_sender] = case.sessions().await;
825 Box::pin(async move {
826 use core_crypto_keystore::Sha256Hash;
827
828 let conversation = case
829 .create_conversation_with_external_sender(&external_sender, [&alice])
830 .await;
831
832 let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
833 assert!(!alice_ext_sender.is_empty());
834 assert_eq!(
835 Sha256Hash::hash_from(alice_ext_sender),
836 external_sender.initial_credential.public_key_hash()
837 );
838 })
839 .await
840 }
841 }
842}