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