core_crypto/mls/conversation/
mod.rs

1//! MLS groups (aka conversation) are the actual entities cementing all the participants in a
2//! conversation.
3//!
4//! This table summarizes what operations are permitted on a group depending its state:
5//! *(PP=pending proposal, PC=pending commit)*
6//!
7//! | can I ?   | 0 PP / 0 PC | 1+ PP / 0 PC | 0 PP / 1 PC | 1+ PP / 1 PC |
8//! |-----------|-------------|--------------|-------------|--------------|
9//! | encrypt   | ✅           | ❌            | ❌           | ❌            |
10//! | handshake | ✅           | ✅            | ❌           | ❌            |
11//! | merge     | ❌           | ❌            | ✅           | ✅            |
12//! | decrypt   | ✅           | ✅            | ✅           | ✅            |
13
14use 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/// The base layer for [Conversation].
64/// The trait is only exposed internally.
65#[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    /// [Session] or [TransactionContext] both implement [HasSessionAndCrypto].
69    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/// The `Conversation` trait provides a set of operations that can be done on
97/// an **immutable** conversation.
98// We keep the super trait internal intentionally, as it is not meant to be used by the public API,
99// hence #[expect(private_bounds)].
100#[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    /// Returns the epoch of a given conversation
105    async fn epoch(&'a self) -> u64 {
106        self.conversation().await.group().epoch().as_u64()
107    }
108
109    /// Returns the ciphersuite of a given conversation
110    async fn ciphersuite(&'a self) -> MlsCiphersuite {
111        self.conversation().await.ciphersuite()
112    }
113
114    /// Derives a new key from the one in the group, to be used elsewhere.
115    ///
116    /// # Arguments
117    /// * `key_length` - the length of the key to be derived. If the value is higher than the
118    ///     bounds of `u16` or the context hash * 255, an error will be returned
119    ///
120    /// # Errors
121    /// OpenMls secret generation error
122    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    /// Exports the clients from a conversation
135    ///
136    /// # Arguments
137    /// * `conversation_id` - the group/conversation id
138    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    /// Returns the raw public key of the single external sender present in this group.
148    /// This should be used to initialize a subconversation
149    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    /// Indicates when to mark a conversation as not verified i.e. when not all its members have a X509
162    /// Credential generated by Wire's end-to-end identity enrollment
163    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    /// From a given conversation, get the identity of the members supplied. Identity is only present for
179    /// members with a Certificate Credential (after turning on end-to-end identity).
180    /// If no member has a x509 certificate, it will return an empty Vec
181    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    /// From a given conversation, get the identity of the users (device holders) supplied.
206    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
207    /// If no member has a x509 certificate, it will return an empty Vec.
208    ///
209    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
210    /// reduce those identities to determine the actual status of a user.
211    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    /// Generate a new [`crate::prelude::HistorySecret`].
241    ///
242    /// This is useful when it's this client's turn to generate a new history client.
243    ///
244    /// The generated secret is cryptographically unrelated to the current CoreCrypto client.
245    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
256/// A unique identifier for a group/conversation. The identifier must be unique within a client.
257pub type ConversationId = Vec<u8>;
258
259/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
260///
261/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
262/// operations that can be done in a group, such as creating proposals and commits.
263/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
264#[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    /// Creates a new group/conversation
275    ///
276    /// # Arguments
277    /// * `id` - group/conversation identifier
278    /// * `author_client` - the client responsible for creating the group
279    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
280    /// * `config` - group configuration
281    /// * `backend` - MLS Provider that will be used to persist the group
282    ///
283    /// # Errors
284    /// Errors can happen from OpenMls or from the KeyStore
285    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    /// Internal API: create a group from an existing conversation. For example by external commit
323    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    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
345    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    /// Group/conversation id
363    pub fn id(&self) -> &ConversationId {
364        &self.id
365    }
366
367    pub(crate) fn group(&self) -> &MlsGroup {
368        &self.group
369    }
370
371    /// Returns all members credentials from the group/conversation
372    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    /// Get actual group members and subtract pending remove proposals
382    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    /// Gather pending remove proposals
400    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    /// Returns all members credentials with their signature public key from the group/conversation
411    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            // Not found
696            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            // Invalid usage
706            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                // Do it a multiple times to avoid WPB-6904 happening again
892                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                // Do it a multiple times to avoid WPB-6904 happening again
945                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        // this test is a duplicate of its counterpart but taking federation into account
1012        // The heavy lifting of cross-signing the certificates is being done by the test utils.
1013        #[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                // Finds both Alice's devices
1052                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                // Finds Bob only device
1066                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                // Finds all devices
1080                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                // Finds both Alice's devices
1129                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                // Finds Bob only device
1143                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                // cross server communication
1205                bobt_android_central
1206                    .try_talk_to(&id, &alices_ios_central)
1207                    .await
1208                    .unwrap();
1209
1210                // same server communication
1211                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                // by default in test no external sender is set. Let's add one
1332                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}