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::test_utils::*;
501 use wasm_bindgen_test::*;
502
503 wasm_bindgen_test_configure!(run_in_browser);
504
505 #[apply(all_cred_cipher)]
506 #[wasm_bindgen_test]
507 pub async fn create_self_conversation_should_succeed(case: TestContext) {
508 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
509 Box::pin(async move {
510 let id = conversation_id();
511 alice_central
512 .transaction
513 .new_conversation(&id, case.credential_type, case.cfg.clone())
514 .await
515 .unwrap();
516 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
517 assert_eq!(
518 alice_central
519 .get_conversation_unchecked(&id)
520 .await
521 .group
522 .group_id()
523 .as_slice(),
524 id
525 );
526 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
527 let alice_can_send_message = alice_central
528 .transaction
529 .conversation(&id)
530 .await
531 .unwrap()
532 .encrypt_message(b"me")
533 .await;
534 assert!(alice_can_send_message.is_ok());
535 })
536 })
537 .await;
538 }
539
540 #[apply(all_cred_cipher)]
541 #[wasm_bindgen_test]
542 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
543 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
544 Box::pin(async move {
545 let id = conversation_id();
546
547 alice_central
548 .transaction
549 .new_conversation(&id, case.credential_type, case.cfg.clone())
550 .await
551 .unwrap();
552
553 let bob = bob_central.rand_key_package(&case).await;
554 alice_central
555 .transaction
556 .conversation(&id)
557 .await
558 .unwrap()
559 .add_members(vec![bob])
560 .await
561 .unwrap();
562
563 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
564 assert_eq!(
565 alice_central
566 .get_conversation_unchecked(&id)
567 .await
568 .group
569 .group_id()
570 .as_slice(),
571 id
572 );
573 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
574
575 let welcome = alice_central.mls_transport.latest_welcome_message().await;
576 bob_central
577 .transaction
578 .process_welcome_message(welcome.into(), case.custom_cfg())
579 .await
580 .unwrap();
581
582 assert_eq!(
583 bob_central.get_conversation_unchecked(&id).await.id(),
584 alice_central.get_conversation_unchecked(&id).await.id()
585 );
586 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
587 })
588 })
589 .await;
590 }
591
592 #[apply(all_cred_cipher)]
593 #[wasm_bindgen_test]
594 pub async fn create_many_people_conversation(case: TestContext) {
595 use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
596
597 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
598 Box::pin(async move {
599 let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
600 let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
601
602 let id = conversation_id();
603 alice_central
604 .transaction
605 .new_conversation(&id, case.credential_type, case.cfg.clone())
606 .await
607 .unwrap();
608 let bob_and_friends = case.sessions_x509::<GROUP_SAMPLE_SIZE>(Some(x509_test_chain)).await;
609
610 let mut bob_and_friends_kps = vec![];
611 for c in &bob_and_friends {
612 bob_and_friends_kps.push(c.rand_key_package(&case).await);
613 }
614
615 alice_central
616 .transaction
617 .conversation(&id)
618 .await
619 .unwrap()
620 .add_members(bob_and_friends_kps)
621 .await
622 .unwrap();
623 let welcome = alice_central.mls_transport.latest_welcome_message().await;
624
625 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
626 assert_eq!(
627 alice_central
628 .get_conversation_unchecked(&id)
629 .await
630 .group
631 .group_id()
632 .as_slice(),
633 id
634 );
635 assert_eq!(
636 alice_central.get_conversation_unchecked(&id).await.members().len(),
637 1 + GROUP_SAMPLE_SIZE
638 );
639
640 let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
641 for c in bob_and_friends {
643 c.transaction
644 .process_welcome_message(welcome.clone().into(), case.custom_cfg())
645 .await
646 .unwrap();
647 assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
648 bob_and_friends_groups.push(c);
649 }
650
651 assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
652 })
653 })
654 .await;
655 }
656
657 mod wire_identity_getters {
658 use wasm_bindgen_test::*;
659
660 use super::Error;
661 use crate::mls::conversation::Conversation as _;
662 use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
663 use crate::transaction_context::TransactionContext;
664 use crate::{
665 prelude::{DeviceStatus, E2eiConversationState},
666 test_utils::*,
667 };
668
669 wasm_bindgen_test_configure!(run_in_browser);
670
671 async fn all_identities_check<const N: usize>(
672 central: &TransactionContext,
673 id: &ConversationId,
674 user_ids: &[String; N],
675 expected_sizes: [usize; N],
676 ) {
677 let all_identities = central
678 .conversation(id)
679 .await
680 .unwrap()
681 .get_user_identities(user_ids)
682 .await
683 .unwrap();
684 assert_eq!(all_identities.len(), N);
685 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
686 let alice_identities = all_identities.get(user_id).unwrap();
687 assert_eq!(alice_identities.len(), expected_size);
688 }
689 let not_found = central
691 .conversation(id)
692 .await
693 .unwrap()
694 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
695 .await
696 .unwrap();
697 assert!(not_found.is_empty());
698
699 let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
701 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
702 }
703
704 async fn check_identities_device_status<const N: usize>(
705 central: &TransactionContext,
706 id: &ConversationId,
707 client_ids: &[ClientId; N],
708 name_status: &[(&'static str, DeviceStatus); N],
709 ) {
710 let mut identities = central
711 .conversation(id)
712 .await
713 .unwrap()
714 .get_device_identities(client_ids)
715 .await
716 .unwrap();
717
718 for j in 0..N {
719 let client_identity = identities.remove(
720 identities
721 .iter()
722 .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
723 .unwrap(),
724 );
725 assert_eq!(client_identity.status, name_status[j].1);
726 }
727 assert!(identities.is_empty());
728
729 assert_eq!(
730 central
731 .conversation(id)
732 .await
733 .unwrap()
734 .e2ei_conversation_state()
735 .await
736 .unwrap(),
737 E2eiConversationState::NotVerified
738 );
739 }
740
741 #[async_std::test]
742 #[wasm_bindgen_test]
743 async fn should_read_device_identities() {
744 let case = TestContext::default_x509();
745 run_test_with_client_ids(
746 case.clone(),
747 ["alice_android", "alice_ios"],
748 move |[alice_android_central, alice_ios_central]| {
749 Box::pin(async move {
750 let id = conversation_id();
751 alice_android_central
752 .transaction
753 .new_conversation(&id, case.credential_type, case.cfg.clone())
754 .await
755 .unwrap();
756 alice_android_central
757 .invite_all(&case, &id, [&alice_ios_central])
758 .await
759 .unwrap();
760
761 let (android_id, ios_id) = (
762 alice_android_central.get_client_id().await,
763 alice_ios_central.get_client_id().await,
764 );
765
766 let mut android_ids = alice_android_central
767 .transaction
768 .conversation(&id)
769 .await
770 .unwrap()
771 .get_device_identities(&[android_id.clone(), ios_id.clone()])
772 .await
773 .unwrap();
774 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
775 assert_eq!(android_ids.len(), 2);
776 let mut ios_ids = alice_ios_central
777 .transaction
778 .conversation(&id)
779 .await
780 .unwrap()
781 .get_device_identities(&[android_id.clone(), ios_id.clone()])
782 .await
783 .unwrap();
784 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
785 assert_eq!(ios_ids.len(), 2);
786
787 assert_eq!(android_ids, ios_ids);
788
789 let android_identities = alice_android_central
790 .transaction
791 .conversation(&id)
792 .await
793 .unwrap()
794 .get_device_identities(&[android_id])
795 .await
796 .unwrap();
797 let android_id = android_identities.first().unwrap();
798 assert_eq!(
799 android_id.client_id.as_bytes(),
800 alice_android_central
801 .transaction
802 .client_id()
803 .await
804 .unwrap()
805 .0
806 .as_slice()
807 );
808
809 let ios_identities = alice_android_central
810 .transaction
811 .conversation(&id)
812 .await
813 .unwrap()
814 .get_device_identities(&[ios_id])
815 .await
816 .unwrap();
817 let ios_id = ios_identities.first().unwrap();
818 assert_eq!(
819 ios_id.client_id.as_bytes(),
820 alice_ios_central.transaction.client_id().await.unwrap().0.as_slice()
821 );
822
823 let invalid = alice_android_central
824 .transaction
825 .conversation(&id)
826 .await
827 .unwrap()
828 .get_device_identities(&[])
829 .await;
830 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
831 })
832 },
833 )
834 .await
835 }
836
837 #[async_std::test]
838 #[wasm_bindgen_test]
839 async fn should_read_revoked_device_cross_signed() {
840 let case = TestContext::default_x509();
841 run_test_with_client_ids_and_revocation(
842 case.clone(),
843 ["alice", "bob", "rupert"],
844 ["john", "dilbert"],
845 &["rupert", "dilbert"],
846 move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
847 Box::pin(async move {
848 let id = conversation_id();
849 alice
850 .transaction
851 .new_conversation(&id, case.credential_type, case.cfg.clone())
852 .await
853 .unwrap();
854 alice
855 .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
856 .await
857 .unwrap();
858
859 let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
860 alice.get_client_id().await,
861 bob.get_client_id().await,
862 rupert.get_client_id().await,
863 dilbert.get_client_id().await,
864 john.get_client_id().await,
865 );
866
867 let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
868 let name_status = [
869 ("alice", DeviceStatus::Valid),
870 ("bob", DeviceStatus::Valid),
871 ("rupert", DeviceStatus::Revoked),
872 ("john", DeviceStatus::Valid),
873 ("dilbert", DeviceStatus::Revoked),
874 ];
875 for _ in 0..2 {
877 check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
878 .await;
879 check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
880 check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
881 .await;
882 check_identities_device_status(&mut john.transaction, &id, &client_ids, &name_status).await;
883 check_identities_device_status(&mut dilbert.transaction, &id, &client_ids, &name_status)
884 .await;
885 }
886 })
887 },
888 )
889 .await
890 }
891
892 #[async_std::test]
893 #[wasm_bindgen_test]
894 async fn should_read_revoked_device() {
895 let case = TestContext::default_x509();
896 run_test_with_client_ids_and_revocation(
897 case.clone(),
898 ["alice", "bob", "rupert"],
899 [],
900 &["rupert"],
901 move |[mut alice, mut bob, mut rupert], []| {
902 Box::pin(async move {
903 let id = conversation_id();
904 alice
905 .transaction
906 .new_conversation(&id, case.credential_type, case.cfg.clone())
907 .await
908 .unwrap();
909 alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
910
911 let (alice_id, bob_id, rupert_id) = (
912 alice.get_client_id().await,
913 bob.get_client_id().await,
914 rupert.get_client_id().await,
915 );
916
917 let client_ids = [alice_id, bob_id, rupert_id];
918 let name_status = [
919 ("alice", DeviceStatus::Valid),
920 ("bob", DeviceStatus::Valid),
921 ("rupert", DeviceStatus::Revoked),
922 ];
923
924 for _ in 0..2 {
926 check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
927 .await;
928 check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
929 check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
930 .await;
931 }
932 })
933 },
934 )
935 .await
936 }
937
938 #[async_std::test]
939 #[wasm_bindgen_test]
940 async fn should_not_fail_when_basic() {
941 let case = TestContext::default();
942 run_test_with_client_ids(
943 case.clone(),
944 ["alice_android", "alice_ios"],
945 move |[alice_android_central, alice_ios_central]| {
946 Box::pin(async move {
947 let id = conversation_id();
948 alice_android_central
949 .transaction
950 .new_conversation(&id, case.credential_type, case.cfg.clone())
951 .await
952 .unwrap();
953 alice_android_central
954 .invite_all(&case, &id, [&alice_ios_central])
955 .await
956 .unwrap();
957
958 let (android_id, ios_id) = (
959 alice_android_central.get_client_id().await,
960 alice_ios_central.get_client_id().await,
961 );
962
963 let mut android_ids = alice_android_central
964 .transaction
965 .conversation(&id)
966 .await
967 .unwrap()
968 .get_device_identities(&[android_id.clone(), ios_id.clone()])
969 .await
970 .unwrap();
971 android_ids.sort();
972
973 let mut ios_ids = alice_ios_central
974 .transaction
975 .conversation(&id)
976 .await
977 .unwrap()
978 .get_device_identities(&[android_id, ios_id])
979 .await
980 .unwrap();
981 ios_ids.sort();
982
983 assert_eq!(ios_ids.len(), 2);
984 assert_eq!(ios_ids, android_ids);
985
986 assert!(ios_ids.iter().all(|i| {
987 matches!(i.credential_type, MlsCredentialType::Basic)
988 && matches!(i.status, DeviceStatus::Valid)
989 && i.x509_identity.is_none()
990 && !i.thumbprint.is_empty()
991 && !i.client_id.is_empty()
992 }));
993 })
994 },
995 )
996 .await
997 }
998
999 #[async_std::test]
1002 #[wasm_bindgen_test]
1003 async fn should_read_users_cross_signed() {
1004 let case = TestContext::default_x509();
1005
1006 let (alice_android, alice_ios) = (
1007 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1008 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1009 );
1010 let (alicem_android, alicem_ios) = (
1011 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1012 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1013 );
1014 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1015 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1016
1017 run_test_with_deterministic_client_ids_and_revocation(
1018 case.clone(),
1019 [
1020 [alice_android, "alice_wire", "Alice Smith"],
1021 [alice_ios, "alice_wire", "Alice Smith"],
1022 [bob_android, "bob_wire", "Bob Doe"],
1023 ],
1024 [
1025 [alicem_android, "alice_zeta", "Alice Muller"],
1026 [alicem_ios, "alice_zeta", "Alice Muller"],
1027 [bobt_android, "bob_zeta", "Bob Tables"],
1028 ],
1029 &[],
1030 move |[alice_android_central, alice_ios_central, bob_android_central],
1031 [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1032 Box::pin(async move {
1033 let id = conversation_id();
1034 alice_android_central
1035 .transaction
1036 .new_conversation(&id, case.credential_type, case.cfg.clone())
1037 .await
1038 .unwrap();
1039 alice_android_central
1040 .invite_all(
1041 &case,
1042 &id,
1043 [
1044 &alice_ios_central,
1045 &bob_android_central,
1046 &bobt_android_central,
1047 &alicem_ios_central,
1048 &alicem_android_central,
1049 ],
1050 )
1051 .await
1052 .unwrap();
1053
1054 let nb_members = alice_android_central
1055 .get_conversation_unchecked(&id)
1056 .await
1057 .members()
1058 .len();
1059 assert_eq!(nb_members, 6);
1060
1061 assert_eq!(
1062 alice_android_central.get_user_id().await,
1063 alice_ios_central.get_user_id().await
1064 );
1065
1066 let alicem_user_id = alicem_ios_central.get_user_id().await;
1067 let bobt_user_id = bobt_android_central.get_user_id().await;
1068
1069 let alice_user_id = alice_android_central.get_user_id().await;
1071 let alice_identities = alice_android_central
1072 .transaction
1073 .conversation(&id)
1074 .await
1075 .unwrap()
1076 .get_user_identities(&[alice_user_id.clone()])
1077 .await
1078 .unwrap();
1079 assert_eq!(alice_identities.len(), 1);
1080 let identities = alice_identities.get(&alice_user_id).unwrap();
1081 assert_eq!(identities.len(), 2);
1082
1083 let bob_user_id = bob_android_central.get_user_id().await;
1085 let bob_identities = alice_android_central
1086 .transaction
1087 .conversation(&id)
1088 .await
1089 .unwrap()
1090 .get_user_identities(&[bob_user_id.clone()])
1091 .await
1092 .unwrap();
1093 assert_eq!(bob_identities.len(), 1);
1094 let identities = bob_identities.get(&bob_user_id).unwrap();
1095 assert_eq!(identities.len(), 1);
1096
1097 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1099 let expected_sizes = [2, 1, 2, 1];
1100
1101 all_identities_check(&alice_android_central.transaction, &id, &user_ids, expected_sizes).await;
1102 all_identities_check(&alicem_android_central.transaction, &id, &user_ids, expected_sizes).await;
1103 all_identities_check(&alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1104 all_identities_check(&alicem_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1105 all_identities_check(&bob_android_central.transaction, &id, &user_ids, expected_sizes).await;
1106 all_identities_check(&bobt_android_central.transaction, &id, &user_ids, expected_sizes).await;
1107 })
1108 },
1109 )
1110 .await
1111 }
1112
1113 #[async_std::test]
1114 #[wasm_bindgen_test]
1115 async fn should_read_users() {
1116 let case = TestContext::default_x509();
1117
1118 let (alice_android, alice_ios) = (
1119 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1120 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1121 );
1122 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1123
1124 run_test_with_deterministic_client_ids(
1125 case.clone(),
1126 [
1127 [alice_android, "alice_wire", "Alice Smith"],
1128 [alice_ios, "alice_wire", "Alice Smith"],
1129 [bob_android, "bob_wire", "Bob Doe"],
1130 ],
1131 move |[
1132 mut alice_android_central,
1133 mut alice_ios_central,
1134 mut bob_android_central,
1135 ]| {
1136 Box::pin(async move {
1137 let id = conversation_id();
1138 alice_android_central
1139 .transaction
1140 .new_conversation(&id, case.credential_type, case.cfg.clone())
1141 .await
1142 .unwrap();
1143 alice_android_central
1144 .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1145 .await
1146 .unwrap();
1147
1148 let nb_members = alice_android_central
1149 .get_conversation_unchecked(&id)
1150 .await
1151 .members()
1152 .len();
1153 assert_eq!(nb_members, 3);
1154
1155 assert_eq!(
1156 alice_android_central.get_user_id().await,
1157 alice_ios_central.get_user_id().await
1158 );
1159
1160 let alice_user_id = alice_android_central.get_user_id().await;
1162 let alice_identities = alice_android_central
1163 .transaction
1164 .conversation(&id)
1165 .await
1166 .unwrap()
1167 .get_user_identities(&[alice_user_id.clone()])
1168 .await
1169 .unwrap();
1170 assert_eq!(alice_identities.len(), 1);
1171 let identities = alice_identities.get(&alice_user_id).unwrap();
1172 assert_eq!(identities.len(), 2);
1173
1174 let bob_user_id = bob_android_central.get_user_id().await;
1176 let bob_identities = alice_android_central
1177 .transaction
1178 .conversation(&id)
1179 .await
1180 .unwrap()
1181 .get_user_identities(&[bob_user_id.clone()])
1182 .await
1183 .unwrap();
1184 assert_eq!(bob_identities.len(), 1);
1185 let identities = bob_identities.get(&bob_user_id).unwrap();
1186 assert_eq!(identities.len(), 1);
1187
1188 let user_ids = [alice_user_id, bob_user_id];
1189 let expected_sizes = [2, 1];
1190
1191 all_identities_check(&mut alice_android_central.transaction, &id, &user_ids, expected_sizes)
1192 .await;
1193 all_identities_check(&mut alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1194 all_identities_check(&mut bob_android_central.transaction, &id, &user_ids, expected_sizes)
1195 .await;
1196 })
1197 },
1198 )
1199 .await
1200 }
1201
1202 #[async_std::test]
1203 #[wasm_bindgen_test]
1204 async fn should_exchange_messages_cross_signed() {
1205 let (alice_android, alice_ios) = (
1206 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1207 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1208 );
1209 let (alicem_android, alicem_ios) = (
1210 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1211 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1212 );
1213 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1214 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1215
1216 let case = TestContext::default_x509();
1217
1218 run_cross_signed_tests_with_client_ids(
1219 case.clone(),
1220 [
1221 [alice_android, "alice_wire", "Alice Smith"],
1222 [alice_ios, "alice_wire", "Alice Smith"],
1223 [bob_android, "bob_wire", "Bob Doe"],
1224 ],
1225 [
1226 [alicem_android, "alice_zeta", "Alice Muller"],
1227 [alicem_ios, "alice_zeta", "Alice Muller"],
1228 [bobt_android, "bob_zeta", "Bob Tables"],
1229 ],
1230 ("wire.com", "zeta.com"),
1231 move |[mut alices_android_central, alices_ios_central, mut bob_android_central],
1232 [
1233 mut alicem_android_central,
1234 mut alicem_ios_central,
1235 mut bobt_android_central,
1236 ]| {
1237 Box::pin(async move {
1238 let id = conversation_id();
1239 alices_ios_central
1240 .transaction
1241 .new_conversation(&id, case.credential_type, case.cfg.clone())
1242 .await
1243 .unwrap();
1244
1245 alices_ios_central
1246 .invite_all(
1247 &case,
1248 &id,
1249 [
1250 &mut alices_android_central,
1251 &mut bob_android_central,
1252 &mut alicem_android_central,
1253 &mut alicem_ios_central,
1254 &mut bobt_android_central,
1255 ],
1256 )
1257 .await
1258 .unwrap();
1259
1260 let nb_members = alices_android_central
1261 .get_conversation_unchecked(&id)
1262 .await
1263 .members()
1264 .len();
1265 assert_eq!(nb_members, 6);
1266
1267 assert_eq!(
1268 alicem_android_central.get_user_id().await,
1269 alicem_ios_central.get_user_id().await
1270 );
1271
1272 bobt_android_central
1274 .try_talk_to(&id, &alices_ios_central)
1275 .await
1276 .unwrap();
1277
1278 bob_android_central.try_talk_to(&id, &alices_ios_central).await.unwrap();
1280 })
1281 },
1282 )
1283 .await;
1284 }
1285 }
1286
1287 mod export_secret {
1288 use super::*;
1289 use crate::MlsErrorKind;
1290 use openmls::prelude::ExportSecretError;
1291
1292 #[apply(all_cred_cipher)]
1293 #[wasm_bindgen_test]
1294 pub async fn can_export_secret_key(case: TestContext) {
1295 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1296 Box::pin(async move {
1297 let id = conversation_id();
1298 alice_central
1299 .transaction
1300 .new_conversation(&id, case.credential_type, case.cfg.clone())
1301 .await
1302 .unwrap();
1303
1304 let key_length = 128;
1305 let result = alice_central
1306 .transaction
1307 .conversation(&id)
1308 .await
1309 .unwrap()
1310 .export_secret_key(key_length)
1311 .await;
1312 assert!(result.is_ok());
1313 assert_eq!(result.unwrap().len(), key_length);
1314 })
1315 })
1316 .await
1317 }
1318
1319 #[apply(all_cred_cipher)]
1320 #[wasm_bindgen_test]
1321 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
1322 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1323 Box::pin(async move {
1324 let id = conversation_id();
1325 alice_central
1326 .transaction
1327 .new_conversation(&id, case.credential_type, case.cfg.clone())
1328 .await
1329 .unwrap();
1330
1331 let result = alice_central
1332 .transaction
1333 .conversation(&id)
1334 .await
1335 .unwrap()
1336 .export_secret_key(usize::MAX)
1337 .await;
1338 let error = result.unwrap_err();
1339 assert!(innermost_source_matches!(
1340 error,
1341 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1342 ));
1343 })
1344 })
1345 .await
1346 }
1347 }
1348
1349 mod get_client_ids {
1350 use super::*;
1351
1352 #[apply(all_cred_cipher)]
1353 #[wasm_bindgen_test]
1354 pub async fn can_get_client_ids(case: TestContext) {
1355 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1356 Box::pin(async move {
1357 let id = conversation_id();
1358 alice_central
1359 .transaction
1360 .new_conversation(&id, case.credential_type, case.cfg.clone())
1361 .await
1362 .unwrap();
1363
1364 assert_eq!(
1365 alice_central
1366 .transaction
1367 .conversation(&id)
1368 .await
1369 .unwrap()
1370 .get_client_ids()
1371 .await
1372 .len(),
1373 1
1374 );
1375
1376 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1377 assert_eq!(
1378 alice_central
1379 .transaction
1380 .conversation(&id)
1381 .await
1382 .unwrap()
1383 .get_client_ids()
1384 .await
1385 .len(),
1386 2
1387 );
1388 })
1389 })
1390 .await
1391 }
1392 }
1393
1394 mod external_sender {
1395 use super::*;
1396
1397 #[apply(all_cred_cipher)]
1398 #[wasm_bindgen_test]
1399 pub async fn should_fetch_ext_sender(case: TestContext) {
1400 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1401 Box::pin(async move {
1402 let id = conversation_id();
1403
1404 let mut cfg = case.cfg.clone();
1406 let external_sender = alice_central.rand_external_sender(&case).await;
1407 cfg.external_senders = vec![external_sender.clone()];
1408
1409 alice_central
1410 .transaction
1411 .new_conversation(&id, case.credential_type, cfg)
1412 .await
1413 .unwrap();
1414
1415 let alice_ext_sender = alice_central
1416 .transaction
1417 .conversation(&id)
1418 .await
1419 .unwrap()
1420 .get_external_sender()
1421 .await
1422 .unwrap();
1423 assert!(!alice_ext_sender.is_empty());
1424 assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1425 })
1426 })
1427 .await
1428 }
1429 }
1430}