1use config::MlsConversationConfiguration;
15use core_crypto_keystore::CryptoKeystoreMls;
16use itertools::Itertools as _;
17use log::trace;
18use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
19use openmls::{
20 group::MlsGroup,
21 prelude::{Credential, CredentialWithKey, LeafNodeIndex, Proposal, SignaturePublicKey},
22};
23use openmls_traits::OpenMlsCryptoProvider;
24use openmls_traits::types::SignatureScheme;
25use std::{collections::HashMap, sync::Arc};
26use std::{collections::HashSet, ops::Deref};
27
28use crate::{
29 KeystoreError, LeafError, MlsError, RecursiveError,
30 mls::Session,
31 prelude::{ClientId, E2eiConversationState, MlsCiphersuite, MlsCredentialType, WireIdentity},
32};
33
34pub(crate) mod commit;
35mod commit_delay;
36pub(crate) mod config;
37pub(crate) mod conversation_guard;
38mod duplicate;
39#[cfg(test)]
40mod durability;
41mod error;
42pub(crate) mod group_info;
43mod immutable_conversation;
44pub(crate) mod merge;
45mod orphan_welcome;
46mod own_commit;
47pub(crate) mod pending_conversation;
48pub(crate) mod proposal;
49mod renew;
50pub(crate) mod welcome;
51mod wipe;
52
53use crate::mls::HasSessionAndCrypto;
54use crate::mls::credential::ext::CredentialExt as _;
55use crate::prelude::user_id::UserId;
56pub use conversation_guard::ConversationGuard;
57pub use error::{Error, Result};
58pub use immutable_conversation::ImmutableConversation;
59
60use super::credential::CredentialBundle;
61
62#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
65#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
66pub(crate) trait ConversationWithMls<'a> {
67 type Context: HasSessionAndCrypto;
69
70 type Conversation: Deref<Target = MlsConversation> + Send;
71
72 async fn context(&self) -> Result<Self::Context>;
73
74 async fn conversation(&'a self) -> Self::Conversation;
75
76 async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
77 self.context()
78 .await?
79 .crypto_provider()
80 .await
81 .map_err(RecursiveError::mls("getting mls provider"))
82 .map_err(Into::into)
83 }
84
85 async fn session(&self) -> Result<Session> {
86 self.context()
87 .await?
88 .session()
89 .await
90 .map_err(RecursiveError::mls("getting mls client"))
91 .map_err(Into::into)
92 }
93}
94
95#[expect(private_bounds)]
100#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
101#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
102pub trait Conversation<'a>: ConversationWithMls<'a> {
103 async fn epoch(&'a self) -> u64 {
105 self.conversation().await.group().epoch().as_u64()
106 }
107
108 async fn ciphersuite(&'a self) -> MlsCiphersuite {
110 self.conversation().await.ciphersuite()
111 }
112
113 async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
122 const EXPORTER_LABEL: &str = "exporter";
123 const EXPORTER_CONTEXT: &[u8] = &[];
124 let backend = self.crypto_provider().await?;
125 let inner = self.conversation().await;
126 inner
127 .group()
128 .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
129 .map_err(MlsError::wrap("exporting secret key"))
130 .map_err(Into::into)
131 }
132
133 async fn get_client_ids(&'a self) -> Vec<ClientId> {
138 let inner = self.conversation().await;
139 inner
140 .group()
141 .members()
142 .map(|kp| ClientId::from(kp.credential.identity()))
143 .collect()
144 }
145
146 async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
149 let inner = self.conversation().await;
150 let ext_senders = inner
151 .group()
152 .group_context_extensions()
153 .external_senders()
154 .ok_or(Error::MissingExternalSenderExtension)?;
155 let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
156 let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
157 Ok(ext_sender_public_key)
158 }
159
160 async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
163 let backend = self.crypto_provider().await?;
164 let authentication_service = backend.authentication_service();
165 authentication_service.refresh_time_of_interest().await;
166 let inner = self.conversation().await;
167 let state = Session::compute_conversation_state(
168 inner.ciphersuite(),
169 inner.group.members_credentials(),
170 MlsCredentialType::X509,
171 authentication_service.borrow().await.as_ref(),
172 )
173 .await;
174 Ok(state)
175 }
176
177 async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
181 if device_ids.is_empty() {
182 return Err(Error::CallerError(
183 "This function accepts a list of IDs as a parameter, but that list was empty.",
184 ));
185 }
186 let mls_provider = self.crypto_provider().await?;
187 let auth_service = mls_provider.authentication_service();
188 auth_service.refresh_time_of_interest().await;
189 let auth_service = auth_service.borrow().await;
190 let env = auth_service.as_ref();
191 let conversation = self.conversation().await;
192 conversation
193 .members_with_key()
194 .into_iter()
195 .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
196 .map(|(_, c)| {
197 c.extract_identity(conversation.ciphersuite(), env)
198 .map_err(RecursiveError::mls_credential("extracting identity"))
199 })
200 .collect::<Result<Vec<_>, _>>()
201 .map_err(Into::into)
202 }
203
204 async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
211 if user_ids.is_empty() {
212 return Err(Error::CallerError(
213 "This function accepts a list of IDs as a parameter, but that list was empty.",
214 ));
215 }
216 let mls_provider = self.crypto_provider().await?;
217 let auth_service = mls_provider.authentication_service();
218 auth_service.refresh_time_of_interest().await;
219 let auth_service = auth_service.borrow().await;
220 let env = auth_service.as_ref();
221 let conversation = self.conversation().await;
222 let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
223
224 conversation
225 .members_with_key()
226 .iter()
227 .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
228 .filter(|(uid, _)| user_ids.contains(uid))
229 .map(|(uid, c)| {
230 let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
231 let identity = c
232 .extract_identity(conversation.ciphersuite(), env)
233 .map_err(RecursiveError::mls_credential("extracting identity"))?;
234 Ok((uid, identity))
235 })
236 .process_results(|iter| iter.into_group_map())
237 }
238
239 async fn generate_history_secret(&'a self) -> Result<crate::prelude::HistorySecret> {
245 let ciphersuite = self.ciphersuite().await;
246 crate::ephemeral::generate_history_secret(ciphersuite)
247 .await
248 .map_err(RecursiveError::root("generating history secret"))
249 .map_err(Into::into)
250 }
251
252 async fn is_history_sharing_enabled(&'a self) -> bool {
255 self.get_client_ids()
256 .await
257 .iter()
258 .any(|client_id| client_id.starts_with(crate::ephemeral::HISTORY_CLIENT_ID_PREFIX.as_bytes()))
259 }
260}
261
262impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
263
264pub type ConversationId = Vec<u8>;
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: MlsCredentialType,
297 configuration: MlsConversationConfiguration,
298 backend: &MlsCryptoProvider,
299 ) -> Result<Self> {
300 let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
301 let cb = author_client
302 .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
303 .await
304 .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
305
306 let group = MlsGroup::new_with_group_id(
307 backend,
308 &cb.signature_key,
309 &configuration.as_openmls_default_configuration()?,
310 openmls::prelude::GroupId::from_slice(id.as_slice()),
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(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
354 let group: MlsGroup =
355 core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
356 let id = ConversationId::from(group.group_id().as_slice());
357 let configuration = MlsConversationConfiguration {
358 ciphersuite: group.ciphersuite().into(),
359 ..Default::default()
360 };
361
362 Ok(Self {
363 id,
364 group,
365 parent_id,
366 configuration,
367 })
368 }
369
370 pub fn id(&self) -> &ConversationId {
372 &self.id
373 }
374
375 pub(crate) fn group(&self) -> &MlsGroup {
376 &self.group
377 }
378
379 pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
381 self.group.members().fold(HashMap::new(), |mut acc, kp| {
382 let credential = kp.credential;
383 let id = credential.identity().to_vec();
384 acc.entry(id).or_insert(credential);
385 acc
386 })
387 }
388
389 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
391 let pending_removals = self.pending_removals();
392 let existing_clients = self
393 .group
394 .members()
395 .filter_map(|kp| {
396 if !pending_removals.contains(&kp.index) {
397 Some(kp.credential.identity().into())
398 } else {
399 trace!(client_index:% = kp.index; "Client is pending removal");
400 None
401 }
402 })
403 .collect::<HashSet<_>>();
404 existing_clients.into_iter().collect()
405 }
406
407 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
409 self.group
410 .pending_proposals()
411 .filter_map(|proposal| match proposal.proposal() {
412 Proposal::Remove(remove) => Some(remove.removed()),
413 _ => None,
414 })
415 .collect::<Vec<_>>()
416 }
417
418 pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
420 self.group.members().fold(HashMap::new(), |mut acc, kp| {
421 let credential = kp.credential;
422 let id = credential.identity().to_vec();
423 let signature_key = SignaturePublicKey::from(kp.signature_key);
424 let credential = CredentialWithKey {
425 credential,
426 signature_key,
427 };
428 acc.entry(id).or_insert(credential);
429 acc
430 })
431 }
432
433 pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
434 if force || self.group.state_changed() == openmls::group::InnerState::Changed {
435 keystore
436 .mls_group_persist(
437 &self.id,
438 &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
439 self.parent_id.as_deref(),
440 )
441 .await
442 .map_err(KeystoreError::wrap("persisting mls group"))?;
443
444 self.group.set_state(openmls::group::InnerState::Persisted);
445 }
446
447 Ok(())
448 }
449
450 pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
451 Ok(self
452 .group
453 .own_leaf_node()
454 .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
455 .credential()
456 .credential_type()
457 .into())
458 }
459
460 pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
461 self.configuration.ciphersuite
462 }
463
464 pub(crate) fn signature_scheme(&self) -> SignatureScheme {
465 self.ciphersuite().signature_algorithm()
466 }
467
468 pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
469 let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
470 let sc = self.ciphersuite().signature_algorithm();
471 let ct = self
472 .own_credential_type()
473 .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
474
475 client
476 .find_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
477 .await
478 .map_err(RecursiveError::mls_client("finding current credential bundle"))
479 .map_err(Into::into)
480 }
481
482 pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
483 let sc = self.ciphersuite().signature_algorithm();
484 let ct = self
485 .own_credential_type()
486 .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
487
488 client
489 .find_most_recent_credential_bundle(sc, ct)
490 .await
491 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
492 .map_err(Into::into)
493 }
494}
495
496#[cfg(test)]
497pub mod test_utils {
498 use super::*;
499
500 impl MlsConversation {
501 pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
502 self.group
503 .members()
504 .map(|m| m.signature_key)
505 .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
506 }
507
508 pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
509 self.group.members().map(|m| m.encryption_key)
510 }
511
512 pub fn extensions(&self) -> &openmls::prelude::Extensions {
513 self.group.export_group_context().extensions()
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::test_utils::*;
522
523 #[apply(all_cred_cipher)]
524 pub async fn create_self_conversation_should_succeed(case: TestContext) {
525 let [alice] = case.sessions().await;
526 Box::pin(async move {
527 let conversation = case.create_conversation([&alice]).await;
528 assert_eq!(1, conversation.member_count().await);
529 let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
530 assert!(alice_can_send_message.is_ok());
531 })
532 .await;
533 }
534
535 #[apply(all_cred_cipher)]
536 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
537 let [alice, bob] = case.sessions().await;
538 Box::pin(async move {
539 let conversation = case.create_conversation([&alice, &bob]).await;
540 assert_eq!(2, conversation.member_count().await);
541 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
542 })
543 .await;
544 }
545
546 #[apply(all_cred_cipher)]
547 pub async fn create_many_people_conversation(case: TestContext) {
548 const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
549 let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
550 Box::pin(async move {
551 let alice = &alice_and_friends[0];
552 let conversation = case.create_conversation([alice]).await;
553
554 let bob_and_friends = &alice_and_friends[1..];
555 let conversation = conversation.invite_notify(bob_and_friends).await;
556
557 assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
558 assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
559 })
560 .await;
561 }
562
563 mod wire_identity_getters {
564 use super::Error;
565 use crate::mls::conversation::Conversation;
566 use crate::prelude::{ClientId, MlsCredentialType};
567 use crate::{
568 prelude::{DeviceStatus, E2eiConversationState},
569 test_utils::*,
570 };
571
572 async fn all_identities_check<'a, C, const N: usize>(
573 conversation: &'a C,
574 user_ids: &[String; N],
575 expected_sizes: [usize; N],
576 ) where
577 C: Conversation<'a> + Sync,
578 {
579 let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
580 assert_eq!(all_identities.len(), N);
581 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
582 let alice_identities = all_identities.get(user_id).unwrap();
583 assert_eq!(alice_identities.len(), expected_size);
584 }
585 let not_found = conversation
587 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
588 .await
589 .unwrap();
590 assert!(not_found.is_empty());
591
592 let invalid = conversation.get_user_identities(&[]).await;
594 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
595 }
596
597 async fn check_identities_device_status<'a, C, const N: usize>(
598 conversation: &'a C,
599 client_ids: &[ClientId; N],
600 name_status: &[(impl ToString, DeviceStatus); N],
601 ) where
602 C: Conversation<'a> + Sync,
603 {
604 let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
605
606 for (user_name, status) in name_status.iter() {
607 let client_identity = identities.remove(
608 identities
609 .iter()
610 .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
611 .unwrap(),
612 );
613 assert_eq!(client_identity.status, *status);
614 }
615 assert!(identities.is_empty());
616
617 assert_eq!(
618 conversation.e2ei_conversation_state().await.unwrap(),
619 E2eiConversationState::NotVerified
620 );
621 }
622
623 #[async_std::test]
624 async fn should_read_device_identities() {
625 let case = TestContext::default_x509();
626
627 let [alice_android, alice_ios] = case.sessions().await;
628 Box::pin(async move {
629 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
630
631 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
632
633 let mut android_ids = conversation
634 .guard()
635 .await
636 .get_device_identities(&[android_id.clone(), ios_id.clone()])
637 .await
638 .unwrap();
639 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
640 assert_eq!(android_ids.len(), 2);
641 let mut ios_ids = conversation
642 .guard_of(&alice_ios)
643 .await
644 .get_device_identities(&[android_id.clone(), ios_id.clone()])
645 .await
646 .unwrap();
647 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
648 assert_eq!(ios_ids.len(), 2);
649
650 assert_eq!(android_ids, ios_ids);
651
652 let android_identities = conversation
653 .guard()
654 .await
655 .get_device_identities(&[android_id])
656 .await
657 .unwrap();
658 let android_id = android_identities.first().unwrap();
659 assert_eq!(
660 android_id.client_id.as_bytes(),
661 alice_android.transaction.client_id().await.unwrap().0.as_slice()
662 );
663
664 let ios_identities = conversation
665 .guard()
666 .await
667 .get_device_identities(&[ios_id])
668 .await
669 .unwrap();
670 let ios_id = ios_identities.first().unwrap();
671 assert_eq!(
672 ios_id.client_id.as_bytes(),
673 alice_ios.transaction.client_id().await.unwrap().0.as_slice()
674 );
675
676 let invalid = conversation.guard().await.get_device_identities(&[]).await;
677 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
678 })
679 .await
680 }
681
682 #[async_std::test]
683 async fn should_read_revoked_device_cross_signed() {
684 let case = TestContext::default_x509();
685 let alice_user_id = uuid::Uuid::new_v4();
686 let bob_user_id = uuid::Uuid::new_v4();
687 let rupert_user_id = uuid::Uuid::new_v4();
688 let john_user_id = uuid::Uuid::new_v4();
689 let dilbert_user_id = uuid::Uuid::new_v4();
690
691 let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
692 let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
693 let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
694 let [john_client_id] = case.x509_client_ids_for_user(&john_user_id);
695 let [dilbert_client_id] = case.x509_client_ids_for_user(&dilbert_user_id);
696
697 let sessions = case
698 .sessions_x509_cross_signed_with_client_ids_and_revocation(
699 [alice_client_id, bob_client_id, rupert_client_id],
700 [john_client_id, dilbert_client_id],
701 &[dilbert_user_id.to_string(), rupert_user_id.to_string()],
702 )
703 .await;
704
705 Box::pin(async move {
706 let ([alice, bob, rupert], [john, dilbert]) = &sessions;
707 let mut sessions = sessions.0.iter().chain(sessions.1.iter());
708 let conversation = case.create_conversation(&mut sessions).await;
709
710 let (alice_id, bob_id, rupert_id, john_id, dilbert_id) = (
711 alice.get_client_id().await,
712 bob.get_client_id().await,
713 rupert.get_client_id().await,
714 john.get_client_id().await,
715 dilbert.get_client_id().await,
716 );
717
718 let client_ids = [alice_id, bob_id, rupert_id, john_id, dilbert_id];
719 let name_status = [
720 (alice_user_id, DeviceStatus::Valid),
721 (bob_user_id, DeviceStatus::Valid),
722 (rupert_user_id, DeviceStatus::Revoked),
723 (john_user_id, DeviceStatus::Valid),
724 (dilbert_user_id, DeviceStatus::Revoked),
725 ];
726 for _ in 0..2 {
728 for session in sessions.clone() {
729 let conversation = conversation.guard_of(session).await;
730 check_identities_device_status(&conversation, &client_ids, &name_status).await;
731 }
732 }
733 })
734 .await
735 }
736
737 #[async_std::test]
738 async fn should_read_revoked_device() {
739 let case = TestContext::default_x509();
740 let rupert_user_id = uuid::Uuid::new_v4();
741 let bob_user_id = uuid::Uuid::new_v4();
742 let alice_user_id = uuid::Uuid::new_v4();
743
744 let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
745 let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
746 let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
747
748 let sessions = case
749 .sessions_x509_with_client_ids_and_revocation(
750 [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
751 &[rupert_user_id.to_string()],
752 )
753 .await;
754
755 Box::pin(async move {
756 let [alice, bob, rupert] = &sessions;
757 let conversation = case.create_conversation(&sessions).await;
758
759 let (alice_id, bob_id, rupert_id) = (
760 alice.get_client_id().await,
761 bob.get_client_id().await,
762 rupert.get_client_id().await,
763 );
764
765 let client_ids = [alice_id, bob_id, rupert_id];
766 let name_status = [
767 (alice_user_id, DeviceStatus::Valid),
768 (bob_user_id, DeviceStatus::Valid),
769 (rupert_user_id, DeviceStatus::Revoked),
770 ];
771
772 for _ in 0..2 {
774 for session in sessions.iter() {
775 let conversation = conversation.guard_of(session).await;
776 check_identities_device_status(&conversation, &client_ids, &name_status).await;
777 }
778 }
779 })
780 .await
781 }
782
783 #[async_std::test]
784 async fn should_not_fail_when_basic() {
785 let case = TestContext::default();
786
787 let [alice_android, alice_ios] = case.sessions().await;
788 Box::pin(async move {
789 let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
790
791 let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
792
793 let mut android_ids = conversation
794 .guard()
795 .await
796 .get_device_identities(&[android_id.clone(), ios_id.clone()])
797 .await
798 .unwrap();
799 android_ids.sort();
800
801 let mut ios_ids = conversation
802 .guard_of(&alice_ios)
803 .await
804 .get_device_identities(&[android_id, ios_id])
805 .await
806 .unwrap();
807 ios_ids.sort();
808
809 assert_eq!(ios_ids.len(), 2);
810 assert_eq!(ios_ids, android_ids);
811
812 assert!(ios_ids.iter().all(|i| {
813 matches!(i.credential_type, MlsCredentialType::Basic)
814 && matches!(i.status, DeviceStatus::Valid)
815 && i.x509_identity.is_none()
816 && !i.thumbprint.is_empty()
817 && !i.client_id.is_empty()
818 }));
819 })
820 .await
821 }
822
823 #[async_std::test]
826 async fn should_read_users_cross_signed() {
827 let case = TestContext::default_x509();
828 let [alice_1_id, alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
829 let [federated_alice_1_id, federated_alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
830 let [bob_id, federated_bob_id] = case.x509_client_ids();
831
832 let ([alice_1, alice_2, bob], [federated_alice_1, federated_alice_2, federated_bob]) = case
833 .sessions_x509_cross_signed_with_client_ids(
834 [alice_1_id, alice_2_id, bob_id],
835 [federated_alice_1_id, federated_alice_2_id, federated_bob_id],
836 )
837 .await;
838 Box::pin(async move {
839 let sessions = [
840 &alice_1,
841 &alice_2,
842 &bob,
843 &federated_bob,
844 &federated_alice_1,
845 &federated_alice_2,
846 ];
847 let conversation = case.create_conversation(sessions).await;
848
849 let nb_members = conversation.member_count().await;
850 assert_eq!(nb_members, 6);
851 let conversation_guard = conversation.guard().await;
852
853 assert_eq!(alice_1.get_user_id().await, alice_2.get_user_id().await);
854
855 let alicem_user_id = federated_alice_2.get_user_id().await;
856 let bobt_user_id = federated_bob.get_user_id().await;
857
858 let alice_user_id = alice_1.get_user_id().await;
860 let alice_identities = conversation_guard
861 .get_user_identities(&[alice_user_id.clone()])
862 .await
863 .unwrap();
864 assert_eq!(alice_identities.len(), 1);
865 let identities = alice_identities.get(&alice_user_id).unwrap();
866 assert_eq!(identities.len(), 2);
867
868 let bob_user_id = bob.get_user_id().await;
870 let bob_identities = conversation_guard
871 .get_user_identities(&[bob_user_id.clone()])
872 .await
873 .unwrap();
874 assert_eq!(bob_identities.len(), 1);
875 let identities = bob_identities.get(&bob_user_id).unwrap();
876 assert_eq!(identities.len(), 1);
877
878 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
880 let expected_sizes = [2, 1, 2, 1];
881
882 for session in sessions {
883 all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
884 }
885 })
886 .await
887 }
888
889 #[async_std::test]
890 async fn should_read_users() {
891 let case = TestContext::default_x509();
892 let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
893 let [bob_android] = case.x509_client_ids();
894
895 let sessions = case
896 .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
897 .await;
898
899 Box::pin(async move {
900 let conversation = case.create_conversation(&sessions).await;
901
902 let nb_members = conversation.member_count().await;
903 assert_eq!(nb_members, 3);
904
905 let [alice_android, alice_ios, bob_android] = &sessions;
906 assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
907
908 let alice_user_id = alice_android.get_user_id().await;
910 let alice_identities = conversation
911 .guard()
912 .await
913 .get_user_identities(&[alice_user_id.clone()])
914 .await
915 .unwrap();
916 assert_eq!(alice_identities.len(), 1);
917 let identities = alice_identities.get(&alice_user_id).unwrap();
918 assert_eq!(identities.len(), 2);
919
920 let bob_user_id = bob_android.get_user_id().await;
922 let bob_identities = conversation
923 .guard()
924 .await
925 .get_user_identities(&[bob_user_id.clone()])
926 .await
927 .unwrap();
928 assert_eq!(bob_identities.len(), 1);
929 let identities = bob_identities.get(&bob_user_id).unwrap();
930 assert_eq!(identities.len(), 1);
931
932 let user_ids = [alice_user_id, bob_user_id];
933 let expected_sizes = [2, 1];
934
935 for session in &sessions {
936 all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
937 }
938 })
939 .await
940 }
941
942 #[async_std::test]
943 async fn should_exchange_messages_cross_signed() {
944 let case = TestContext::default_x509();
945 let sessions = case.sessions_x509_cross_signed::<3, 3>().await;
946 Box::pin(async move {
947 let sessions = sessions.0.iter().chain(sessions.1.iter());
948 let conversation = case.create_conversation(sessions.clone()).await;
949
950 assert_eq!(conversation.member_count().await, 6);
951
952 assert!(conversation.is_functional_and_contains(sessions).await);
953 })
954 .await;
955 }
956 }
957
958 mod export_secret {
959 use super::*;
960 use crate::MlsErrorKind;
961 use openmls::prelude::ExportSecretError;
962
963 #[apply(all_cred_cipher)]
964 pub async fn can_export_secret_key(case: TestContext) {
965 let [alice] = case.sessions().await;
966 Box::pin(async move {
967 let conversation = case.create_conversation([&alice]).await;
968
969 let key_length = 128;
970 let result = conversation.guard().await.export_secret_key(key_length).await;
971 assert!(result.is_ok());
972 assert_eq!(result.unwrap().len(), key_length);
973 })
974 .await
975 }
976
977 #[apply(all_cred_cipher)]
978 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
979 let [alice] = case.sessions().await;
980 Box::pin(async move {
981 let conversation = case.create_conversation([&alice]).await;
982
983 let result = conversation.guard().await.export_secret_key(usize::MAX).await;
984 let error = result.unwrap_err();
985 assert!(innermost_source_matches!(
986 error,
987 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
988 ));
989 })
990 .await
991 }
992 }
993
994 mod get_client_ids {
995 use super::*;
996
997 #[apply(all_cred_cipher)]
998 pub async fn can_get_client_ids(case: TestContext) {
999 let [alice, bob] = case.sessions().await;
1000 Box::pin(async move {
1001 let conversation = case.create_conversation([&alice]).await;
1002
1003 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
1004
1005 let conversation = conversation.invite_notify([&bob]).await;
1006
1007 assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
1008 })
1009 .await
1010 }
1011 }
1012
1013 mod external_sender {
1014 use super::*;
1015
1016 #[apply(all_cred_cipher)]
1017 pub async fn should_fetch_ext_sender(mut case: TestContext) {
1018 let [alice, external_sender] = case.sessions().await;
1019 Box::pin(async move {
1020 let conversation = case
1021 .create_conversation_with_external_sender(&external_sender, [&alice])
1022 .await;
1023
1024 let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
1025 assert!(!alice_ext_sender.is_empty());
1026 assert_eq!(
1027 alice_ext_sender,
1028 external_sender.client_signature_key(&case).await.as_slice().to_vec()
1029 );
1030 })
1031 .await
1032 }
1033 }
1034}