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