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