1use config::MlsConversationConfiguration;
31use core_crypto_keystore::CryptoKeystoreMls;
32use itertools::Itertools as _;
33use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
34use openmls::{
35 group::MlsGroup,
36 prelude::{Credential, CredentialWithKey, SignaturePublicKey},
37};
38use openmls_traits::OpenMlsCryptoProvider;
39use openmls_traits::types::SignatureScheme;
40use std::collections::HashMap;
41use std::ops::Deref;
42
43use crate::{
44 KeystoreError, LeafError, MlsError, RecursiveError,
45 group_store::GroupStore,
46 mls::{MlsCentral, client::Client},
47 prelude::{MlsCiphersuite, MlsCredentialType},
48};
49
50use crate::context::CentralContext;
51
52pub(crate) mod commit;
53mod commit_delay;
54pub(crate) mod config;
55pub(crate) mod conversation_guard;
56#[cfg(test)]
57mod db_count;
58mod duplicate;
59#[cfg(test)]
60mod durability;
61mod error;
62pub(crate) mod group_info;
63mod immutable_conversation;
64mod leaf_node_validation;
65pub(crate) mod merge;
66mod orphan_welcome;
67mod own_commit;
68pub(crate) mod pending_conversation;
69pub(crate) mod proposal;
70mod renew;
71pub(crate) mod welcome;
72mod wipe;
73
74use crate::e2e_identity::conversation_state::compute_state;
75use crate::mls::HasClientAndProvider;
76use crate::mls::conversation::pending_conversation::PendingConversation;
77use crate::mls::credential::ext::CredentialExt as _;
78use crate::prelude::user_id::UserId;
79use crate::prelude::{ClientId, E2eiConversationState, WireIdentity};
80pub use conversation_guard::ConversationGuard;
81use core_crypto_keystore::connection::FetchFromDatabase;
82use core_crypto_keystore::entities::PersistedMlsPendingGroup;
83pub use error::{Error, Result};
84pub use immutable_conversation::ImmutableConversation;
85
86#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
89#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
90pub(crate) trait ConversationWithMls<'a> {
91 type Central: HasClientAndProvider;
93
94 type Conversation: Deref<Target = MlsConversation> + Send;
95
96 async fn central(&self) -> Result<Self::Central>;
97
98 async fn conversation(&'a self) -> Self::Conversation;
99
100 async fn mls_provider(&self) -> Result<MlsCryptoProvider> {
101 self.central()
102 .await?
103 .mls_provider()
104 .await
105 .map_err(RecursiveError::mls("getting mls provider"))
106 .map_err(Into::into)
107 }
108
109 async fn mls_client(&self) -> Result<Client> {
110 self.central()
111 .await?
112 .client()
113 .await
114 .map_err(RecursiveError::mls("getting mls client"))
115 .map_err(Into::into)
116 }
117}
118
119#[expect(private_bounds)]
124#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
125#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
126pub trait Conversation<'a>: ConversationWithMls<'a> {
127 async fn epoch(&'a self) -> u64 {
129 self.conversation().await.group().epoch().as_u64()
130 }
131
132 async fn ciphersuite(&'a self) -> MlsCiphersuite {
134 self.conversation().await.ciphersuite()
135 }
136
137 async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
146 const EXPORTER_LABEL: &str = "exporter";
147 const EXPORTER_CONTEXT: &[u8] = &[];
148 let backend = self.mls_provider().await?;
149 let inner = self.conversation().await;
150 inner
151 .group()
152 .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
153 .map_err(MlsError::wrap("exporting secret key"))
154 .map_err(Into::into)
155 }
156
157 async fn get_client_ids(&'a self) -> Vec<ClientId> {
162 let inner = self.conversation().await;
163 inner
164 .group()
165 .members()
166 .map(|kp| ClientId::from(kp.credential.identity()))
167 .collect()
168 }
169
170 async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
173 let inner = self.conversation().await;
174 let ext_senders = inner
175 .group()
176 .group_context_extensions()
177 .external_senders()
178 .ok_or(Error::MissingExternalSenderExtension)?;
179 let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
180 let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
181 Ok(ext_sender_public_key)
182 }
183
184 async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
187 let backend = self.mls_provider().await?;
188 let authentication_service = backend.authentication_service();
189 authentication_service.refresh_time_of_interest().await;
190 let inner = self.conversation().await;
191 let state = compute_state(
192 inner.ciphersuite(),
193 inner.group.members_credentials(),
194 MlsCredentialType::X509,
195 authentication_service.borrow().await.as_ref(),
196 )
197 .await;
198 Ok(state)
199 }
200
201 async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
205 if device_ids.is_empty() {
206 return Err(Error::CallerError(
207 "This function accepts a list of IDs as a parameter, but that list was empty.",
208 ));
209 }
210 let mls_provider = self.mls_provider().await?;
211 let auth_service = mls_provider.authentication_service();
212 auth_service.refresh_time_of_interest().await;
213 let auth_service = auth_service.borrow().await;
214 let env = auth_service.as_ref();
215 let conversation = self.conversation().await;
216 conversation
217 .members_with_key()
218 .into_iter()
219 .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
220 .map(|(_, c)| {
221 c.extract_identity(conversation.ciphersuite(), env)
222 .map_err(RecursiveError::mls_credential("extracting identity"))
223 })
224 .collect::<Result<Vec<_>, _>>()
225 .map_err(Into::into)
226 }
227
228 async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
235 if user_ids.is_empty() {
236 return Err(Error::CallerError(
237 "This function accepts a list of IDs as a parameter, but that list was empty.",
238 ));
239 }
240 let mls_provider = self.mls_provider().await?;
241 let auth_service = mls_provider.authentication_service();
242 auth_service.refresh_time_of_interest().await;
243 let auth_service = auth_service.borrow().await;
244 let env = auth_service.as_ref();
245 let conversation = self.conversation().await;
246 let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
247
248 conversation
249 .members_with_key()
250 .iter()
251 .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
252 .filter(|(uid, _)| user_ids.contains(uid))
253 .map(|(uid, c)| {
254 let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
255 let identity = c
256 .extract_identity(conversation.ciphersuite(), env)
257 .map_err(RecursiveError::mls_credential("extracting identity"))?;
258 Ok((uid, identity))
259 })
260 .process_results(|iter| iter.into_group_map())
261 }
262}
263
264impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
265
266pub type ConversationId = Vec<u8>;
268
269#[derive(Debug)]
275#[allow(dead_code)]
276pub struct MlsConversation {
277 pub(crate) id: ConversationId,
278 pub(crate) parent_id: Option<ConversationId>,
279 pub(crate) group: MlsGroup,
280 configuration: MlsConversationConfiguration,
281}
282
283impl MlsConversation {
284 pub async fn create(
296 id: ConversationId,
297 author_client: &Client,
298 creator_credential_type: MlsCredentialType,
299 configuration: MlsConversationConfiguration,
300 backend: &MlsCryptoProvider,
301 ) -> Result<Self> {
302 let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
303 let cb = author_client
304 .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
305 .await
306 .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
307
308 let group = MlsGroup::new_with_group_id(
309 backend,
310 &cb.signature_key,
311 &configuration.as_openmls_default_configuration()?,
312 openmls::prelude::GroupId::from_slice(id.as_slice()),
313 cb.to_mls_credential_with_key(),
314 )
315 .await
316 .map_err(MlsError::wrap("creating group with id"))?;
317
318 let mut conversation = Self {
319 id,
320 group,
321 parent_id: None,
322 configuration,
323 };
324
325 conversation
326 .persist_group_when_changed(&backend.keystore(), true)
327 .await?;
328
329 Ok(conversation)
330 }
331
332 pub(crate) async fn from_mls_group(
334 group: MlsGroup,
335 configuration: MlsConversationConfiguration,
336 backend: &MlsCryptoProvider,
337 ) -> Result<Self> {
338 let id = ConversationId::from(group.group_id().as_slice());
339
340 let mut conversation = Self {
341 id,
342 group,
343 configuration,
344 parent_id: None,
345 };
346
347 conversation
348 .persist_group_when_changed(&backend.keystore(), true)
349 .await?;
350
351 Ok(conversation)
352 }
353
354 pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
356 let group: MlsGroup =
357 core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
358 let id = ConversationId::from(group.group_id().as_slice());
359 let configuration = MlsConversationConfiguration {
360 ciphersuite: group.ciphersuite().into(),
361 ..Default::default()
362 };
363
364 Ok(Self {
365 id,
366 group,
367 parent_id,
368 configuration,
369 })
370 }
371
372 pub fn id(&self) -> &ConversationId {
374 &self.id
375 }
376
377 pub(crate) fn group(&self) -> &MlsGroup {
378 &self.group
379 }
380
381 pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
383 self.group.members().fold(HashMap::new(), |mut acc, kp| {
384 let credential = kp.credential;
385 let id = credential.identity().to_vec();
386 acc.entry(id).or_insert(credential);
387 acc
388 })
389 }
390
391 pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
393 self.group.members().fold(HashMap::new(), |mut acc, kp| {
394 let credential = kp.credential;
395 let id = credential.identity().to_vec();
396 let signature_key = SignaturePublicKey::from(kp.signature_key);
397 let credential = CredentialWithKey {
398 credential,
399 signature_key,
400 };
401 acc.entry(id).or_insert(credential);
402 acc
403 })
404 }
405
406 pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
407 if force || self.group.state_changed() == openmls::group::InnerState::Changed {
408 keystore
409 .mls_group_persist(
410 &self.id,
411 &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
412 self.parent_id.as_deref(),
413 )
414 .await
415 .map_err(KeystoreError::wrap("persisting mls group"))?;
416
417 self.group.set_state(openmls::group::InnerState::Persisted);
418 }
419
420 Ok(())
421 }
422
423 pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
424 Ok(self
425 .group
426 .own_leaf_node()
427 .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
428 .credential()
429 .credential_type()
430 .into())
431 }
432
433 pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
434 self.configuration.ciphersuite
435 }
436
437 pub(crate) fn signature_scheme(&self) -> SignatureScheme {
438 self.ciphersuite().signature_algorithm()
439 }
440}
441
442impl MlsCentral {
443 pub async fn get_raw_conversation(&self, id: &ConversationId) -> Result<ImmutableConversation> {
448 let raw_conversation = GroupStore::fetch_from_keystore(id, &self.mls_backend.keystore(), None)
449 .await
450 .map_err(RecursiveError::root("getting conversation by id"))?
451 .ok_or_else(|| LeafError::ConversationNotFound(id.clone()))?;
452 Ok(ImmutableConversation::new(raw_conversation, self.clone()))
453 }
454}
455
456impl CentralContext {
457 pub async fn conversation(&self, id: &ConversationId) -> Result<ConversationGuard> {
461 let keystore = self
462 .mls_provider()
463 .await
464 .map_err(RecursiveError::root("getting mls provider"))?
465 .keystore();
466 let inner = self
467 .mls_groups()
468 .await
469 .map_err(RecursiveError::root("getting mls groups"))?
470 .get_fetch(id, &keystore, None)
471 .await
472 .map_err(RecursiveError::root("fetching conversation from mls groups by id"))?;
473
474 if let Some(inner) = inner {
475 return Ok(ConversationGuard::new(inner, self.clone()));
476 }
477 let pending = self.pending_conversation(id).await.map(Error::PendingConversation)?;
480 Err(pending)
481 }
482
483 pub(crate) async fn pending_conversation(&self, id: &ConversationId) -> Result<PendingConversation> {
484 let keystore = self
485 .keystore()
486 .await
487 .map_err(RecursiveError::root("getting keystore"))?;
488 let Some(pending_group) = keystore
489 .find::<PersistedMlsPendingGroup>(id)
490 .await
491 .map_err(KeystoreError::wrap("finding persisted mls pending group"))?
492 else {
493 return Err(LeafError::ConversationNotFound(id.clone()).into());
494 };
495 Ok(PendingConversation::new(pending_group, self.clone()))
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use crate::e2e_identity::rotate::tests::all::failsafe_ctx;
502 use std::sync::Arc;
503
504 use wasm_bindgen_test::*;
505
506 use crate::{
507 CoreCrypto,
508 prelude::{ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsCentralConfiguration},
509 test_utils::*,
510 };
511
512 use super::*;
513
514 wasm_bindgen_test_configure!(run_in_browser);
515
516 #[apply(all_cred_cipher)]
517 #[wasm_bindgen_test]
518 pub async fn create_self_conversation_should_succeed(case: TestCase) {
519 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
520 Box::pin(async move {
521 let id = conversation_id();
522 alice_central
523 .context
524 .new_conversation(&id, case.credential_type, case.cfg.clone())
525 .await
526 .unwrap();
527 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
528 assert_eq!(
529 alice_central
530 .get_conversation_unchecked(&id)
531 .await
532 .group
533 .group_id()
534 .as_slice(),
535 id
536 );
537 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
538 let alice_can_send_message = alice_central
539 .context
540 .conversation(&id)
541 .await
542 .unwrap()
543 .encrypt_message(b"me")
544 .await;
545 assert!(alice_can_send_message.is_ok());
546 })
547 })
548 .await;
549 }
550
551 #[apply(all_cred_cipher)]
552 #[wasm_bindgen_test]
553 pub async fn create_1_1_conversation_should_succeed(case: TestCase) {
554 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
555 Box::pin(async move {
556 let id = conversation_id();
557
558 alice_central
559 .context
560 .new_conversation(&id, case.credential_type, case.cfg.clone())
561 .await
562 .unwrap();
563
564 let bob = bob_central.rand_key_package(&case).await;
565 alice_central
566 .context
567 .conversation(&id)
568 .await
569 .unwrap()
570 .add_members(vec![bob])
571 .await
572 .unwrap();
573
574 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
575 assert_eq!(
576 alice_central
577 .get_conversation_unchecked(&id)
578 .await
579 .group
580 .group_id()
581 .as_slice(),
582 id
583 );
584 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
585
586 let welcome = alice_central.mls_transport.latest_welcome_message().await;
587 bob_central
588 .context
589 .process_welcome_message(welcome.into(), case.custom_cfg())
590 .await
591 .unwrap();
592
593 assert_eq!(
594 bob_central.get_conversation_unchecked(&id).await.id(),
595 alice_central.get_conversation_unchecked(&id).await.id()
596 );
597 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
598 })
599 })
600 .await;
601 }
602
603 #[apply(all_cred_cipher)]
604 #[wasm_bindgen_test]
605 pub async fn create_many_people_conversation(case: TestCase) {
606 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
607 Box::pin(async move {
608 let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
609 let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
610
611 let id = conversation_id();
612 alice_central
613 .context
614 .new_conversation(&id, case.credential_type, case.cfg.clone())
615 .await
616 .unwrap();
617
618 let mut bob_and_friends: Vec<ClientContext> = Vec::with_capacity(GROUP_SAMPLE_SIZE);
619 for _ in 0..GROUP_SAMPLE_SIZE {
620 let uuid = uuid::Uuid::new_v4();
621 let name = uuid.hyphenated().to_string();
622 let path = tmp_db_file();
623 let config = MlsCentralConfiguration::try_new(
624 path.0,
625 name.clone(),
626 None,
627 vec![case.ciphersuite()],
628 None,
629 Some(INITIAL_KEYING_MATERIAL_COUNT),
630 )
631 .unwrap();
632 let central = MlsCentral::try_new(config).await.unwrap();
633 let cc = CoreCrypto::from(central);
634 let friend_context = cc.new_transaction().await.unwrap();
635 let central = cc.mls;
636
637 x509_test_chain.register_with_central(&friend_context).await;
638
639 let client_id: crate::prelude::ClientId = name.as_str().into();
640 let identity = match case.credential_type {
641 MlsCredentialType::Basic => ClientIdentifier::Basic(client_id),
642 MlsCredentialType::X509 => {
643 let x509_test_chain = alice_central
644 .x509_test_chain
645 .as_ref()
646 .as_ref()
647 .expect("No x509 test chain");
648 let cert = crate::prelude::CertificateBundle::rand(
649 &client_id,
650 x509_test_chain.find_local_intermediate_ca(),
651 );
652 ClientIdentifier::X509(HashMap::from([(case.cfg.ciphersuite.signature_algorithm(), cert)]))
653 }
654 };
655 friend_context
656 .mls_init(
657 identity,
658 vec![case.cfg.ciphersuite],
659 Some(INITIAL_KEYING_MATERIAL_COUNT),
660 )
661 .await
662 .unwrap();
663
664 let context = ClientContext {
665 context: friend_context,
666 central,
667 mls_transport: Arc::<CoreCryptoTransportSuccessProvider>::default(),
668 x509_test_chain: x509_test_chain_arc.clone(),
669 };
670 bob_and_friends.push(context);
671 }
672
673 let number_of_friends = bob_and_friends.len();
674
675 let mut bob_and_friends_kps = vec![];
676 for c in &bob_and_friends {
677 bob_and_friends_kps.push(c.rand_key_package(&case).await);
678 }
679
680 alice_central
681 .context
682 .conversation(&id)
683 .await
684 .unwrap()
685 .add_members(bob_and_friends_kps)
686 .await
687 .unwrap();
688 let welcome = alice_central.mls_transport.latest_welcome_message().await;
689
690 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
691 assert_eq!(
692 alice_central
693 .get_conversation_unchecked(&id)
694 .await
695 .group
696 .group_id()
697 .as_slice(),
698 id
699 );
700 assert_eq!(
701 alice_central.get_conversation_unchecked(&id).await.members().len(),
702 1 + number_of_friends
703 );
704
705 let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
706 for c in bob_and_friends {
708 c.context
709 .process_welcome_message(welcome.clone().into(), case.custom_cfg())
710 .await
711 .unwrap();
712 assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
713 bob_and_friends_groups.push(c);
714 }
715
716 assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
717 })
718 })
719 .await;
720 }
721
722 mod wire_identity_getters {
723 use wasm_bindgen_test::*;
724
725 use super::Error;
726 use crate::context::CentralContext;
727 use crate::mls::conversation::Conversation as _;
728 use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
729 use crate::{
730 prelude::{DeviceStatus, E2eiConversationState},
731 test_utils::*,
732 };
733
734 wasm_bindgen_test_configure!(run_in_browser);
735
736 async fn all_identities_check<const N: usize>(
737 central: &CentralContext,
738 id: &ConversationId,
739 user_ids: &[String; N],
740 expected_sizes: [usize; N],
741 ) {
742 let all_identities = central
743 .conversation(id)
744 .await
745 .unwrap()
746 .get_user_identities(user_ids)
747 .await
748 .unwrap();
749 assert_eq!(all_identities.len(), N);
750 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
751 let alice_identities = all_identities.get(user_id).unwrap();
752 assert_eq!(alice_identities.len(), expected_size);
753 }
754 let not_found = central
756 .conversation(id)
757 .await
758 .unwrap()
759 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
760 .await
761 .unwrap();
762 assert!(not_found.is_empty());
763
764 let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
766 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
767 }
768
769 async fn check_identities_device_status<const N: usize>(
770 central: &CentralContext,
771 id: &ConversationId,
772 client_ids: &[ClientId; N],
773 name_status: &[(&'static str, DeviceStatus); N],
774 ) {
775 let mut identities = central
776 .conversation(id)
777 .await
778 .unwrap()
779 .get_device_identities(client_ids)
780 .await
781 .unwrap();
782
783 for j in 0..N {
784 let client_identity = identities.remove(
785 identities
786 .iter()
787 .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
788 .unwrap(),
789 );
790 assert_eq!(client_identity.status, name_status[j].1);
791 }
792 assert!(identities.is_empty());
793
794 assert_eq!(
795 central
796 .conversation(id)
797 .await
798 .unwrap()
799 .e2ei_conversation_state()
800 .await
801 .unwrap(),
802 E2eiConversationState::NotVerified
803 );
804 }
805
806 #[async_std::test]
807 #[wasm_bindgen_test]
808 async fn should_read_device_identities() {
809 let case = TestCase::default_x509();
810 run_test_with_client_ids(
811 case.clone(),
812 ["alice_android", "alice_ios"],
813 move |[alice_android_central, alice_ios_central]| {
814 Box::pin(async move {
815 let id = conversation_id();
816 alice_android_central
817 .context
818 .new_conversation(&id, case.credential_type, case.cfg.clone())
819 .await
820 .unwrap();
821 alice_android_central
822 .invite_all(&case, &id, [&alice_ios_central])
823 .await
824 .unwrap();
825
826 let (android_id, ios_id) = (
827 alice_android_central.get_client_id().await,
828 alice_ios_central.get_client_id().await,
829 );
830
831 let mut android_ids = alice_android_central
832 .context
833 .conversation(&id)
834 .await
835 .unwrap()
836 .get_device_identities(&[android_id.clone(), ios_id.clone()])
837 .await
838 .unwrap();
839 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
840 assert_eq!(android_ids.len(), 2);
841 let mut ios_ids = alice_ios_central
842 .context
843 .conversation(&id)
844 .await
845 .unwrap()
846 .get_device_identities(&[android_id.clone(), ios_id.clone()])
847 .await
848 .unwrap();
849 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
850 assert_eq!(ios_ids.len(), 2);
851
852 assert_eq!(android_ids, ios_ids);
853
854 let android_identities = alice_android_central
855 .context
856 .conversation(&id)
857 .await
858 .unwrap()
859 .get_device_identities(&[android_id])
860 .await
861 .unwrap();
862 let android_id = android_identities.first().unwrap();
863 assert_eq!(
864 android_id.client_id.as_bytes(),
865 alice_android_central.context.client_id().await.unwrap().0.as_slice()
866 );
867
868 let ios_identities = alice_android_central
869 .context
870 .conversation(&id)
871 .await
872 .unwrap()
873 .get_device_identities(&[ios_id])
874 .await
875 .unwrap();
876 let ios_id = ios_identities.first().unwrap();
877 assert_eq!(
878 ios_id.client_id.as_bytes(),
879 alice_ios_central.context.client_id().await.unwrap().0.as_slice()
880 );
881
882 let invalid = alice_android_central
883 .context
884 .conversation(&id)
885 .await
886 .unwrap()
887 .get_device_identities(&[])
888 .await;
889 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
890 })
891 },
892 )
893 .await
894 }
895
896 #[async_std::test]
897 #[wasm_bindgen_test]
898 async fn should_read_revoked_device_cross_signed() {
899 let case = TestCase::default_x509();
900 run_test_with_client_ids_and_revocation(
901 case.clone(),
902 ["alice", "bob", "rupert"],
903 ["john", "dilbert"],
904 &["rupert", "dilbert"],
905 move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
906 Box::pin(async move {
907 let id = conversation_id();
908 alice
909 .context
910 .new_conversation(&id, case.credential_type, case.cfg.clone())
911 .await
912 .unwrap();
913 alice
914 .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
915 .await
916 .unwrap();
917
918 let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
919 alice.get_client_id().await,
920 bob.get_client_id().await,
921 rupert.get_client_id().await,
922 dilbert.get_client_id().await,
923 john.get_client_id().await,
924 );
925
926 let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
927 let name_status = [
928 ("alice", DeviceStatus::Valid),
929 ("bob", DeviceStatus::Valid),
930 ("rupert", DeviceStatus::Revoked),
931 ("john", DeviceStatus::Valid),
932 ("dilbert", DeviceStatus::Revoked),
933 ];
934 for _ in 0..2 {
936 check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
937 check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
938 check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
939 check_identities_device_status(&mut john.context, &id, &client_ids, &name_status).await;
940 check_identities_device_status(&mut dilbert.context, &id, &client_ids, &name_status).await;
941 }
942 })
943 },
944 )
945 .await
946 }
947
948 #[async_std::test]
949 #[wasm_bindgen_test]
950 async fn should_read_revoked_device() {
951 let case = TestCase::default_x509();
952 run_test_with_client_ids_and_revocation(
953 case.clone(),
954 ["alice", "bob", "rupert"],
955 [],
956 &["rupert"],
957 move |[mut alice, mut bob, mut rupert], []| {
958 Box::pin(async move {
959 let id = conversation_id();
960 alice
961 .context
962 .new_conversation(&id, case.credential_type, case.cfg.clone())
963 .await
964 .unwrap();
965 alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
966
967 let (alice_id, bob_id, rupert_id) = (
968 alice.get_client_id().await,
969 bob.get_client_id().await,
970 rupert.get_client_id().await,
971 );
972
973 let client_ids = [alice_id, bob_id, rupert_id];
974 let name_status = [
975 ("alice", DeviceStatus::Valid),
976 ("bob", DeviceStatus::Valid),
977 ("rupert", DeviceStatus::Revoked),
978 ];
979
980 for _ in 0..2 {
982 check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
983 check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
984 check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
985 }
986 })
987 },
988 )
989 .await
990 }
991
992 #[async_std::test]
993 #[wasm_bindgen_test]
994 async fn should_not_fail_when_basic() {
995 let case = TestCase::default();
996 run_test_with_client_ids(
997 case.clone(),
998 ["alice_android", "alice_ios"],
999 move |[alice_android_central, alice_ios_central]| {
1000 Box::pin(async move {
1001 let id = conversation_id();
1002 alice_android_central
1003 .context
1004 .new_conversation(&id, case.credential_type, case.cfg.clone())
1005 .await
1006 .unwrap();
1007 alice_android_central
1008 .invite_all(&case, &id, [&alice_ios_central])
1009 .await
1010 .unwrap();
1011
1012 let (android_id, ios_id) = (
1013 alice_android_central.get_client_id().await,
1014 alice_ios_central.get_client_id().await,
1015 );
1016
1017 let mut android_ids = alice_android_central
1018 .context
1019 .conversation(&id)
1020 .await
1021 .unwrap()
1022 .get_device_identities(&[android_id.clone(), ios_id.clone()])
1023 .await
1024 .unwrap();
1025 android_ids.sort();
1026
1027 let mut ios_ids = alice_ios_central
1028 .context
1029 .conversation(&id)
1030 .await
1031 .unwrap()
1032 .get_device_identities(&[android_id, ios_id])
1033 .await
1034 .unwrap();
1035 ios_ids.sort();
1036
1037 assert_eq!(ios_ids.len(), 2);
1038 assert_eq!(ios_ids, android_ids);
1039
1040 assert!(ios_ids.iter().all(|i| {
1041 matches!(i.credential_type, MlsCredentialType::Basic)
1042 && matches!(i.status, DeviceStatus::Valid)
1043 && i.x509_identity.is_none()
1044 && !i.thumbprint.is_empty()
1045 && !i.client_id.is_empty()
1046 }));
1047 })
1048 },
1049 )
1050 .await
1051 }
1052
1053 #[async_std::test]
1056 #[wasm_bindgen_test]
1057 async fn should_read_users_cross_signed() {
1058 let case = TestCase::default_x509();
1059
1060 let (alice_android, alice_ios) = (
1061 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1062 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1063 );
1064 let (alicem_android, alicem_ios) = (
1065 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1066 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1067 );
1068 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1069 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1070
1071 run_test_with_deterministic_client_ids_and_revocation(
1072 case.clone(),
1073 [
1074 [alice_android, "alice_wire", "Alice Smith"],
1075 [alice_ios, "alice_wire", "Alice Smith"],
1076 [bob_android, "bob_wire", "Bob Doe"],
1077 ],
1078 [
1079 [alicem_android, "alice_zeta", "Alice Muller"],
1080 [alicem_ios, "alice_zeta", "Alice Muller"],
1081 [bobt_android, "bob_zeta", "Bob Tables"],
1082 ],
1083 &[],
1084 move |[alice_android_central, alice_ios_central, bob_android_central],
1085 [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1086 Box::pin(async move {
1087 let id = conversation_id();
1088 alice_android_central
1089 .context
1090 .new_conversation(&id, case.credential_type, case.cfg.clone())
1091 .await
1092 .unwrap();
1093 alice_android_central
1094 .invite_all(
1095 &case,
1096 &id,
1097 [
1098 &alice_ios_central,
1099 &bob_android_central,
1100 &bobt_android_central,
1101 &alicem_ios_central,
1102 &alicem_android_central,
1103 ],
1104 )
1105 .await
1106 .unwrap();
1107
1108 let nb_members = alice_android_central
1109 .get_conversation_unchecked(&id)
1110 .await
1111 .members()
1112 .len();
1113 assert_eq!(nb_members, 6);
1114
1115 assert_eq!(
1116 alice_android_central.get_user_id().await,
1117 alice_ios_central.get_user_id().await
1118 );
1119
1120 let alicem_user_id = alicem_ios_central.get_user_id().await;
1121 let bobt_user_id = bobt_android_central.get_user_id().await;
1122
1123 let alice_user_id = alice_android_central.get_user_id().await;
1125 let alice_identities = alice_android_central
1126 .context
1127 .conversation(&id)
1128 .await
1129 .unwrap()
1130 .get_user_identities(&[alice_user_id.clone()])
1131 .await
1132 .unwrap();
1133 assert_eq!(alice_identities.len(), 1);
1134 let identities = alice_identities.get(&alice_user_id).unwrap();
1135 assert_eq!(identities.len(), 2);
1136
1137 let bob_user_id = bob_android_central.get_user_id().await;
1139 let bob_identities = alice_android_central
1140 .context
1141 .conversation(&id)
1142 .await
1143 .unwrap()
1144 .get_user_identities(&[bob_user_id.clone()])
1145 .await
1146 .unwrap();
1147 assert_eq!(bob_identities.len(), 1);
1148 let identities = bob_identities.get(&bob_user_id).unwrap();
1149 assert_eq!(identities.len(), 1);
1150
1151 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1153 let expected_sizes = [2, 1, 2, 1];
1154
1155 all_identities_check(&alice_android_central.context, &id, &user_ids, expected_sizes).await;
1156 all_identities_check(&alicem_android_central.context, &id, &user_ids, expected_sizes).await;
1157 all_identities_check(&alice_ios_central.context, &id, &user_ids, expected_sizes).await;
1158 all_identities_check(&alicem_ios_central.context, &id, &user_ids, expected_sizes).await;
1159 all_identities_check(&bob_android_central.context, &id, &user_ids, expected_sizes).await;
1160 all_identities_check(&bobt_android_central.context, &id, &user_ids, expected_sizes).await;
1161 })
1162 },
1163 )
1164 .await
1165 }
1166
1167 #[async_std::test]
1168 #[wasm_bindgen_test]
1169 async fn should_read_users() {
1170 let case = TestCase::default_x509();
1171
1172 let (alice_android, alice_ios) = (
1173 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1174 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1175 );
1176 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1177
1178 run_test_with_deterministic_client_ids(
1179 case.clone(),
1180 [
1181 [alice_android, "alice_wire", "Alice Smith"],
1182 [alice_ios, "alice_wire", "Alice Smith"],
1183 [bob_android, "bob_wire", "Bob Doe"],
1184 ],
1185 move |[
1186 mut alice_android_central,
1187 mut alice_ios_central,
1188 mut bob_android_central,
1189 ]| {
1190 Box::pin(async move {
1191 let id = conversation_id();
1192 alice_android_central
1193 .context
1194 .new_conversation(&id, case.credential_type, case.cfg.clone())
1195 .await
1196 .unwrap();
1197 alice_android_central
1198 .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1199 .await
1200 .unwrap();
1201
1202 let nb_members = alice_android_central
1203 .get_conversation_unchecked(&id)
1204 .await
1205 .members()
1206 .len();
1207 assert_eq!(nb_members, 3);
1208
1209 assert_eq!(
1210 alice_android_central.get_user_id().await,
1211 alice_ios_central.get_user_id().await
1212 );
1213
1214 let alice_user_id = alice_android_central.get_user_id().await;
1216 let alice_identities = alice_android_central
1217 .context
1218 .conversation(&id)
1219 .await
1220 .unwrap()
1221 .get_user_identities(&[alice_user_id.clone()])
1222 .await
1223 .unwrap();
1224 assert_eq!(alice_identities.len(), 1);
1225 let identities = alice_identities.get(&alice_user_id).unwrap();
1226 assert_eq!(identities.len(), 2);
1227
1228 let bob_user_id = bob_android_central.get_user_id().await;
1230 let bob_identities = alice_android_central
1231 .context
1232 .conversation(&id)
1233 .await
1234 .unwrap()
1235 .get_user_identities(&[bob_user_id.clone()])
1236 .await
1237 .unwrap();
1238 assert_eq!(bob_identities.len(), 1);
1239 let identities = bob_identities.get(&bob_user_id).unwrap();
1240 assert_eq!(identities.len(), 1);
1241
1242 let user_ids = [alice_user_id, bob_user_id];
1243 let expected_sizes = [2, 1];
1244
1245 all_identities_check(&mut alice_android_central.context, &id, &user_ids, expected_sizes).await;
1246 all_identities_check(&mut alice_ios_central.context, &id, &user_ids, expected_sizes).await;
1247 all_identities_check(&mut bob_android_central.context, &id, &user_ids, expected_sizes).await;
1248 })
1249 },
1250 )
1251 .await
1252 }
1253
1254 #[async_std::test]
1255 #[wasm_bindgen_test]
1256 async fn should_exchange_messages_cross_signed() {
1257 let (alice_android, alice_ios) = (
1258 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1259 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1260 );
1261 let (alicem_android, alicem_ios) = (
1262 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1263 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1264 );
1265 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1266 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1267
1268 let case = TestCase::default_x509();
1269
1270 run_cross_signed_tests_with_client_ids(
1271 case.clone(),
1272 [
1273 [alice_android, "alice_wire", "Alice Smith"],
1274 [alice_ios, "alice_wire", "Alice Smith"],
1275 [bob_android, "bob_wire", "Bob Doe"],
1276 ],
1277 [
1278 [alicem_android, "alice_zeta", "Alice Muller"],
1279 [alicem_ios, "alice_zeta", "Alice Muller"],
1280 [bobt_android, "bob_zeta", "Bob Tables"],
1281 ],
1282 ("wire.com", "zeta.com"),
1283 move |[
1284 mut alices_android_central,
1285 mut alices_ios_central,
1286 mut bob_android_central,
1287 ],
1288 [
1289 mut alicem_android_central,
1290 mut alicem_ios_central,
1291 mut bobt_android_central,
1292 ]| {
1293 Box::pin(async move {
1294 let id = conversation_id();
1295 alices_ios_central
1296 .context
1297 .new_conversation(&id, case.credential_type, case.cfg.clone())
1298 .await
1299 .unwrap();
1300
1301 alices_ios_central
1302 .invite_all(
1303 &case,
1304 &id,
1305 [
1306 &mut alices_android_central,
1307 &mut bob_android_central,
1308 &mut alicem_android_central,
1309 &mut alicem_ios_central,
1310 &mut bobt_android_central,
1311 ],
1312 )
1313 .await
1314 .unwrap();
1315
1316 let nb_members = alices_android_central
1317 .get_conversation_unchecked(&id)
1318 .await
1319 .members()
1320 .len();
1321 assert_eq!(nb_members, 6);
1322
1323 assert_eq!(
1324 alicem_android_central.get_user_id().await,
1325 alicem_ios_central.get_user_id().await
1326 );
1327
1328 bobt_android_central
1330 .try_talk_to(&id, &mut alices_ios_central)
1331 .await
1332 .unwrap();
1333
1334 bob_android_central
1336 .try_talk_to(&id, &mut alices_ios_central)
1337 .await
1338 .unwrap();
1339 })
1340 },
1341 )
1342 .await;
1343 }
1344 }
1345
1346 mod export_secret {
1347 use super::*;
1348 use crate::MlsErrorKind;
1349 use openmls::prelude::ExportSecretError;
1350
1351 #[apply(all_cred_cipher)]
1352 #[wasm_bindgen_test]
1353 pub async fn can_export_secret_key(case: TestCase) {
1354 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1355 Box::pin(async move {
1356 let id = conversation_id();
1357 alice_central
1358 .context
1359 .new_conversation(&id, case.credential_type, case.cfg.clone())
1360 .await
1361 .unwrap();
1362
1363 let key_length = 128;
1364 let result = alice_central
1365 .context
1366 .conversation(&id)
1367 .await
1368 .unwrap()
1369 .export_secret_key(key_length)
1370 .await;
1371 assert!(result.is_ok());
1372 assert_eq!(result.unwrap().len(), key_length);
1373 })
1374 })
1375 .await
1376 }
1377
1378 #[apply(all_cred_cipher)]
1379 #[wasm_bindgen_test]
1380 pub async fn cannot_export_secret_key_invalid_length(case: TestCase) {
1381 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1382 Box::pin(async move {
1383 let id = conversation_id();
1384 alice_central
1385 .context
1386 .new_conversation(&id, case.credential_type, case.cfg.clone())
1387 .await
1388 .unwrap();
1389
1390 let result = alice_central
1391 .context
1392 .conversation(&id)
1393 .await
1394 .unwrap()
1395 .export_secret_key(usize::MAX)
1396 .await;
1397 let error = result.unwrap_err();
1398 assert!(innermost_source_matches!(
1399 error,
1400 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1401 ));
1402 })
1403 })
1404 .await
1405 }
1406 }
1407
1408 mod get_client_ids {
1409 use super::*;
1410
1411 #[apply(all_cred_cipher)]
1412 #[wasm_bindgen_test]
1413 pub async fn can_get_client_ids(case: TestCase) {
1414 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1415 Box::pin(async move {
1416 let id = conversation_id();
1417 alice_central
1418 .context
1419 .new_conversation(&id, case.credential_type, case.cfg.clone())
1420 .await
1421 .unwrap();
1422
1423 assert_eq!(
1424 alice_central
1425 .context
1426 .conversation(&id)
1427 .await
1428 .unwrap()
1429 .get_client_ids()
1430 .await
1431 .len(),
1432 1
1433 );
1434
1435 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1436 assert_eq!(
1437 alice_central
1438 .context
1439 .conversation(&id)
1440 .await
1441 .unwrap()
1442 .get_client_ids()
1443 .await
1444 .len(),
1445 2
1446 );
1447 })
1448 })
1449 .await
1450 }
1451 }
1452
1453 mod external_sender {
1454 use super::*;
1455
1456 #[apply(all_cred_cipher)]
1457 #[wasm_bindgen_test]
1458 pub async fn should_fetch_ext_sender(case: TestCase) {
1459 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1460 Box::pin(async move {
1461 let id = conversation_id();
1462
1463 let mut cfg = case.cfg.clone();
1465 let external_sender = alice_central.rand_external_sender(&case).await;
1466 cfg.external_senders = vec![external_sender.clone()];
1467
1468 alice_central
1469 .context
1470 .new_conversation(&id, case.credential_type, cfg)
1471 .await
1472 .unwrap();
1473
1474 let alice_ext_sender = alice_central
1475 .context
1476 .conversation(&id)
1477 .await
1478 .unwrap()
1479 .get_external_sender()
1480 .await
1481 .unwrap();
1482 assert!(!alice_ext_sender.is_empty());
1483 assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1484 })
1485 })
1486 .await
1487 }
1488 }
1489}