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