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;
44pub(crate) mod merge;
45mod orphan_welcome;
46mod own_commit;
47pub(crate) mod pending_conversation;
48pub(crate) mod proposal;
49mod renew;
50pub(crate) mod welcome;
51mod wipe;
52
53use crate::mls::HasSessionAndCrypto;
54use crate::mls::credential::ext::CredentialExt as _;
55use crate::prelude::user_id::UserId;
56pub use conversation_guard::ConversationGuard;
57pub use error::{Error, Result};
58pub use immutable_conversation::ImmutableConversation;
59
60use super::credential::CredentialBundle;
61
62/// The base layer for [Conversation].
63/// The trait is only exposed internally.
64#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
65#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
66pub(crate) trait ConversationWithMls<'a> {
67    /// [Session] or [TransactionContext] both implement [HasSessionAndCrypto].
68    type Context: HasSessionAndCrypto;
69
70    type Conversation: Deref<Target = MlsConversation> + Send;
71
72    async fn context(&self) -> Result<Self::Context>;
73
74    async fn conversation(&'a self) -> Self::Conversation;
75
76    async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
77        self.context()
78            .await?
79            .crypto_provider()
80            .await
81            .map_err(RecursiveError::mls("getting mls provider"))
82            .map_err(Into::into)
83    }
84
85    async fn session(&self) -> Result<Session> {
86        self.context()
87            .await?
88            .session()
89            .await
90            .map_err(RecursiveError::mls("getting mls client"))
91            .map_err(Into::into)
92    }
93}
94
95/// The `Conversation` trait provides a set of operations that can be done on
96/// an **immutable** conversation.
97// We keep the super trait internal intentionally, as it is not meant to be used by the public API,
98// hence #[expect(private_bounds)].
99#[expect(private_bounds)]
100#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
101#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
102pub trait Conversation<'a>: ConversationWithMls<'a> {
103    /// Returns the epoch of a given conversation
104    async fn epoch(&'a self) -> u64 {
105        self.conversation().await.group().epoch().as_u64()
106    }
107
108    /// Returns the ciphersuite of a given conversation
109    async fn ciphersuite(&'a self) -> MlsCiphersuite {
110        self.conversation().await.ciphersuite()
111    }
112
113    /// Derives a new key from the one in the group, to be used elsewhere.
114    ///
115    /// # Arguments
116    /// * `key_length` - the length of the key to be derived. If the value is higher than the
117    ///     bounds of `u16` or the context hash * 255, an error will be returned
118    ///
119    /// # Errors
120    /// OpenMls secret generation error
121    async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
122        const EXPORTER_LABEL: &str = "exporter";
123        const EXPORTER_CONTEXT: &[u8] = &[];
124        let backend = self.crypto_provider().await?;
125        let inner = self.conversation().await;
126        inner
127            .group()
128            .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
129            .map_err(MlsError::wrap("exporting secret key"))
130            .map_err(Into::into)
131    }
132
133    /// Exports the clients from a conversation
134    ///
135    /// # Arguments
136    /// * `conversation_id` - the group/conversation id
137    async fn get_client_ids(&'a self) -> Vec<ClientId> {
138        let inner = self.conversation().await;
139        inner
140            .group()
141            .members()
142            .map(|kp| ClientId::from(kp.credential.identity()))
143            .collect()
144    }
145
146    /// Returns the raw public key of the single external sender present in this group.
147    /// This should be used to initialize a subconversation
148    async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
149        let inner = self.conversation().await;
150        let ext_senders = inner
151            .group()
152            .group_context_extensions()
153            .external_senders()
154            .ok_or(Error::MissingExternalSenderExtension)?;
155        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
156        let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
157        Ok(ext_sender_public_key)
158    }
159
160    /// Indicates when to mark a conversation as not verified i.e. when not all its members have a X509
161    /// Credential generated by Wire's end-to-end identity enrollment
162    async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
163        let backend = self.crypto_provider().await?;
164        let authentication_service = backend.authentication_service();
165        authentication_service.refresh_time_of_interest().await;
166        let inner = self.conversation().await;
167        let state = Session::compute_conversation_state(
168            inner.ciphersuite(),
169            inner.group.members_credentials(),
170            MlsCredentialType::X509,
171            authentication_service.borrow().await.as_ref(),
172        )
173        .await;
174        Ok(state)
175    }
176
177    /// From a given conversation, get the identity of the members supplied. Identity is only present for
178    /// members with a Certificate Credential (after turning on end-to-end identity).
179    /// If no member has a x509 certificate, it will return an empty Vec
180    async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
181        if device_ids.is_empty() {
182            return Err(Error::CallerError(
183                "This function accepts a list of IDs as a parameter, but that list was empty.",
184            ));
185        }
186        let mls_provider = self.crypto_provider().await?;
187        let auth_service = mls_provider.authentication_service();
188        auth_service.refresh_time_of_interest().await;
189        let auth_service = auth_service.borrow().await;
190        let env = auth_service.as_ref();
191        let conversation = self.conversation().await;
192        conversation
193            .members_with_key()
194            .into_iter()
195            .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
196            .map(|(_, c)| {
197                c.extract_identity(conversation.ciphersuite(), env)
198                    .map_err(RecursiveError::mls_credential("extracting identity"))
199            })
200            .collect::<Result<Vec<_>, _>>()
201            .map_err(Into::into)
202    }
203
204    /// From a given conversation, get the identity of the users (device holders) supplied.
205    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
206    /// If no member has a x509 certificate, it will return an empty Vec.
207    ///
208    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
209    /// reduce those identities to determine the actual status of a user.
210    async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
211        if user_ids.is_empty() {
212            return Err(Error::CallerError(
213                "This function accepts a list of IDs as a parameter, but that list was empty.",
214            ));
215        }
216        let mls_provider = self.crypto_provider().await?;
217        let auth_service = mls_provider.authentication_service();
218        auth_service.refresh_time_of_interest().await;
219        let auth_service = auth_service.borrow().await;
220        let env = auth_service.as_ref();
221        let conversation = self.conversation().await;
222        let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
223
224        conversation
225            .members_with_key()
226            .iter()
227            .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
228            .filter(|(uid, _)| user_ids.contains(uid))
229            .map(|(uid, c)| {
230                let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
231                let identity = c
232                    .extract_identity(conversation.ciphersuite(), env)
233                    .map_err(RecursiveError::mls_credential("extracting identity"))?;
234                Ok((uid, identity))
235            })
236            .process_results(|iter| iter.into_group_map())
237    }
238
239    /// Generate a new [`crate::prelude::HistorySecret`].
240    ///
241    /// This is useful when it's this client's turn to generate a new history client.
242    ///
243    /// The generated secret is cryptographically unrelated to the current CoreCrypto client.
244    async fn generate_history_secret(&'a self) -> Result<crate::prelude::HistorySecret> {
245        let ciphersuite = self.ciphersuite().await;
246        crate::ephemeral::generate_history_secret(ciphersuite)
247            .await
248            .map_err(RecursiveError::root("generating history secret"))
249            .map_err(Into::into)
250    }
251
252    /// Check if history sharing is enabled, i.e., if any of the conversation members have a [ClientId] starting
253    /// with [crate::prelude::HISTORY_CLIENT_ID_PREFIX].
254    async fn is_history_sharing_enabled(&'a self) -> bool {
255        self.get_client_ids()
256            .await
257            .iter()
258            .any(|client_id| client_id.starts_with(crate::ephemeral::HISTORY_CLIENT_ID_PREFIX.as_bytes()))
259    }
260}
261
262impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
263
264/// A unique identifier for a group/conversation. The identifier must be unique within a client.
265pub type ConversationId = Vec<u8>;
266
267/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
268///
269/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
270/// operations that can be done in a group, such as creating proposals and commits.
271/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
272#[derive(Debug)]
273#[allow(dead_code)]
274pub struct MlsConversation {
275    pub(crate) id: ConversationId,
276    pub(crate) parent_id: Option<ConversationId>,
277    pub(crate) group: MlsGroup,
278    configuration: MlsConversationConfiguration,
279}
280
281impl MlsConversation {
282    /// Creates a new group/conversation
283    ///
284    /// # Arguments
285    /// * `id` - group/conversation identifier
286    /// * `author_client` - the client responsible for creating the group
287    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
288    /// * `config` - group configuration
289    /// * `backend` - MLS Provider that will be used to persist the group
290    ///
291    /// # Errors
292    /// Errors can happen from OpenMls or from the KeyStore
293    pub async fn create(
294        id: ConversationId,
295        author_client: &Session,
296        creator_credential_type: MlsCredentialType,
297        configuration: MlsConversationConfiguration,
298        backend: &MlsCryptoProvider,
299    ) -> Result<Self> {
300        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
301        let cb = author_client
302            .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
303            .await
304            .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
305
306        let group = MlsGroup::new_with_group_id(
307            backend,
308            &cb.signature_key,
309            &configuration.as_openmls_default_configuration()?,
310            openmls::prelude::GroupId::from_slice(id.as_slice()),
311            cb.to_mls_credential_with_key(),
312        )
313        .await
314        .map_err(MlsError::wrap("creating group with id"))?;
315
316        let mut conversation = Self {
317            id,
318            group,
319            parent_id: None,
320            configuration,
321        };
322
323        conversation
324            .persist_group_when_changed(&backend.keystore(), true)
325            .await?;
326
327        Ok(conversation)
328    }
329
330    /// Internal API: create a group from an existing conversation. For example by external commit
331    pub(crate) async fn from_mls_group(
332        group: MlsGroup,
333        configuration: MlsConversationConfiguration,
334        backend: &MlsCryptoProvider,
335    ) -> Result<Self> {
336        let id = ConversationId::from(group.group_id().as_slice());
337
338        let mut conversation = Self {
339            id,
340            group,
341            configuration,
342            parent_id: None,
343        };
344
345        conversation
346            .persist_group_when_changed(&backend.keystore(), true)
347            .await?;
348
349        Ok(conversation)
350    }
351
352    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
353    pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
354        let group: MlsGroup =
355            core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
356        let id = ConversationId::from(group.group_id().as_slice());
357        let configuration = MlsConversationConfiguration {
358            ciphersuite: group.ciphersuite().into(),
359            ..Default::default()
360        };
361
362        Ok(Self {
363            id,
364            group,
365            parent_id,
366            configuration,
367        })
368    }
369
370    /// Group/conversation id
371    pub fn id(&self) -> &ConversationId {
372        &self.id
373    }
374
375    pub(crate) fn group(&self) -> &MlsGroup {
376        &self.group
377    }
378
379    /// Returns all members credentials from the group/conversation
380    pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
381        self.group.members().fold(HashMap::new(), |mut acc, kp| {
382            let credential = kp.credential;
383            let id = credential.identity().to_vec();
384            acc.entry(id).or_insert(credential);
385            acc
386        })
387    }
388
389    /// Get actual group members and subtract pending remove proposals
390    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
391        let pending_removals = self.pending_removals();
392        let existing_clients = self
393            .group
394            .members()
395            .filter_map(|kp| {
396                if !pending_removals.contains(&kp.index) {
397                    Some(kp.credential.identity().into())
398                } else {
399                    trace!(client_index:% = kp.index; "Client is pending removal");
400                    None
401                }
402            })
403            .collect::<HashSet<_>>();
404        existing_clients.into_iter().collect()
405    }
406
407    /// Gather pending remove proposals
408    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
409        self.group
410            .pending_proposals()
411            .filter_map(|proposal| match proposal.proposal() {
412                Proposal::Remove(remove) => Some(remove.removed()),
413                _ => None,
414            })
415            .collect::<Vec<_>>()
416    }
417
418    /// Returns all members credentials with their signature public key from the group/conversation
419    pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
420        self.group.members().fold(HashMap::new(), |mut acc, kp| {
421            let credential = kp.credential;
422            let id = credential.identity().to_vec();
423            let signature_key = SignaturePublicKey::from(kp.signature_key);
424            let credential = CredentialWithKey {
425                credential,
426                signature_key,
427            };
428            acc.entry(id).or_insert(credential);
429            acc
430        })
431    }
432
433    pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
434        if force || self.group.state_changed() == openmls::group::InnerState::Changed {
435            keystore
436                .mls_group_persist(
437                    &self.id,
438                    &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
439                    self.parent_id.as_deref(),
440                )
441                .await
442                .map_err(KeystoreError::wrap("persisting mls group"))?;
443
444            self.group.set_state(openmls::group::InnerState::Persisted);
445        }
446
447        Ok(())
448    }
449
450    pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
451        Ok(self
452            .group
453            .own_leaf_node()
454            .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
455            .credential()
456            .credential_type()
457            .into())
458    }
459
460    pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
461        self.configuration.ciphersuite
462    }
463
464    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
465        self.ciphersuite().signature_algorithm()
466    }
467
468    pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
469        let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
470        let sc = self.ciphersuite().signature_algorithm();
471        let ct = self
472            .own_credential_type()
473            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
474
475        client
476            .find_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
477            .await
478            .map_err(RecursiveError::mls_client("finding current credential bundle"))
479            .map_err(Into::into)
480    }
481
482    pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
483        let sc = self.ciphersuite().signature_algorithm();
484        let ct = self
485            .own_credential_type()
486            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
487
488        client
489            .find_most_recent_credential_bundle(sc, ct)
490            .await
491            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
492            .map_err(Into::into)
493    }
494}
495
496#[cfg(test)]
497pub mod test_utils {
498    use super::*;
499
500    impl MlsConversation {
501        pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
502            self.group
503                .members()
504                .map(|m| m.signature_key)
505                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
506        }
507
508        pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
509            self.group.members().map(|m| m.encryption_key)
510        }
511
512        pub fn extensions(&self) -> &openmls::prelude::Extensions {
513            self.group.export_group_context().extensions()
514        }
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use crate::test_utils::*;
522
523    #[apply(all_cred_cipher)]
524    pub async fn create_self_conversation_should_succeed(case: TestContext) {
525        let [alice] = case.sessions().await;
526        Box::pin(async move {
527            let conversation = case.create_conversation([&alice]).await;
528            assert_eq!(1, conversation.member_count().await);
529            let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
530            assert!(alice_can_send_message.is_ok());
531        })
532        .await;
533    }
534
535    #[apply(all_cred_cipher)]
536    pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
537        let [alice, bob] = case.sessions().await;
538        Box::pin(async move {
539            let conversation = case.create_conversation([&alice, &bob]).await;
540            assert_eq!(2, conversation.member_count().await);
541            assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
542        })
543        .await;
544    }
545
546    #[apply(all_cred_cipher)]
547    pub async fn create_many_people_conversation(case: TestContext) {
548        const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
549        let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
550        Box::pin(async move {
551            let alice = &alice_and_friends[0];
552            let conversation = case.create_conversation([alice]).await;
553
554            let bob_and_friends = &alice_and_friends[1..];
555            let conversation = conversation.invite_notify(bob_and_friends).await;
556
557            assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
558            assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
559        })
560        .await;
561    }
562
563    mod wire_identity_getters {
564        use super::Error;
565        use crate::mls::conversation::Conversation;
566        use crate::prelude::{ClientId, MlsCredentialType};
567        use crate::{
568            prelude::{DeviceStatus, E2eiConversationState},
569            test_utils::*,
570        };
571
572        async fn all_identities_check<'a, C, const N: usize>(
573            conversation: &'a C,
574            user_ids: &[String; N],
575            expected_sizes: [usize; N],
576        ) where
577            C: Conversation<'a> + Sync,
578        {
579            let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
580            assert_eq!(all_identities.len(), N);
581            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
582                let alice_identities = all_identities.get(user_id).unwrap();
583                assert_eq!(alice_identities.len(), expected_size);
584            }
585            // Not found
586            let not_found = conversation
587                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
588                .await
589                .unwrap();
590            assert!(not_found.is_empty());
591
592            // Invalid usage
593            let invalid = conversation.get_user_identities(&[]).await;
594            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
595        }
596
597        async fn check_identities_device_status<'a, C, const N: usize>(
598            conversation: &'a C,
599            client_ids: &[ClientId; N],
600            name_status: &[(impl ToString, DeviceStatus); N],
601        ) where
602            C: Conversation<'a> + Sync,
603        {
604            let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
605
606            for (user_name, status) in name_status.iter() {
607                let client_identity = identities.remove(
608                    identities
609                        .iter()
610                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
611                        .unwrap(),
612                );
613                assert_eq!(client_identity.status, *status);
614            }
615            assert!(identities.is_empty());
616
617            assert_eq!(
618                conversation.e2ei_conversation_state().await.unwrap(),
619                E2eiConversationState::NotVerified
620            );
621        }
622
623        #[async_std::test]
624        async fn should_read_device_identities() {
625            let case = TestContext::default_x509();
626
627            let [alice_android, alice_ios] = case.sessions().await;
628            Box::pin(async move {
629                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
630
631                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
632
633                let mut android_ids = conversation
634                    .guard()
635                    .await
636                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
637                    .await
638                    .unwrap();
639                android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
640                assert_eq!(android_ids.len(), 2);
641                let mut ios_ids = conversation
642                    .guard_of(&alice_ios)
643                    .await
644                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
645                    .await
646                    .unwrap();
647                ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
648                assert_eq!(ios_ids.len(), 2);
649
650                assert_eq!(android_ids, ios_ids);
651
652                let android_identities = conversation
653                    .guard()
654                    .await
655                    .get_device_identities(&[android_id])
656                    .await
657                    .unwrap();
658                let android_id = android_identities.first().unwrap();
659                assert_eq!(
660                    android_id.client_id.as_bytes(),
661                    alice_android.transaction.client_id().await.unwrap().0.as_slice()
662                );
663
664                let ios_identities = conversation
665                    .guard()
666                    .await
667                    .get_device_identities(&[ios_id])
668                    .await
669                    .unwrap();
670                let ios_id = ios_identities.first().unwrap();
671                assert_eq!(
672                    ios_id.client_id.as_bytes(),
673                    alice_ios.transaction.client_id().await.unwrap().0.as_slice()
674                );
675
676                let invalid = conversation.guard().await.get_device_identities(&[]).await;
677                assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
678            })
679            .await
680        }
681
682        #[async_std::test]
683        async fn should_read_revoked_device_cross_signed() {
684            let case = TestContext::default_x509();
685            let alice_user_id = uuid::Uuid::new_v4();
686            let bob_user_id = uuid::Uuid::new_v4();
687            let rupert_user_id = uuid::Uuid::new_v4();
688            let john_user_id = uuid::Uuid::new_v4();
689            let dilbert_user_id = uuid::Uuid::new_v4();
690
691            let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
692            let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
693            let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
694            let [john_client_id] = case.x509_client_ids_for_user(&john_user_id);
695            let [dilbert_client_id] = case.x509_client_ids_for_user(&dilbert_user_id);
696
697            let sessions = case
698                .sessions_x509_cross_signed_with_client_ids_and_revocation(
699                    [alice_client_id, bob_client_id, rupert_client_id],
700                    [john_client_id, dilbert_client_id],
701                    &[dilbert_user_id.to_string(), rupert_user_id.to_string()],
702                )
703                .await;
704
705            Box::pin(async move {
706                let ([alice, bob, rupert], [john, dilbert]) = &sessions;
707                let mut sessions = sessions.0.iter().chain(sessions.1.iter());
708                let conversation = case.create_conversation(&mut sessions).await;
709
710                let (alice_id, bob_id, rupert_id, john_id, dilbert_id) = (
711                    alice.get_client_id().await,
712                    bob.get_client_id().await,
713                    rupert.get_client_id().await,
714                    john.get_client_id().await,
715                    dilbert.get_client_id().await,
716                );
717
718                let client_ids = [alice_id, bob_id, rupert_id, john_id, dilbert_id];
719                let name_status = [
720                    (alice_user_id, DeviceStatus::Valid),
721                    (bob_user_id, DeviceStatus::Valid),
722                    (rupert_user_id, DeviceStatus::Revoked),
723                    (john_user_id, DeviceStatus::Valid),
724                    (dilbert_user_id, DeviceStatus::Revoked),
725                ];
726                // Do it a multiple times to avoid WPB-6904 happening again
727                for _ in 0..2 {
728                    for session in sessions.clone() {
729                        let conversation = conversation.guard_of(session).await;
730                        check_identities_device_status(&conversation, &client_ids, &name_status).await;
731                    }
732                }
733            })
734            .await
735        }
736
737        #[async_std::test]
738        async fn should_read_revoked_device() {
739            let case = TestContext::default_x509();
740            let rupert_user_id = uuid::Uuid::new_v4();
741            let bob_user_id = uuid::Uuid::new_v4();
742            let alice_user_id = uuid::Uuid::new_v4();
743
744            let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
745            let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
746            let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
747
748            let sessions = case
749                .sessions_x509_with_client_ids_and_revocation(
750                    [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
751                    &[rupert_user_id.to_string()],
752                )
753                .await;
754
755            Box::pin(async move {
756                let [alice, bob, rupert] = &sessions;
757                let conversation = case.create_conversation(&sessions).await;
758
759                let (alice_id, bob_id, rupert_id) = (
760                    alice.get_client_id().await,
761                    bob.get_client_id().await,
762                    rupert.get_client_id().await,
763                );
764
765                let client_ids = [alice_id, bob_id, rupert_id];
766                let name_status = [
767                    (alice_user_id, DeviceStatus::Valid),
768                    (bob_user_id, DeviceStatus::Valid),
769                    (rupert_user_id, DeviceStatus::Revoked),
770                ];
771
772                // Do it a multiple times to avoid WPB-6904 happening again
773                for _ in 0..2 {
774                    for session in sessions.iter() {
775                        let conversation = conversation.guard_of(session).await;
776                        check_identities_device_status(&conversation, &client_ids, &name_status).await;
777                    }
778                }
779            })
780            .await
781        }
782
783        #[async_std::test]
784        async fn should_not_fail_when_basic() {
785            let case = TestContext::default();
786
787            let [alice_android, alice_ios] = case.sessions().await;
788            Box::pin(async move {
789                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
790
791                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
792
793                let mut android_ids = conversation
794                    .guard()
795                    .await
796                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
797                    .await
798                    .unwrap();
799                android_ids.sort();
800
801                let mut ios_ids = conversation
802                    .guard_of(&alice_ios)
803                    .await
804                    .get_device_identities(&[android_id, ios_id])
805                    .await
806                    .unwrap();
807                ios_ids.sort();
808
809                assert_eq!(ios_ids.len(), 2);
810                assert_eq!(ios_ids, android_ids);
811
812                assert!(ios_ids.iter().all(|i| {
813                    matches!(i.credential_type, MlsCredentialType::Basic)
814                        && matches!(i.status, DeviceStatus::Valid)
815                        && i.x509_identity.is_none()
816                        && !i.thumbprint.is_empty()
817                        && !i.client_id.is_empty()
818                }));
819            })
820            .await
821        }
822
823        // this test is a duplicate of its counterpart but taking federation into account
824        // The heavy lifting of cross-signing the certificates is being done by the test utils.
825        #[async_std::test]
826        async fn should_read_users_cross_signed() {
827            let case = TestContext::default_x509();
828            let [alice_1_id, alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
829            let [federated_alice_1_id, federated_alice_2_id] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
830            let [bob_id, federated_bob_id] = case.x509_client_ids();
831
832            let ([alice_1, alice_2, bob], [federated_alice_1, federated_alice_2, federated_bob]) = case
833                .sessions_x509_cross_signed_with_client_ids(
834                    [alice_1_id, alice_2_id, bob_id],
835                    [federated_alice_1_id, federated_alice_2_id, federated_bob_id],
836                )
837                .await;
838            Box::pin(async move {
839                let sessions = [
840                    &alice_1,
841                    &alice_2,
842                    &bob,
843                    &federated_bob,
844                    &federated_alice_1,
845                    &federated_alice_2,
846                ];
847                let conversation = case.create_conversation(sessions).await;
848
849                let nb_members = conversation.member_count().await;
850                assert_eq!(nb_members, 6);
851                let conversation_guard = conversation.guard().await;
852
853                assert_eq!(alice_1.get_user_id().await, alice_2.get_user_id().await);
854
855                let alicem_user_id = federated_alice_2.get_user_id().await;
856                let bobt_user_id = federated_bob.get_user_id().await;
857
858                // Finds both Alice's devices
859                let alice_user_id = alice_1.get_user_id().await;
860                let alice_identities = conversation_guard
861                    .get_user_identities(&[alice_user_id.clone()])
862                    .await
863                    .unwrap();
864                assert_eq!(alice_identities.len(), 1);
865                let identities = alice_identities.get(&alice_user_id).unwrap();
866                assert_eq!(identities.len(), 2);
867
868                // Finds Bob only device
869                let bob_user_id = bob.get_user_id().await;
870                let bob_identities = conversation_guard
871                    .get_user_identities(&[bob_user_id.clone()])
872                    .await
873                    .unwrap();
874                assert_eq!(bob_identities.len(), 1);
875                let identities = bob_identities.get(&bob_user_id).unwrap();
876                assert_eq!(identities.len(), 1);
877
878                // Finds all devices
879                let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
880                let expected_sizes = [2, 1, 2, 1];
881
882                for session in sessions {
883                    all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
884                }
885            })
886            .await
887        }
888
889        #[async_std::test]
890        async fn should_read_users() {
891            let case = TestContext::default_x509();
892            let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
893            let [bob_android] = case.x509_client_ids();
894
895            let sessions = case
896                .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
897                .await;
898
899            Box::pin(async move {
900                let conversation = case.create_conversation(&sessions).await;
901
902                let nb_members = conversation.member_count().await;
903                assert_eq!(nb_members, 3);
904
905                let [alice_android, alice_ios, bob_android] = &sessions;
906                assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
907
908                // Finds both Alice's devices
909                let alice_user_id = alice_android.get_user_id().await;
910                let alice_identities = conversation
911                    .guard()
912                    .await
913                    .get_user_identities(&[alice_user_id.clone()])
914                    .await
915                    .unwrap();
916                assert_eq!(alice_identities.len(), 1);
917                let identities = alice_identities.get(&alice_user_id).unwrap();
918                assert_eq!(identities.len(), 2);
919
920                // Finds Bob only device
921                let bob_user_id = bob_android.get_user_id().await;
922                let bob_identities = conversation
923                    .guard()
924                    .await
925                    .get_user_identities(&[bob_user_id.clone()])
926                    .await
927                    .unwrap();
928                assert_eq!(bob_identities.len(), 1);
929                let identities = bob_identities.get(&bob_user_id).unwrap();
930                assert_eq!(identities.len(), 1);
931
932                let user_ids = [alice_user_id, bob_user_id];
933                let expected_sizes = [2, 1];
934
935                for session in &sessions {
936                    all_identities_check(&conversation.guard_of(session).await, &user_ids, expected_sizes).await;
937                }
938            })
939            .await
940        }
941
942        #[async_std::test]
943        async fn should_exchange_messages_cross_signed() {
944            let case = TestContext::default_x509();
945            let sessions = case.sessions_x509_cross_signed::<3, 3>().await;
946            Box::pin(async move {
947                let sessions = sessions.0.iter().chain(sessions.1.iter());
948                let conversation = case.create_conversation(sessions.clone()).await;
949
950                assert_eq!(conversation.member_count().await, 6);
951
952                assert!(conversation.is_functional_and_contains(sessions).await);
953            })
954            .await;
955        }
956    }
957
958    mod export_secret {
959        use super::*;
960        use crate::MlsErrorKind;
961        use openmls::prelude::ExportSecretError;
962
963        #[apply(all_cred_cipher)]
964        pub async fn can_export_secret_key(case: TestContext) {
965            let [alice] = case.sessions().await;
966            Box::pin(async move {
967                let conversation = case.create_conversation([&alice]).await;
968
969                let key_length = 128;
970                let result = conversation.guard().await.export_secret_key(key_length).await;
971                assert!(result.is_ok());
972                assert_eq!(result.unwrap().len(), key_length);
973            })
974            .await
975        }
976
977        #[apply(all_cred_cipher)]
978        pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
979            let [alice] = case.sessions().await;
980            Box::pin(async move {
981                let conversation = case.create_conversation([&alice]).await;
982
983                let result = conversation.guard().await.export_secret_key(usize::MAX).await;
984                let error = result.unwrap_err();
985                assert!(innermost_source_matches!(
986                    error,
987                    MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
988                ));
989            })
990            .await
991        }
992    }
993
994    mod get_client_ids {
995        use super::*;
996
997        #[apply(all_cred_cipher)]
998        pub async fn can_get_client_ids(case: TestContext) {
999            let [alice, bob] = case.sessions().await;
1000            Box::pin(async move {
1001                let conversation = case.create_conversation([&alice]).await;
1002
1003                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
1004
1005                let conversation = conversation.invite_notify([&bob]).await;
1006
1007                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
1008            })
1009            .await
1010        }
1011    }
1012
1013    mod external_sender {
1014        use super::*;
1015
1016        #[apply(all_cred_cipher)]
1017        pub async fn should_fetch_ext_sender(mut case: TestContext) {
1018            let [alice, external_sender] = case.sessions().await;
1019            Box::pin(async move {
1020                let conversation = case
1021                    .create_conversation_with_external_sender(&external_sender, [&alice])
1022                    .await;
1023
1024                let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
1025                assert!(!alice_ext_sender.is_empty());
1026                assert_eq!(
1027                    alice_ext_sender,
1028                    external_sender.client_signature_key(&case).await.as_slice().to_vec()
1029                );
1030            })
1031            .await
1032        }
1033    }
1034}