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 async fn generate_history_secret(&'a self) -> Result<crate::prelude::HistorySecret> {
246 let ciphersuite = self.ciphersuite().await;
247 crate::ephemeral::generate_history_secret(ciphersuite)
248 .await
249 .map_err(RecursiveError::root("generating history secret"))
250 .map_err(Into::into)
251 }
252}
253
254impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
255
256pub type ConversationId = Vec<u8>;
258
259#[derive(Debug)]
265#[allow(dead_code)]
266pub struct MlsConversation {
267 pub(crate) id: ConversationId,
268 pub(crate) parent_id: Option<ConversationId>,
269 pub(crate) group: MlsGroup,
270 configuration: MlsConversationConfiguration,
271}
272
273impl MlsConversation {
274 pub async fn create(
286 id: ConversationId,
287 author_client: &Session,
288 creator_credential_type: MlsCredentialType,
289 configuration: MlsConversationConfiguration,
290 backend: &MlsCryptoProvider,
291 ) -> Result<Self> {
292 let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
293 let cb = author_client
294 .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
295 .await
296 .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
297
298 let group = MlsGroup::new_with_group_id(
299 backend,
300 &cb.signature_key,
301 &configuration.as_openmls_default_configuration()?,
302 openmls::prelude::GroupId::from_slice(id.as_slice()),
303 cb.to_mls_credential_with_key(),
304 )
305 .await
306 .map_err(MlsError::wrap("creating group with id"))?;
307
308 let mut conversation = Self {
309 id,
310 group,
311 parent_id: None,
312 configuration,
313 };
314
315 conversation
316 .persist_group_when_changed(&backend.keystore(), true)
317 .await?;
318
319 Ok(conversation)
320 }
321
322 pub(crate) async fn from_mls_group(
324 group: MlsGroup,
325 configuration: MlsConversationConfiguration,
326 backend: &MlsCryptoProvider,
327 ) -> Result<Self> {
328 let id = ConversationId::from(group.group_id().as_slice());
329
330 let mut conversation = Self {
331 id,
332 group,
333 configuration,
334 parent_id: None,
335 };
336
337 conversation
338 .persist_group_when_changed(&backend.keystore(), true)
339 .await?;
340
341 Ok(conversation)
342 }
343
344 pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
346 let group: MlsGroup =
347 core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
348 let id = ConversationId::from(group.group_id().as_slice());
349 let configuration = MlsConversationConfiguration {
350 ciphersuite: group.ciphersuite().into(),
351 ..Default::default()
352 };
353
354 Ok(Self {
355 id,
356 group,
357 parent_id,
358 configuration,
359 })
360 }
361
362 pub fn id(&self) -> &ConversationId {
364 &self.id
365 }
366
367 pub(crate) fn group(&self) -> &MlsGroup {
368 &self.group
369 }
370
371 pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
373 self.group.members().fold(HashMap::new(), |mut acc, kp| {
374 let credential = kp.credential;
375 let id = credential.identity().to_vec();
376 acc.entry(id).or_insert(credential);
377 acc
378 })
379 }
380
381 pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
383 let pending_removals = self.pending_removals();
384 let existing_clients = self
385 .group
386 .members()
387 .filter_map(|kp| {
388 if !pending_removals.contains(&kp.index) {
389 Some(kp.credential.identity().into())
390 } else {
391 trace!(client_index:% = kp.index; "Client is pending removal");
392 None
393 }
394 })
395 .collect::<HashSet<_>>();
396 existing_clients.into_iter().collect()
397 }
398
399 fn pending_removals(&self) -> Vec<LeafNodeIndex> {
401 self.group
402 .pending_proposals()
403 .filter_map(|proposal| match proposal.proposal() {
404 Proposal::Remove(remove) => Some(remove.removed()),
405 _ => None,
406 })
407 .collect::<Vec<_>>()
408 }
409
410 pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
412 self.group.members().fold(HashMap::new(), |mut acc, kp| {
413 let credential = kp.credential;
414 let id = credential.identity().to_vec();
415 let signature_key = SignaturePublicKey::from(kp.signature_key);
416 let credential = CredentialWithKey {
417 credential,
418 signature_key,
419 };
420 acc.entry(id).or_insert(credential);
421 acc
422 })
423 }
424
425 pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
426 if force || self.group.state_changed() == openmls::group::InnerState::Changed {
427 keystore
428 .mls_group_persist(
429 &self.id,
430 &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
431 self.parent_id.as_deref(),
432 )
433 .await
434 .map_err(KeystoreError::wrap("persisting mls group"))?;
435
436 self.group.set_state(openmls::group::InnerState::Persisted);
437 }
438
439 Ok(())
440 }
441
442 pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
443 Ok(self
444 .group
445 .own_leaf_node()
446 .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
447 .credential()
448 .credential_type()
449 .into())
450 }
451
452 pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
453 self.configuration.ciphersuite
454 }
455
456 pub(crate) fn signature_scheme(&self) -> SignatureScheme {
457 self.ciphersuite().signature_algorithm()
458 }
459
460 pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
461 let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
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_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
469 .await
470 .map_err(RecursiveError::mls_client("finding current credential bundle"))
471 .map_err(Into::into)
472 }
473
474 pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
475 let sc = self.ciphersuite().signature_algorithm();
476 let ct = self
477 .own_credential_type()
478 .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
479
480 client
481 .find_most_recent_credential_bundle(sc, ct)
482 .await
483 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
484 .map_err(Into::into)
485 }
486}
487
488#[cfg(test)]
489pub mod test_utils {
490 use super::*;
491
492 impl MlsConversation {
493 pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
494 self.group
495 .members()
496 .map(|m| m.signature_key)
497 .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
498 }
499
500 pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
501 self.group.members().map(|m| m.encryption_key)
502 }
503
504 pub fn extensions(&self) -> &openmls::prelude::Extensions {
505 self.group.export_group_context().extensions()
506 }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::test_utils::*;
514 use wasm_bindgen_test::*;
515
516 wasm_bindgen_test_configure!(run_in_browser);
517
518 #[apply(all_cred_cipher)]
519 #[wasm_bindgen_test]
520 pub async fn create_self_conversation_should_succeed(case: TestContext) {
521 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
522 Box::pin(async move {
523 let id = conversation_id();
524 alice_central
525 .transaction
526 .new_conversation(&id, case.credential_type, case.cfg.clone())
527 .await
528 .unwrap();
529 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
530 assert_eq!(
531 alice_central
532 .get_conversation_unchecked(&id)
533 .await
534 .group
535 .group_id()
536 .as_slice(),
537 id
538 );
539 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
540 let alice_can_send_message = alice_central
541 .transaction
542 .conversation(&id)
543 .await
544 .unwrap()
545 .encrypt_message(b"me")
546 .await;
547 assert!(alice_can_send_message.is_ok());
548 })
549 })
550 .await;
551 }
552
553 #[apply(all_cred_cipher)]
554 #[wasm_bindgen_test]
555 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
556 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
557 Box::pin(async move {
558 let id = conversation_id();
559
560 alice_central
561 .transaction
562 .new_conversation(&id, case.credential_type, case.cfg.clone())
563 .await
564 .unwrap();
565
566 let bob = bob_central.rand_key_package(&case).await;
567 alice_central
568 .transaction
569 .conversation(&id)
570 .await
571 .unwrap()
572 .add_members(vec![bob])
573 .await
574 .unwrap();
575
576 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
577 assert_eq!(
578 alice_central
579 .get_conversation_unchecked(&id)
580 .await
581 .group
582 .group_id()
583 .as_slice(),
584 id
585 );
586 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
587
588 let welcome = alice_central.mls_transport.latest_welcome_message().await;
589 bob_central
590 .transaction
591 .process_welcome_message(welcome.into(), case.custom_cfg())
592 .await
593 .unwrap();
594
595 assert_eq!(
596 bob_central.get_conversation_unchecked(&id).await.id(),
597 alice_central.get_conversation_unchecked(&id).await.id()
598 );
599 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
600 })
601 })
602 .await;
603 }
604
605 #[apply(all_cred_cipher)]
606 #[wasm_bindgen_test]
607 pub async fn create_many_people_conversation(case: TestContext) {
608 use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
609
610 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
611 Box::pin(async move {
612 let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
613 let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
614
615 let id = conversation_id();
616 alice_central
617 .transaction
618 .new_conversation(&id, case.credential_type, case.cfg.clone())
619 .await
620 .unwrap();
621 let bob_and_friends = case.sessions_x509::<GROUP_SAMPLE_SIZE>(Some(x509_test_chain)).await;
622
623 let mut bob_and_friends_kps = vec![];
624 for c in &bob_and_friends {
625 bob_and_friends_kps.push(c.rand_key_package(&case).await);
626 }
627
628 alice_central
629 .transaction
630 .conversation(&id)
631 .await
632 .unwrap()
633 .add_members(bob_and_friends_kps)
634 .await
635 .unwrap();
636 let welcome = alice_central.mls_transport.latest_welcome_message().await;
637
638 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
639 assert_eq!(
640 alice_central
641 .get_conversation_unchecked(&id)
642 .await
643 .group
644 .group_id()
645 .as_slice(),
646 id
647 );
648 assert_eq!(
649 alice_central.get_conversation_unchecked(&id).await.members().len(),
650 1 + GROUP_SAMPLE_SIZE
651 );
652
653 let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
654 for c in bob_and_friends {
655 c.transaction
656 .process_welcome_message(welcome.clone().into(), case.custom_cfg())
657 .await
658 .unwrap();
659 assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
660 bob_and_friends_groups.push(c);
661 }
662
663 assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
664 })
665 })
666 .await;
667 }
668
669 mod wire_identity_getters {
670 use wasm_bindgen_test::*;
671
672 use super::Error;
673 use crate::mls::conversation::Conversation as _;
674 use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
675 use crate::transaction_context::TransactionContext;
676 use crate::{
677 prelude::{DeviceStatus, E2eiConversationState},
678 test_utils::*,
679 };
680
681 wasm_bindgen_test_configure!(run_in_browser);
682
683 async fn all_identities_check<const N: usize>(
684 central: &TransactionContext,
685 id: &ConversationId,
686 user_ids: &[String; N],
687 expected_sizes: [usize; N],
688 ) {
689 let all_identities = central
690 .conversation(id)
691 .await
692 .unwrap()
693 .get_user_identities(user_ids)
694 .await
695 .unwrap();
696 assert_eq!(all_identities.len(), N);
697 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
698 let alice_identities = all_identities.get(user_id).unwrap();
699 assert_eq!(alice_identities.len(), expected_size);
700 }
701 let not_found = central
703 .conversation(id)
704 .await
705 .unwrap()
706 .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
707 .await
708 .unwrap();
709 assert!(not_found.is_empty());
710
711 let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
713 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
714 }
715
716 async fn check_identities_device_status<const N: usize>(
717 central: &TransactionContext,
718 id: &ConversationId,
719 client_ids: &[ClientId; N],
720 name_status: &[(&'static str, DeviceStatus); N],
721 ) {
722 let mut identities = central
723 .conversation(id)
724 .await
725 .unwrap()
726 .get_device_identities(client_ids)
727 .await
728 .unwrap();
729
730 for j in 0..N {
731 let client_identity = identities.remove(
732 identities
733 .iter()
734 .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
735 .unwrap(),
736 );
737 assert_eq!(client_identity.status, name_status[j].1);
738 }
739 assert!(identities.is_empty());
740
741 assert_eq!(
742 central
743 .conversation(id)
744 .await
745 .unwrap()
746 .e2ei_conversation_state()
747 .await
748 .unwrap(),
749 E2eiConversationState::NotVerified
750 );
751 }
752
753 #[async_std::test]
754 #[wasm_bindgen_test]
755 async fn should_read_device_identities() {
756 let case = TestContext::default_x509();
757 run_test_with_client_ids(
758 case.clone(),
759 ["alice_android", "alice_ios"],
760 move |[alice_android_central, alice_ios_central]| {
761 Box::pin(async move {
762 let id = conversation_id();
763 alice_android_central
764 .transaction
765 .new_conversation(&id, case.credential_type, case.cfg.clone())
766 .await
767 .unwrap();
768 alice_android_central
769 .invite_all(&case, &id, [&alice_ios_central])
770 .await
771 .unwrap();
772
773 let (android_id, ios_id) = (
774 alice_android_central.get_client_id().await,
775 alice_ios_central.get_client_id().await,
776 );
777
778 let mut android_ids = alice_android_central
779 .transaction
780 .conversation(&id)
781 .await
782 .unwrap()
783 .get_device_identities(&[android_id.clone(), ios_id.clone()])
784 .await
785 .unwrap();
786 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
787 assert_eq!(android_ids.len(), 2);
788 let mut ios_ids = alice_ios_central
789 .transaction
790 .conversation(&id)
791 .await
792 .unwrap()
793 .get_device_identities(&[android_id.clone(), ios_id.clone()])
794 .await
795 .unwrap();
796 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
797 assert_eq!(ios_ids.len(), 2);
798
799 assert_eq!(android_ids, ios_ids);
800
801 let android_identities = alice_android_central
802 .transaction
803 .conversation(&id)
804 .await
805 .unwrap()
806 .get_device_identities(&[android_id])
807 .await
808 .unwrap();
809 let android_id = android_identities.first().unwrap();
810 assert_eq!(
811 android_id.client_id.as_bytes(),
812 alice_android_central
813 .transaction
814 .client_id()
815 .await
816 .unwrap()
817 .0
818 .as_slice()
819 );
820
821 let ios_identities = alice_android_central
822 .transaction
823 .conversation(&id)
824 .await
825 .unwrap()
826 .get_device_identities(&[ios_id])
827 .await
828 .unwrap();
829 let ios_id = ios_identities.first().unwrap();
830 assert_eq!(
831 ios_id.client_id.as_bytes(),
832 alice_ios_central.transaction.client_id().await.unwrap().0.as_slice()
833 );
834
835 let invalid = alice_android_central
836 .transaction
837 .conversation(&id)
838 .await
839 .unwrap()
840 .get_device_identities(&[])
841 .await;
842 assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
843 })
844 },
845 )
846 .await
847 }
848
849 #[async_std::test]
850 #[wasm_bindgen_test]
851 async fn should_read_revoked_device_cross_signed() {
852 let case = TestContext::default_x509();
853 run_test_with_client_ids_and_revocation(
854 case.clone(),
855 ["alice", "bob", "rupert"],
856 ["john", "dilbert"],
857 &["rupert", "dilbert"],
858 move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
859 Box::pin(async move {
860 let id = conversation_id();
861 alice
862 .transaction
863 .new_conversation(&id, case.credential_type, case.cfg.clone())
864 .await
865 .unwrap();
866 alice
867 .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
868 .await
869 .unwrap();
870
871 let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
872 alice.get_client_id().await,
873 bob.get_client_id().await,
874 rupert.get_client_id().await,
875 dilbert.get_client_id().await,
876 john.get_client_id().await,
877 );
878
879 let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
880 let name_status = [
881 ("alice", DeviceStatus::Valid),
882 ("bob", DeviceStatus::Valid),
883 ("rupert", DeviceStatus::Revoked),
884 ("john", DeviceStatus::Valid),
885 ("dilbert", DeviceStatus::Revoked),
886 ];
887 for _ in 0..2 {
889 check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
890 .await;
891 check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
892 check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
893 .await;
894 check_identities_device_status(&mut john.transaction, &id, &client_ids, &name_status).await;
895 check_identities_device_status(&mut dilbert.transaction, &id, &client_ids, &name_status)
896 .await;
897 }
898 })
899 },
900 )
901 .await
902 }
903
904 #[async_std::test]
905 #[wasm_bindgen_test]
906 async fn should_read_revoked_device() {
907 let case = TestContext::default_x509();
908 run_test_with_client_ids_and_revocation(
909 case.clone(),
910 ["alice", "bob", "rupert"],
911 [],
912 &["rupert"],
913 move |[mut alice, mut bob, mut rupert], []| {
914 Box::pin(async move {
915 let id = conversation_id();
916 alice
917 .transaction
918 .new_conversation(&id, case.credential_type, case.cfg.clone())
919 .await
920 .unwrap();
921 alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
922
923 let (alice_id, bob_id, rupert_id) = (
924 alice.get_client_id().await,
925 bob.get_client_id().await,
926 rupert.get_client_id().await,
927 );
928
929 let client_ids = [alice_id, bob_id, rupert_id];
930 let name_status = [
931 ("alice", DeviceStatus::Valid),
932 ("bob", DeviceStatus::Valid),
933 ("rupert", DeviceStatus::Revoked),
934 ];
935
936 for _ in 0..2 {
938 check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
939 .await;
940 check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
941 check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
942 .await;
943 }
944 })
945 },
946 )
947 .await
948 }
949
950 #[async_std::test]
951 #[wasm_bindgen_test]
952 async fn should_not_fail_when_basic() {
953 let case = TestContext::default();
954 run_test_with_client_ids(
955 case.clone(),
956 ["alice_android", "alice_ios"],
957 move |[alice_android_central, alice_ios_central]| {
958 Box::pin(async move {
959 let id = conversation_id();
960 alice_android_central
961 .transaction
962 .new_conversation(&id, case.credential_type, case.cfg.clone())
963 .await
964 .unwrap();
965 alice_android_central
966 .invite_all(&case, &id, [&alice_ios_central])
967 .await
968 .unwrap();
969
970 let (android_id, ios_id) = (
971 alice_android_central.get_client_id().await,
972 alice_ios_central.get_client_id().await,
973 );
974
975 let mut android_ids = alice_android_central
976 .transaction
977 .conversation(&id)
978 .await
979 .unwrap()
980 .get_device_identities(&[android_id.clone(), ios_id.clone()])
981 .await
982 .unwrap();
983 android_ids.sort();
984
985 let mut ios_ids = alice_ios_central
986 .transaction
987 .conversation(&id)
988 .await
989 .unwrap()
990 .get_device_identities(&[android_id, ios_id])
991 .await
992 .unwrap();
993 ios_ids.sort();
994
995 assert_eq!(ios_ids.len(), 2);
996 assert_eq!(ios_ids, android_ids);
997
998 assert!(ios_ids.iter().all(|i| {
999 matches!(i.credential_type, MlsCredentialType::Basic)
1000 && matches!(i.status, DeviceStatus::Valid)
1001 && i.x509_identity.is_none()
1002 && !i.thumbprint.is_empty()
1003 && !i.client_id.is_empty()
1004 }));
1005 })
1006 },
1007 )
1008 .await
1009 }
1010
1011 #[async_std::test]
1014 #[wasm_bindgen_test]
1015 async fn should_read_users_cross_signed() {
1016 let case = TestContext::default_x509();
1017
1018 let (alice_android, alice_ios) = (
1019 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1020 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1021 );
1022 let (alicem_android, alicem_ios) = (
1023 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1024 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1025 );
1026 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1027 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1028
1029 run_test_with_deterministic_client_ids_and_revocation(
1030 case.clone(),
1031 [
1032 [alice_android, "alice_wire", "Alice Smith"],
1033 [alice_ios, "alice_wire", "Alice Smith"],
1034 [bob_android, "bob_wire", "Bob Doe"],
1035 ],
1036 [
1037 [alicem_android, "alice_zeta", "Alice Muller"],
1038 [alicem_ios, "alice_zeta", "Alice Muller"],
1039 [bobt_android, "bob_zeta", "Bob Tables"],
1040 ],
1041 &[],
1042 move |[alice_android_central, alice_ios_central, bob_android_central],
1043 [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1044 Box::pin(async move {
1045 let id = conversation_id();
1046 alice_android_central
1047 .transaction
1048 .new_conversation(&id, case.credential_type, case.cfg.clone())
1049 .await
1050 .unwrap();
1051 alice_android_central
1052 .invite_all(
1053 &case,
1054 &id,
1055 [
1056 &alice_ios_central,
1057 &bob_android_central,
1058 &bobt_android_central,
1059 &alicem_ios_central,
1060 &alicem_android_central,
1061 ],
1062 )
1063 .await
1064 .unwrap();
1065
1066 let nb_members = alice_android_central
1067 .get_conversation_unchecked(&id)
1068 .await
1069 .members()
1070 .len();
1071 assert_eq!(nb_members, 6);
1072
1073 assert_eq!(
1074 alice_android_central.get_user_id().await,
1075 alice_ios_central.get_user_id().await
1076 );
1077
1078 let alicem_user_id = alicem_ios_central.get_user_id().await;
1079 let bobt_user_id = bobt_android_central.get_user_id().await;
1080
1081 let alice_user_id = alice_android_central.get_user_id().await;
1083 let alice_identities = alice_android_central
1084 .transaction
1085 .conversation(&id)
1086 .await
1087 .unwrap()
1088 .get_user_identities(&[alice_user_id.clone()])
1089 .await
1090 .unwrap();
1091 assert_eq!(alice_identities.len(), 1);
1092 let identities = alice_identities.get(&alice_user_id).unwrap();
1093 assert_eq!(identities.len(), 2);
1094
1095 let bob_user_id = bob_android_central.get_user_id().await;
1097 let bob_identities = alice_android_central
1098 .transaction
1099 .conversation(&id)
1100 .await
1101 .unwrap()
1102 .get_user_identities(&[bob_user_id.clone()])
1103 .await
1104 .unwrap();
1105 assert_eq!(bob_identities.len(), 1);
1106 let identities = bob_identities.get(&bob_user_id).unwrap();
1107 assert_eq!(identities.len(), 1);
1108
1109 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1111 let expected_sizes = [2, 1, 2, 1];
1112
1113 all_identities_check(&alice_android_central.transaction, &id, &user_ids, expected_sizes).await;
1114 all_identities_check(&alicem_android_central.transaction, &id, &user_ids, expected_sizes).await;
1115 all_identities_check(&alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1116 all_identities_check(&alicem_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1117 all_identities_check(&bob_android_central.transaction, &id, &user_ids, expected_sizes).await;
1118 all_identities_check(&bobt_android_central.transaction, &id, &user_ids, expected_sizes).await;
1119 })
1120 },
1121 )
1122 .await
1123 }
1124
1125 #[async_std::test]
1126 #[wasm_bindgen_test]
1127 async fn should_read_users() {
1128 let case = TestContext::default_x509();
1129
1130 let (alice_android, alice_ios) = (
1131 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1132 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1133 );
1134 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1135
1136 run_test_with_deterministic_client_ids(
1137 case.clone(),
1138 [
1139 [alice_android, "alice_wire", "Alice Smith"],
1140 [alice_ios, "alice_wire", "Alice Smith"],
1141 [bob_android, "bob_wire", "Bob Doe"],
1142 ],
1143 move |[
1144 mut alice_android_central,
1145 mut alice_ios_central,
1146 mut bob_android_central,
1147 ]| {
1148 Box::pin(async move {
1149 let id = conversation_id();
1150 alice_android_central
1151 .transaction
1152 .new_conversation(&id, case.credential_type, case.cfg.clone())
1153 .await
1154 .unwrap();
1155 alice_android_central
1156 .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1157 .await
1158 .unwrap();
1159
1160 let nb_members = alice_android_central
1161 .get_conversation_unchecked(&id)
1162 .await
1163 .members()
1164 .len();
1165 assert_eq!(nb_members, 3);
1166
1167 assert_eq!(
1168 alice_android_central.get_user_id().await,
1169 alice_ios_central.get_user_id().await
1170 );
1171
1172 let alice_user_id = alice_android_central.get_user_id().await;
1174 let alice_identities = alice_android_central
1175 .transaction
1176 .conversation(&id)
1177 .await
1178 .unwrap()
1179 .get_user_identities(&[alice_user_id.clone()])
1180 .await
1181 .unwrap();
1182 assert_eq!(alice_identities.len(), 1);
1183 let identities = alice_identities.get(&alice_user_id).unwrap();
1184 assert_eq!(identities.len(), 2);
1185
1186 let bob_user_id = bob_android_central.get_user_id().await;
1188 let bob_identities = alice_android_central
1189 .transaction
1190 .conversation(&id)
1191 .await
1192 .unwrap()
1193 .get_user_identities(&[bob_user_id.clone()])
1194 .await
1195 .unwrap();
1196 assert_eq!(bob_identities.len(), 1);
1197 let identities = bob_identities.get(&bob_user_id).unwrap();
1198 assert_eq!(identities.len(), 1);
1199
1200 let user_ids = [alice_user_id, bob_user_id];
1201 let expected_sizes = [2, 1];
1202
1203 all_identities_check(&mut alice_android_central.transaction, &id, &user_ids, expected_sizes)
1204 .await;
1205 all_identities_check(&mut alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1206 all_identities_check(&mut bob_android_central.transaction, &id, &user_ids, expected_sizes)
1207 .await;
1208 })
1209 },
1210 )
1211 .await
1212 }
1213
1214 #[async_std::test]
1215 #[wasm_bindgen_test]
1216 async fn should_exchange_messages_cross_signed() {
1217 let (alice_android, alice_ios) = (
1218 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1219 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1220 );
1221 let (alicem_android, alicem_ios) = (
1222 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1223 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1224 );
1225 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1226 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1227
1228 let case = TestContext::default_x509();
1229
1230 run_cross_signed_tests_with_client_ids(
1231 case.clone(),
1232 [
1233 [alice_android, "alice_wire", "Alice Smith"],
1234 [alice_ios, "alice_wire", "Alice Smith"],
1235 [bob_android, "bob_wire", "Bob Doe"],
1236 ],
1237 [
1238 [alicem_android, "alice_zeta", "Alice Muller"],
1239 [alicem_ios, "alice_zeta", "Alice Muller"],
1240 [bobt_android, "bob_zeta", "Bob Tables"],
1241 ],
1242 ("wire.com", "zeta.com"),
1243 move |[alices_android_central, alices_ios_central, bob_android_central],
1244 [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1245 Box::pin(async move {
1246 let id = conversation_id();
1247 alices_ios_central
1248 .transaction
1249 .new_conversation(&id, case.credential_type, case.cfg.clone())
1250 .await
1251 .unwrap();
1252
1253 alices_ios_central
1254 .invite_all(
1255 &case,
1256 &id,
1257 [
1258 &alices_android_central,
1259 &bob_android_central,
1260 &alicem_android_central,
1261 &alicem_ios_central,
1262 &bobt_android_central,
1263 ],
1264 )
1265 .await
1266 .unwrap();
1267
1268 let nb_members = alices_android_central
1269 .get_conversation_unchecked(&id)
1270 .await
1271 .members()
1272 .len();
1273 assert_eq!(nb_members, 6);
1274
1275 assert_eq!(
1276 alicem_android_central.get_user_id().await,
1277 alicem_ios_central.get_user_id().await
1278 );
1279
1280 bobt_android_central
1282 .try_talk_to(&id, &alices_ios_central)
1283 .await
1284 .unwrap();
1285
1286 bob_android_central.try_talk_to(&id, &alices_ios_central).await.unwrap();
1288 })
1289 },
1290 )
1291 .await;
1292 }
1293 }
1294
1295 mod export_secret {
1296 use super::*;
1297 use crate::MlsErrorKind;
1298 use openmls::prelude::ExportSecretError;
1299
1300 #[apply(all_cred_cipher)]
1301 #[wasm_bindgen_test]
1302 pub async fn can_export_secret_key(case: TestContext) {
1303 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1304 Box::pin(async move {
1305 let id = conversation_id();
1306 alice_central
1307 .transaction
1308 .new_conversation(&id, case.credential_type, case.cfg.clone())
1309 .await
1310 .unwrap();
1311
1312 let key_length = 128;
1313 let result = alice_central
1314 .transaction
1315 .conversation(&id)
1316 .await
1317 .unwrap()
1318 .export_secret_key(key_length)
1319 .await;
1320 assert!(result.is_ok());
1321 assert_eq!(result.unwrap().len(), key_length);
1322 })
1323 })
1324 .await
1325 }
1326
1327 #[apply(all_cred_cipher)]
1328 #[wasm_bindgen_test]
1329 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
1330 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1331 Box::pin(async move {
1332 let id = conversation_id();
1333 alice_central
1334 .transaction
1335 .new_conversation(&id, case.credential_type, case.cfg.clone())
1336 .await
1337 .unwrap();
1338
1339 let result = alice_central
1340 .transaction
1341 .conversation(&id)
1342 .await
1343 .unwrap()
1344 .export_secret_key(usize::MAX)
1345 .await;
1346 let error = result.unwrap_err();
1347 assert!(innermost_source_matches!(
1348 error,
1349 MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1350 ));
1351 })
1352 })
1353 .await
1354 }
1355 }
1356
1357 mod get_client_ids {
1358 use super::*;
1359
1360 #[apply(all_cred_cipher)]
1361 #[wasm_bindgen_test]
1362 pub async fn can_get_client_ids(case: TestContext) {
1363 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1364 Box::pin(async move {
1365 let id = conversation_id();
1366 alice_central
1367 .transaction
1368 .new_conversation(&id, case.credential_type, case.cfg.clone())
1369 .await
1370 .unwrap();
1371
1372 assert_eq!(
1373 alice_central
1374 .transaction
1375 .conversation(&id)
1376 .await
1377 .unwrap()
1378 .get_client_ids()
1379 .await
1380 .len(),
1381 1
1382 );
1383
1384 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1385 assert_eq!(
1386 alice_central
1387 .transaction
1388 .conversation(&id)
1389 .await
1390 .unwrap()
1391 .get_client_ids()
1392 .await
1393 .len(),
1394 2
1395 );
1396 })
1397 })
1398 .await
1399 }
1400 }
1401
1402 mod external_sender {
1403 use super::*;
1404
1405 #[apply(all_cred_cipher)]
1406 #[wasm_bindgen_test]
1407 pub async fn should_fetch_ext_sender(case: TestContext) {
1408 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1409 Box::pin(async move {
1410 let id = conversation_id();
1411
1412 let mut cfg = case.cfg.clone();
1414 let external_sender = alice_central.rand_external_sender(&case).await;
1415 cfg.external_senders = vec![external_sender.clone()];
1416
1417 alice_central
1418 .transaction
1419 .new_conversation(&id, case.credential_type, cfg)
1420 .await
1421 .unwrap();
1422
1423 let alice_ext_sender = alice_central
1424 .transaction
1425 .conversation(&id)
1426 .await
1427 .unwrap()
1428 .get_external_sender()
1429 .await
1430 .unwrap();
1431 assert!(!alice_ext_sender.is_empty());
1432 assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1433 })
1434 })
1435 .await
1436 }
1437 }
1438}