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