core_crypto/mls/conversation/
mod.rs

1//! MLS groups (aka conversation) are the actual entities cementing all the participants in a
2//! conversation.
3//!
4//! This table summarizes what operations are permitted on a group depending its state:
5//! *(PP=pending proposal, PC=pending commit)*
6//!
7//! | can I ?   | 0 PP / 0 PC | 1+ PP / 0 PC | 0 PP / 1 PC | 1+ PP / 1 PC |
8//! |-----------|-------------|--------------|-------------|--------------|
9//! | encrypt   | ✅           | ❌            | ❌           | ❌            |
10//! | handshake | ✅           | ✅            | ❌           | ❌            |
11//! | merge     | ❌           | ❌            | ✅           | ✅            |
12//! | decrypt   | ✅           | ✅            | ✅           | ✅            |
13
14use config::MlsConversationConfiguration;
15use core_crypto_keystore::CryptoKeystoreMls;
16use itertools::Itertools as _;
17use log::trace;
18use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
19use openmls::{
20    group::MlsGroup,
21    prelude::{Credential, CredentialWithKey, LeafNodeIndex, Proposal, SignaturePublicKey},
22};
23use openmls_traits::OpenMlsCryptoProvider;
24use openmls_traits::types::SignatureScheme;
25use std::{collections::HashMap, sync::Arc};
26use std::{collections::HashSet, ops::Deref};
27
28use crate::{
29    KeystoreError, LeafError, MlsError, RecursiveError,
30    mls::Session,
31    prelude::{ClientId, E2eiConversationState, MlsCiphersuite, MlsCredentialType, WireIdentity},
32};
33
34pub(crate) mod commit;
35mod commit_delay;
36pub(crate) mod config;
37pub(crate) mod conversation_guard;
38mod duplicate;
39#[cfg(test)]
40mod durability;
41mod error;
42pub(crate) mod group_info;
43mod immutable_conversation;
44mod leaf_node_validation;
45pub(crate) mod merge;
46mod orphan_welcome;
47mod own_commit;
48pub(crate) mod pending_conversation;
49pub(crate) mod proposal;
50mod renew;
51pub(crate) mod welcome;
52mod wipe;
53
54use crate::mls::HasSessionAndCrypto;
55use crate::mls::credential::ext::CredentialExt as _;
56use crate::prelude::user_id::UserId;
57pub use conversation_guard::ConversationGuard;
58pub use error::{Error, Result};
59pub use immutable_conversation::ImmutableConversation;
60
61use super::credential::CredentialBundle;
62
63/// The base layer for [Conversation].
64/// The trait is only exposed internally.
65#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
66#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
67pub(crate) trait ConversationWithMls<'a> {
68    /// [Session] or [TransactionContext] both implement [HasSessionAndCrypto].
69    type Context: HasSessionAndCrypto;
70
71    type Conversation: Deref<Target = MlsConversation> + Send;
72
73    async fn context(&self) -> Result<Self::Context>;
74
75    async fn conversation(&'a self) -> Self::Conversation;
76
77    async fn crypto_provider(&self) -> Result<MlsCryptoProvider> {
78        self.context()
79            .await?
80            .crypto_provider()
81            .await
82            .map_err(RecursiveError::mls("getting mls provider"))
83            .map_err(Into::into)
84    }
85
86    async fn session(&self) -> Result<Session> {
87        self.context()
88            .await?
89            .session()
90            .await
91            .map_err(RecursiveError::mls("getting mls client"))
92            .map_err(Into::into)
93    }
94}
95
96/// The `Conversation` trait provides a set of operations that can be done on
97/// an **immutable** conversation.
98// We keep the super trait internal intentionally, as it is not meant to be used by the public API,
99// hence #[expect(private_bounds)].
100#[expect(private_bounds)]
101#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
102#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
103pub trait Conversation<'a>: ConversationWithMls<'a> {
104    /// Returns the epoch of a given conversation
105    async fn epoch(&'a self) -> u64 {
106        self.conversation().await.group().epoch().as_u64()
107    }
108
109    /// Returns the ciphersuite of a given conversation
110    async fn ciphersuite(&'a self) -> MlsCiphersuite {
111        self.conversation().await.ciphersuite()
112    }
113
114    /// Derives a new key from the one in the group, to be used elsewhere.
115    ///
116    /// # Arguments
117    /// * `key_length` - the length of the key to be derived. If the value is higher than the
118    ///     bounds of `u16` or the context hash * 255, an error will be returned
119    ///
120    /// # Errors
121    /// OpenMls secret generation error
122    async fn export_secret_key(&'a self, key_length: usize) -> Result<Vec<u8>> {
123        const EXPORTER_LABEL: &str = "exporter";
124        const EXPORTER_CONTEXT: &[u8] = &[];
125        let backend = self.crypto_provider().await?;
126        let inner = self.conversation().await;
127        inner
128            .group()
129            .export_secret(&backend, EXPORTER_LABEL, EXPORTER_CONTEXT, key_length)
130            .map_err(MlsError::wrap("exporting secret key"))
131            .map_err(Into::into)
132    }
133
134    /// Exports the clients from a conversation
135    ///
136    /// # Arguments
137    /// * `conversation_id` - the group/conversation id
138    async fn get_client_ids(&'a self) -> Vec<ClientId> {
139        let inner = self.conversation().await;
140        inner
141            .group()
142            .members()
143            .map(|kp| ClientId::from(kp.credential.identity()))
144            .collect()
145    }
146
147    /// Returns the raw public key of the single external sender present in this group.
148    /// This should be used to initialize a subconversation
149    async fn get_external_sender(&'a self) -> Result<Vec<u8>> {
150        let inner = self.conversation().await;
151        let ext_senders = inner
152            .group()
153            .group_context_extensions()
154            .external_senders()
155            .ok_or(Error::MissingExternalSenderExtension)?;
156        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
157        let ext_sender_public_key = ext_sender.signature_key().as_slice().to_vec();
158        Ok(ext_sender_public_key)
159    }
160
161    /// Indicates when to mark a conversation as not verified i.e. when not all its members have a X509
162    /// Credential generated by Wire's end-to-end identity enrollment
163    async fn e2ei_conversation_state(&'a self) -> Result<E2eiConversationState> {
164        let backend = self.crypto_provider().await?;
165        let authentication_service = backend.authentication_service();
166        authentication_service.refresh_time_of_interest().await;
167        let inner = self.conversation().await;
168        let state = Session::compute_conversation_state(
169            inner.ciphersuite(),
170            inner.group.members_credentials(),
171            MlsCredentialType::X509,
172            authentication_service.borrow().await.as_ref(),
173        )
174        .await;
175        Ok(state)
176    }
177
178    /// From a given conversation, get the identity of the members supplied. Identity is only present for
179    /// members with a Certificate Credential (after turning on end-to-end identity).
180    /// If no member has a x509 certificate, it will return an empty Vec
181    async fn get_device_identities(&'a self, device_ids: &[ClientId]) -> Result<Vec<WireIdentity>> {
182        if device_ids.is_empty() {
183            return Err(Error::CallerError(
184                "This function accepts a list of IDs as a parameter, but that list was empty.",
185            ));
186        }
187        let mls_provider = self.crypto_provider().await?;
188        let auth_service = mls_provider.authentication_service();
189        auth_service.refresh_time_of_interest().await;
190        let auth_service = auth_service.borrow().await;
191        let env = auth_service.as_ref();
192        let conversation = self.conversation().await;
193        conversation
194            .members_with_key()
195            .into_iter()
196            .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
197            .map(|(_, c)| {
198                c.extract_identity(conversation.ciphersuite(), env)
199                    .map_err(RecursiveError::mls_credential("extracting identity"))
200            })
201            .collect::<Result<Vec<_>, _>>()
202            .map_err(Into::into)
203    }
204
205    /// From a given conversation, get the identity of the users (device holders) supplied.
206    /// Identity is only present for devices with a Certificate Credential (after turning on end-to-end identity).
207    /// If no member has a x509 certificate, it will return an empty Vec.
208    ///
209    /// Returns a Map with all the identities for a given users. Consumers are then recommended to
210    /// reduce those identities to determine the actual status of a user.
211    async fn get_user_identities(&'a self, user_ids: &[String]) -> Result<HashMap<String, Vec<WireIdentity>>> {
212        if user_ids.is_empty() {
213            return Err(Error::CallerError(
214                "This function accepts a list of IDs as a parameter, but that list was empty.",
215            ));
216        }
217        let mls_provider = self.crypto_provider().await?;
218        let auth_service = mls_provider.authentication_service();
219        auth_service.refresh_time_of_interest().await;
220        let auth_service = auth_service.borrow().await;
221        let env = auth_service.as_ref();
222        let conversation = self.conversation().await;
223        let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
224
225        conversation
226            .members_with_key()
227            .iter()
228            .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
229            .filter(|(uid, _)| user_ids.contains(uid))
230            .map(|(uid, c)| {
231                let uid = String::try_from(uid).map_err(RecursiveError::mls_client("getting user identities"))?;
232                let identity = c
233                    .extract_identity(conversation.ciphersuite(), env)
234                    .map_err(RecursiveError::mls_credential("extracting identity"))?;
235                Ok((uid, identity))
236            })
237            .process_results(|iter| iter.into_group_map())
238    }
239}
240
241impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
242
243/// A unique identifier for a group/conversation. The identifier must be unique within a client.
244pub type ConversationId = Vec<u8>;
245
246/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
247///
248/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
249/// operations that can be done in a group, such as creating proposals and commits.
250/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
251#[derive(Debug)]
252#[allow(dead_code)]
253pub struct MlsConversation {
254    pub(crate) id: ConversationId,
255    pub(crate) parent_id: Option<ConversationId>,
256    pub(crate) group: MlsGroup,
257    configuration: MlsConversationConfiguration,
258}
259
260impl MlsConversation {
261    /// Creates a new group/conversation
262    ///
263    /// # Arguments
264    /// * `id` - group/conversation identifier
265    /// * `author_client` - the client responsible for creating the group
266    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
267    /// * `config` - group configuration
268    /// * `backend` - MLS Provider that will be used to persist the group
269    ///
270    /// # Errors
271    /// Errors can happen from OpenMls or from the KeyStore
272    pub async fn create(
273        id: ConversationId,
274        author_client: &Session,
275        creator_credential_type: MlsCredentialType,
276        configuration: MlsConversationConfiguration,
277        backend: &MlsCryptoProvider,
278    ) -> Result<Self> {
279        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
280        let cb = author_client
281            .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
282            .await
283            .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
284
285        let group = MlsGroup::new_with_group_id(
286            backend,
287            &cb.signature_key,
288            &configuration.as_openmls_default_configuration()?,
289            openmls::prelude::GroupId::from_slice(id.as_slice()),
290            cb.to_mls_credential_with_key(),
291        )
292        .await
293        .map_err(MlsError::wrap("creating group with id"))?;
294
295        let mut conversation = Self {
296            id,
297            group,
298            parent_id: None,
299            configuration,
300        };
301
302        conversation
303            .persist_group_when_changed(&backend.keystore(), true)
304            .await?;
305
306        Ok(conversation)
307    }
308
309    /// Internal API: create a group from an existing conversation. For example by external commit
310    pub(crate) async fn from_mls_group(
311        group: MlsGroup,
312        configuration: MlsConversationConfiguration,
313        backend: &MlsCryptoProvider,
314    ) -> Result<Self> {
315        let id = ConversationId::from(group.group_id().as_slice());
316
317        let mut conversation = Self {
318            id,
319            group,
320            configuration,
321            parent_id: None,
322        };
323
324        conversation
325            .persist_group_when_changed(&backend.keystore(), true)
326            .await?;
327
328        Ok(conversation)
329    }
330
331    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
332    pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
333        let group: MlsGroup =
334            core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
335        let id = ConversationId::from(group.group_id().as_slice());
336        let configuration = MlsConversationConfiguration {
337            ciphersuite: group.ciphersuite().into(),
338            ..Default::default()
339        };
340
341        Ok(Self {
342            id,
343            group,
344            parent_id,
345            configuration,
346        })
347    }
348
349    /// Group/conversation id
350    pub fn id(&self) -> &ConversationId {
351        &self.id
352    }
353
354    pub(crate) fn group(&self) -> &MlsGroup {
355        &self.group
356    }
357
358    /// Returns all members credentials from the group/conversation
359    pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
360        self.group.members().fold(HashMap::new(), |mut acc, kp| {
361            let credential = kp.credential;
362            let id = credential.identity().to_vec();
363            acc.entry(id).or_insert(credential);
364            acc
365        })
366    }
367
368    /// Get actual group members and subtract pending remove proposals
369    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
370        let pending_removals = self.pending_removals();
371        let existing_clients = self
372            .group
373            .members()
374            .filter_map(|kp| {
375                if !pending_removals.contains(&kp.index) {
376                    Some(kp.credential.identity().into())
377                } else {
378                    trace!(client_index:% = kp.index; "Client is pending removal");
379                    None
380                }
381            })
382            .collect::<HashSet<_>>();
383        existing_clients.into_iter().collect()
384    }
385
386    /// Gather pending remove proposals
387    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
388        self.group
389            .pending_proposals()
390            .filter_map(|proposal| match proposal.proposal() {
391                Proposal::Remove(remove) => Some(remove.removed()),
392                _ => None,
393            })
394            .collect::<Vec<_>>()
395    }
396
397    /// Returns all members credentials with their signature public key from the group/conversation
398    pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
399        self.group.members().fold(HashMap::new(), |mut acc, kp| {
400            let credential = kp.credential;
401            let id = credential.identity().to_vec();
402            let signature_key = SignaturePublicKey::from(kp.signature_key);
403            let credential = CredentialWithKey {
404                credential,
405                signature_key,
406            };
407            acc.entry(id).or_insert(credential);
408            acc
409        })
410    }
411
412    pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
413        if force || self.group.state_changed() == openmls::group::InnerState::Changed {
414            keystore
415                .mls_group_persist(
416                    &self.id,
417                    &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
418                    self.parent_id.as_deref(),
419                )
420                .await
421                .map_err(KeystoreError::wrap("persisting mls group"))?;
422
423            self.group.set_state(openmls::group::InnerState::Persisted);
424        }
425
426        Ok(())
427    }
428
429    pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
430        Ok(self
431            .group
432            .own_leaf_node()
433            .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
434            .credential()
435            .credential_type()
436            .into())
437    }
438
439    pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
440        self.configuration.ciphersuite
441    }
442
443    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
444        self.ciphersuite().signature_algorithm()
445    }
446
447    pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
448        let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
449        let sc = self.ciphersuite().signature_algorithm();
450        let ct = self
451            .own_credential_type()
452            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
453
454        client
455            .find_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
456            .await
457            .map_err(RecursiveError::mls_client("finding current credential bundle"))
458            .map_err(Into::into)
459    }
460
461    pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
462        let sc = self.ciphersuite().signature_algorithm();
463        let ct = self
464            .own_credential_type()
465            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
466
467        client
468            .find_most_recent_credential_bundle(sc, ct)
469            .await
470            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
471            .map_err(Into::into)
472    }
473}
474
475#[cfg(test)]
476pub mod test_utils {
477    use super::*;
478
479    impl MlsConversation {
480        pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
481            self.group
482                .members()
483                .map(|m| m.signature_key)
484                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
485        }
486
487        pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
488            self.group.members().map(|m| m.encryption_key)
489        }
490
491        pub fn extensions(&self) -> &openmls::prelude::Extensions {
492            self.group.export_group_context().extensions()
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::{
501        CoreCrypto,
502        prelude::{ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsClientConfiguration},
503        test_utils::*,
504    };
505    use core_crypto_keystore::DatabaseKey;
506    use std::sync::Arc;
507    use wasm_bindgen_test::*;
508
509    wasm_bindgen_test_configure!(run_in_browser);
510
511    #[apply(all_cred_cipher)]
512    #[wasm_bindgen_test]
513    pub async fn create_self_conversation_should_succeed(case: TestCase) {
514        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
515            Box::pin(async move {
516                let id = conversation_id();
517                alice_central
518                    .context
519                    .new_conversation(&id, case.credential_type, case.cfg.clone())
520                    .await
521                    .unwrap();
522                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
523                assert_eq!(
524                    alice_central
525                        .get_conversation_unchecked(&id)
526                        .await
527                        .group
528                        .group_id()
529                        .as_slice(),
530                    id
531                );
532                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
533                let alice_can_send_message = alice_central
534                    .context
535                    .conversation(&id)
536                    .await
537                    .unwrap()
538                    .encrypt_message(b"me")
539                    .await;
540                assert!(alice_can_send_message.is_ok());
541            })
542        })
543        .await;
544    }
545
546    #[apply(all_cred_cipher)]
547    #[wasm_bindgen_test]
548    pub async fn create_1_1_conversation_should_succeed(case: TestCase) {
549        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
550            Box::pin(async move {
551                let id = conversation_id();
552
553                alice_central
554                    .context
555                    .new_conversation(&id, case.credential_type, case.cfg.clone())
556                    .await
557                    .unwrap();
558
559                let bob = bob_central.rand_key_package(&case).await;
560                alice_central
561                    .context
562                    .conversation(&id)
563                    .await
564                    .unwrap()
565                    .add_members(vec![bob])
566                    .await
567                    .unwrap();
568
569                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
570                assert_eq!(
571                    alice_central
572                        .get_conversation_unchecked(&id)
573                        .await
574                        .group
575                        .group_id()
576                        .as_slice(),
577                    id
578                );
579                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
580
581                let welcome = alice_central.mls_transport.latest_welcome_message().await;
582                bob_central
583                    .context
584                    .process_welcome_message(welcome.into(), case.custom_cfg())
585                    .await
586                    .unwrap();
587
588                assert_eq!(
589                    bob_central.get_conversation_unchecked(&id).await.id(),
590                    alice_central.get_conversation_unchecked(&id).await.id()
591                );
592                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
593            })
594        })
595        .await;
596    }
597
598    #[apply(all_cred_cipher)]
599    #[wasm_bindgen_test]
600    pub async fn create_many_people_conversation(case: TestCase) {
601        use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
602
603        run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
604            Box::pin(async move {
605                let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
606                let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
607
608                let id = conversation_id();
609                alice_central
610                    .context
611                    .new_conversation(&id, case.credential_type, case.cfg.clone())
612                    .await
613                    .unwrap();
614
615                let mut bob_and_friends: Vec<SessionContext> = Vec::with_capacity(GROUP_SAMPLE_SIZE);
616                for _ in 0..GROUP_SAMPLE_SIZE {
617                    let uuid = uuid::Uuid::new_v4();
618                    let name = uuid.hyphenated().to_string();
619                    let path = tmp_db_file();
620                    let config = MlsClientConfiguration::try_new(
621                        path.0,
622                        DatabaseKey::generate(),
623                        None,
624                        vec![case.ciphersuite()],
625                        None,
626                        Some(INITIAL_KEYING_MATERIAL_COUNT),
627                    )
628                    .unwrap();
629                    let client = Session::try_new(config).await.unwrap();
630                    let cc = CoreCrypto::from(client);
631                    let friend_context = cc.new_transaction().await.unwrap();
632                    let central = cc.mls;
633
634                    x509_test_chain.register_with_central(&friend_context).await;
635
636                    let client_id: crate::prelude::ClientId = name.as_str().into();
637                    let identity = match case.credential_type {
638                        MlsCredentialType::Basic => ClientIdentifier::Basic(client_id),
639                        MlsCredentialType::X509 => {
640                            let x509_test_chain = alice_central
641                                .x509_test_chain
642                                .as_ref()
643                                .as_ref()
644                                .expect("No x509 test chain");
645                            let cert = crate::prelude::CertificateBundle::rand(
646                                &client_id,
647                                x509_test_chain.find_local_intermediate_ca(),
648                            );
649                            ClientIdentifier::X509(HashMap::from([(case.cfg.ciphersuite.signature_algorithm(), cert)]))
650                        }
651                    };
652                    friend_context
653                        .mls_init(
654                            identity,
655                            vec![case.cfg.ciphersuite],
656                            Some(INITIAL_KEYING_MATERIAL_COUNT),
657                        )
658                        .await
659                        .unwrap();
660
661                    let context = SessionContext {
662                        context: friend_context,
663                        session: central,
664                        mls_transport: Arc::<CoreCryptoTransportSuccessProvider>::default(),
665                        x509_test_chain: x509_test_chain_arc.clone(),
666                    };
667                    bob_and_friends.push(context);
668                }
669
670                let number_of_friends = bob_and_friends.len();
671
672                let mut bob_and_friends_kps = vec![];
673                for c in &bob_and_friends {
674                    bob_and_friends_kps.push(c.rand_key_package(&case).await);
675                }
676
677                alice_central
678                    .context
679                    .conversation(&id)
680                    .await
681                    .unwrap()
682                    .add_members(bob_and_friends_kps)
683                    .await
684                    .unwrap();
685                let welcome = alice_central.mls_transport.latest_welcome_message().await;
686
687                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
688                assert_eq!(
689                    alice_central
690                        .get_conversation_unchecked(&id)
691                        .await
692                        .group
693                        .group_id()
694                        .as_slice(),
695                    id
696                );
697                assert_eq!(
698                    alice_central.get_conversation_unchecked(&id).await.members().len(),
699                    1 + number_of_friends
700                );
701
702                let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
703                // TODO: Do things in parallel, this is waaaaay too slow (takes around 5 minutes). Tracking issue: WPB-9624
704                for c in bob_and_friends {
705                    c.context
706                        .process_welcome_message(welcome.clone().into(), case.custom_cfg())
707                        .await
708                        .unwrap();
709                    assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
710                    bob_and_friends_groups.push(c);
711                }
712
713                assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
714            })
715        })
716        .await;
717    }
718
719    mod wire_identity_getters {
720        use wasm_bindgen_test::*;
721
722        use super::Error;
723        use crate::mls::conversation::Conversation as _;
724        use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
725        use crate::transaction_context::TransactionContext;
726        use crate::{
727            prelude::{DeviceStatus, E2eiConversationState},
728            test_utils::*,
729        };
730
731        wasm_bindgen_test_configure!(run_in_browser);
732
733        async fn all_identities_check<const N: usize>(
734            central: &TransactionContext,
735            id: &ConversationId,
736            user_ids: &[String; N],
737            expected_sizes: [usize; N],
738        ) {
739            let all_identities = central
740                .conversation(id)
741                .await
742                .unwrap()
743                .get_user_identities(user_ids)
744                .await
745                .unwrap();
746            assert_eq!(all_identities.len(), N);
747            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
748                let alice_identities = all_identities.get(user_id).unwrap();
749                assert_eq!(alice_identities.len(), expected_size);
750            }
751            // Not found
752            let not_found = central
753                .conversation(id)
754                .await
755                .unwrap()
756                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
757                .await
758                .unwrap();
759            assert!(not_found.is_empty());
760
761            // Invalid usage
762            let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
763            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
764        }
765
766        async fn check_identities_device_status<const N: usize>(
767            central: &TransactionContext,
768            id: &ConversationId,
769            client_ids: &[ClientId; N],
770            name_status: &[(&'static str, DeviceStatus); N],
771        ) {
772            let mut identities = central
773                .conversation(id)
774                .await
775                .unwrap()
776                .get_device_identities(client_ids)
777                .await
778                .unwrap();
779
780            for j in 0..N {
781                let client_identity = identities.remove(
782                    identities
783                        .iter()
784                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
785                        .unwrap(),
786                );
787                assert_eq!(client_identity.status, name_status[j].1);
788            }
789            assert!(identities.is_empty());
790
791            assert_eq!(
792                central
793                    .conversation(id)
794                    .await
795                    .unwrap()
796                    .e2ei_conversation_state()
797                    .await
798                    .unwrap(),
799                E2eiConversationState::NotVerified
800            );
801        }
802
803        #[async_std::test]
804        #[wasm_bindgen_test]
805        async fn should_read_device_identities() {
806            let case = TestCase::default_x509();
807            run_test_with_client_ids(
808                case.clone(),
809                ["alice_android", "alice_ios"],
810                move |[alice_android_central, alice_ios_central]| {
811                    Box::pin(async move {
812                        let id = conversation_id();
813                        alice_android_central
814                            .context
815                            .new_conversation(&id, case.credential_type, case.cfg.clone())
816                            .await
817                            .unwrap();
818                        alice_android_central
819                            .invite_all(&case, &id, [&alice_ios_central])
820                            .await
821                            .unwrap();
822
823                        let (android_id, ios_id) = (
824                            alice_android_central.get_client_id().await,
825                            alice_ios_central.get_client_id().await,
826                        );
827
828                        let mut android_ids = alice_android_central
829                            .context
830                            .conversation(&id)
831                            .await
832                            .unwrap()
833                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
834                            .await
835                            .unwrap();
836                        android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
837                        assert_eq!(android_ids.len(), 2);
838                        let mut ios_ids = alice_ios_central
839                            .context
840                            .conversation(&id)
841                            .await
842                            .unwrap()
843                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
844                            .await
845                            .unwrap();
846                        ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
847                        assert_eq!(ios_ids.len(), 2);
848
849                        assert_eq!(android_ids, ios_ids);
850
851                        let android_identities = alice_android_central
852                            .context
853                            .conversation(&id)
854                            .await
855                            .unwrap()
856                            .get_device_identities(&[android_id])
857                            .await
858                            .unwrap();
859                        let android_id = android_identities.first().unwrap();
860                        assert_eq!(
861                            android_id.client_id.as_bytes(),
862                            alice_android_central.context.client_id().await.unwrap().0.as_slice()
863                        );
864
865                        let ios_identities = alice_android_central
866                            .context
867                            .conversation(&id)
868                            .await
869                            .unwrap()
870                            .get_device_identities(&[ios_id])
871                            .await
872                            .unwrap();
873                        let ios_id = ios_identities.first().unwrap();
874                        assert_eq!(
875                            ios_id.client_id.as_bytes(),
876                            alice_ios_central.context.client_id().await.unwrap().0.as_slice()
877                        );
878
879                        let invalid = alice_android_central
880                            .context
881                            .conversation(&id)
882                            .await
883                            .unwrap()
884                            .get_device_identities(&[])
885                            .await;
886                        assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
887                    })
888                },
889            )
890            .await
891        }
892
893        #[async_std::test]
894        #[wasm_bindgen_test]
895        async fn should_read_revoked_device_cross_signed() {
896            let case = TestCase::default_x509();
897            run_test_with_client_ids_and_revocation(
898                case.clone(),
899                ["alice", "bob", "rupert"],
900                ["john", "dilbert"],
901                &["rupert", "dilbert"],
902                move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
903                    Box::pin(async move {
904                        let id = conversation_id();
905                        alice
906                            .context
907                            .new_conversation(&id, case.credential_type, case.cfg.clone())
908                            .await
909                            .unwrap();
910                        alice
911                            .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
912                            .await
913                            .unwrap();
914
915                        let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
916                            alice.get_client_id().await,
917                            bob.get_client_id().await,
918                            rupert.get_client_id().await,
919                            dilbert.get_client_id().await,
920                            john.get_client_id().await,
921                        );
922
923                        let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
924                        let name_status = [
925                            ("alice", DeviceStatus::Valid),
926                            ("bob", DeviceStatus::Valid),
927                            ("rupert", DeviceStatus::Revoked),
928                            ("john", DeviceStatus::Valid),
929                            ("dilbert", DeviceStatus::Revoked),
930                        ];
931                        // Do it a multiple times to avoid WPB-6904 happening again
932                        for _ in 0..2 {
933                            check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
934                            check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
935                            check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
936                            check_identities_device_status(&mut john.context, &id, &client_ids, &name_status).await;
937                            check_identities_device_status(&mut dilbert.context, &id, &client_ids, &name_status).await;
938                        }
939                    })
940                },
941            )
942            .await
943        }
944
945        #[async_std::test]
946        #[wasm_bindgen_test]
947        async fn should_read_revoked_device() {
948            let case = TestCase::default_x509();
949            run_test_with_client_ids_and_revocation(
950                case.clone(),
951                ["alice", "bob", "rupert"],
952                [],
953                &["rupert"],
954                move |[mut alice, mut bob, mut rupert], []| {
955                    Box::pin(async move {
956                        let id = conversation_id();
957                        alice
958                            .context
959                            .new_conversation(&id, case.credential_type, case.cfg.clone())
960                            .await
961                            .unwrap();
962                        alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
963
964                        let (alice_id, bob_id, rupert_id) = (
965                            alice.get_client_id().await,
966                            bob.get_client_id().await,
967                            rupert.get_client_id().await,
968                        );
969
970                        let client_ids = [alice_id, bob_id, rupert_id];
971                        let name_status = [
972                            ("alice", DeviceStatus::Valid),
973                            ("bob", DeviceStatus::Valid),
974                            ("rupert", DeviceStatus::Revoked),
975                        ];
976
977                        // Do it a multiple times to avoid WPB-6904 happening again
978                        for _ in 0..2 {
979                            check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
980                            check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
981                            check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
982                        }
983                    })
984                },
985            )
986            .await
987        }
988
989        #[async_std::test]
990        #[wasm_bindgen_test]
991        async fn should_not_fail_when_basic() {
992            let case = TestCase::default();
993            run_test_with_client_ids(
994                case.clone(),
995                ["alice_android", "alice_ios"],
996                move |[alice_android_central, alice_ios_central]| {
997                    Box::pin(async move {
998                        let id = conversation_id();
999                        alice_android_central
1000                            .context
1001                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1002                            .await
1003                            .unwrap();
1004                        alice_android_central
1005                            .invite_all(&case, &id, [&alice_ios_central])
1006                            .await
1007                            .unwrap();
1008
1009                        let (android_id, ios_id) = (
1010                            alice_android_central.get_client_id().await,
1011                            alice_ios_central.get_client_id().await,
1012                        );
1013
1014                        let mut android_ids = alice_android_central
1015                            .context
1016                            .conversation(&id)
1017                            .await
1018                            .unwrap()
1019                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
1020                            .await
1021                            .unwrap();
1022                        android_ids.sort();
1023
1024                        let mut ios_ids = alice_ios_central
1025                            .context
1026                            .conversation(&id)
1027                            .await
1028                            .unwrap()
1029                            .get_device_identities(&[android_id, ios_id])
1030                            .await
1031                            .unwrap();
1032                        ios_ids.sort();
1033
1034                        assert_eq!(ios_ids.len(), 2);
1035                        assert_eq!(ios_ids, android_ids);
1036
1037                        assert!(ios_ids.iter().all(|i| {
1038                            matches!(i.credential_type, MlsCredentialType::Basic)
1039                                && matches!(i.status, DeviceStatus::Valid)
1040                                && i.x509_identity.is_none()
1041                                && !i.thumbprint.is_empty()
1042                                && !i.client_id.is_empty()
1043                        }));
1044                    })
1045                },
1046            )
1047            .await
1048        }
1049
1050        // this test is a duplicate of its counterpart but taking federation into account
1051        // The heavy lifting of cross-signing the certificates is being done by the test utils.
1052        #[async_std::test]
1053        #[wasm_bindgen_test]
1054        async fn should_read_users_cross_signed() {
1055            let case = TestCase::default_x509();
1056
1057            let (alice_android, alice_ios) = (
1058                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1059                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1060            );
1061            let (alicem_android, alicem_ios) = (
1062                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1063                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1064            );
1065            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1066            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1067
1068            run_test_with_deterministic_client_ids_and_revocation(
1069                case.clone(),
1070                [
1071                    [alice_android, "alice_wire", "Alice Smith"],
1072                    [alice_ios, "alice_wire", "Alice Smith"],
1073                    [bob_android, "bob_wire", "Bob Doe"],
1074                ],
1075                [
1076                    [alicem_android, "alice_zeta", "Alice Muller"],
1077                    [alicem_ios, "alice_zeta", "Alice Muller"],
1078                    [bobt_android, "bob_zeta", "Bob Tables"],
1079                ],
1080                &[],
1081                move |[alice_android_central, alice_ios_central, bob_android_central],
1082                      [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1083                    Box::pin(async move {
1084                        let id = conversation_id();
1085                        alice_android_central
1086                            .context
1087                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1088                            .await
1089                            .unwrap();
1090                        alice_android_central
1091                            .invite_all(
1092                                &case,
1093                                &id,
1094                                [
1095                                    &alice_ios_central,
1096                                    &bob_android_central,
1097                                    &bobt_android_central,
1098                                    &alicem_ios_central,
1099                                    &alicem_android_central,
1100                                ],
1101                            )
1102                            .await
1103                            .unwrap();
1104
1105                        let nb_members = alice_android_central
1106                            .get_conversation_unchecked(&id)
1107                            .await
1108                            .members()
1109                            .len();
1110                        assert_eq!(nb_members, 6);
1111
1112                        assert_eq!(
1113                            alice_android_central.get_user_id().await,
1114                            alice_ios_central.get_user_id().await
1115                        );
1116
1117                        let alicem_user_id = alicem_ios_central.get_user_id().await;
1118                        let bobt_user_id = bobt_android_central.get_user_id().await;
1119
1120                        // Finds both Alice's devices
1121                        let alice_user_id = alice_android_central.get_user_id().await;
1122                        let alice_identities = alice_android_central
1123                            .context
1124                            .conversation(&id)
1125                            .await
1126                            .unwrap()
1127                            .get_user_identities(&[alice_user_id.clone()])
1128                            .await
1129                            .unwrap();
1130                        assert_eq!(alice_identities.len(), 1);
1131                        let identities = alice_identities.get(&alice_user_id).unwrap();
1132                        assert_eq!(identities.len(), 2);
1133
1134                        // Finds Bob only device
1135                        let bob_user_id = bob_android_central.get_user_id().await;
1136                        let bob_identities = alice_android_central
1137                            .context
1138                            .conversation(&id)
1139                            .await
1140                            .unwrap()
1141                            .get_user_identities(&[bob_user_id.clone()])
1142                            .await
1143                            .unwrap();
1144                        assert_eq!(bob_identities.len(), 1);
1145                        let identities = bob_identities.get(&bob_user_id).unwrap();
1146                        assert_eq!(identities.len(), 1);
1147
1148                        // Finds all devices
1149                        let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1150                        let expected_sizes = [2, 1, 2, 1];
1151
1152                        all_identities_check(&alice_android_central.context, &id, &user_ids, expected_sizes).await;
1153                        all_identities_check(&alicem_android_central.context, &id, &user_ids, expected_sizes).await;
1154                        all_identities_check(&alice_ios_central.context, &id, &user_ids, expected_sizes).await;
1155                        all_identities_check(&alicem_ios_central.context, &id, &user_ids, expected_sizes).await;
1156                        all_identities_check(&bob_android_central.context, &id, &user_ids, expected_sizes).await;
1157                        all_identities_check(&bobt_android_central.context, &id, &user_ids, expected_sizes).await;
1158                    })
1159                },
1160            )
1161            .await
1162        }
1163
1164        #[async_std::test]
1165        #[wasm_bindgen_test]
1166        async fn should_read_users() {
1167            let case = TestCase::default_x509();
1168
1169            let (alice_android, alice_ios) = (
1170                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1171                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1172            );
1173            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1174
1175            run_test_with_deterministic_client_ids(
1176                case.clone(),
1177                [
1178                    [alice_android, "alice_wire", "Alice Smith"],
1179                    [alice_ios, "alice_wire", "Alice Smith"],
1180                    [bob_android, "bob_wire", "Bob Doe"],
1181                ],
1182                move |[
1183                    mut alice_android_central,
1184                    mut alice_ios_central,
1185                    mut bob_android_central,
1186                ]| {
1187                    Box::pin(async move {
1188                        let id = conversation_id();
1189                        alice_android_central
1190                            .context
1191                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1192                            .await
1193                            .unwrap();
1194                        alice_android_central
1195                            .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1196                            .await
1197                            .unwrap();
1198
1199                        let nb_members = alice_android_central
1200                            .get_conversation_unchecked(&id)
1201                            .await
1202                            .members()
1203                            .len();
1204                        assert_eq!(nb_members, 3);
1205
1206                        assert_eq!(
1207                            alice_android_central.get_user_id().await,
1208                            alice_ios_central.get_user_id().await
1209                        );
1210
1211                        // Finds both Alice's devices
1212                        let alice_user_id = alice_android_central.get_user_id().await;
1213                        let alice_identities = alice_android_central
1214                            .context
1215                            .conversation(&id)
1216                            .await
1217                            .unwrap()
1218                            .get_user_identities(&[alice_user_id.clone()])
1219                            .await
1220                            .unwrap();
1221                        assert_eq!(alice_identities.len(), 1);
1222                        let identities = alice_identities.get(&alice_user_id).unwrap();
1223                        assert_eq!(identities.len(), 2);
1224
1225                        // Finds Bob only device
1226                        let bob_user_id = bob_android_central.get_user_id().await;
1227                        let bob_identities = alice_android_central
1228                            .context
1229                            .conversation(&id)
1230                            .await
1231                            .unwrap()
1232                            .get_user_identities(&[bob_user_id.clone()])
1233                            .await
1234                            .unwrap();
1235                        assert_eq!(bob_identities.len(), 1);
1236                        let identities = bob_identities.get(&bob_user_id).unwrap();
1237                        assert_eq!(identities.len(), 1);
1238
1239                        let user_ids = [alice_user_id, bob_user_id];
1240                        let expected_sizes = [2, 1];
1241
1242                        all_identities_check(&mut alice_android_central.context, &id, &user_ids, expected_sizes).await;
1243                        all_identities_check(&mut alice_ios_central.context, &id, &user_ids, expected_sizes).await;
1244                        all_identities_check(&mut bob_android_central.context, &id, &user_ids, expected_sizes).await;
1245                    })
1246                },
1247            )
1248            .await
1249        }
1250
1251        #[async_std::test]
1252        #[wasm_bindgen_test]
1253        async fn should_exchange_messages_cross_signed() {
1254            let (alice_android, alice_ios) = (
1255                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1256                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1257            );
1258            let (alicem_android, alicem_ios) = (
1259                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1260                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1261            );
1262            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1263            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1264
1265            let case = TestCase::default_x509();
1266
1267            run_cross_signed_tests_with_client_ids(
1268                case.clone(),
1269                [
1270                    [alice_android, "alice_wire", "Alice Smith"],
1271                    [alice_ios, "alice_wire", "Alice Smith"],
1272                    [bob_android, "bob_wire", "Bob Doe"],
1273                ],
1274                [
1275                    [alicem_android, "alice_zeta", "Alice Muller"],
1276                    [alicem_ios, "alice_zeta", "Alice Muller"],
1277                    [bobt_android, "bob_zeta", "Bob Tables"],
1278                ],
1279                ("wire.com", "zeta.com"),
1280                move |[
1281                    mut alices_android_central,
1282                    mut alices_ios_central,
1283                    mut bob_android_central,
1284                ],
1285                      [
1286                    mut alicem_android_central,
1287                    mut alicem_ios_central,
1288                    mut bobt_android_central,
1289                ]| {
1290                    Box::pin(async move {
1291                        let id = conversation_id();
1292                        alices_ios_central
1293                            .context
1294                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1295                            .await
1296                            .unwrap();
1297
1298                        alices_ios_central
1299                            .invite_all(
1300                                &case,
1301                                &id,
1302                                [
1303                                    &mut alices_android_central,
1304                                    &mut bob_android_central,
1305                                    &mut alicem_android_central,
1306                                    &mut alicem_ios_central,
1307                                    &mut bobt_android_central,
1308                                ],
1309                            )
1310                            .await
1311                            .unwrap();
1312
1313                        let nb_members = alices_android_central
1314                            .get_conversation_unchecked(&id)
1315                            .await
1316                            .members()
1317                            .len();
1318                        assert_eq!(nb_members, 6);
1319
1320                        assert_eq!(
1321                            alicem_android_central.get_user_id().await,
1322                            alicem_ios_central.get_user_id().await
1323                        );
1324
1325                        // cross server communication
1326                        bobt_android_central
1327                            .try_talk_to(&id, &mut alices_ios_central)
1328                            .await
1329                            .unwrap();
1330
1331                        // same server communication
1332                        bob_android_central
1333                            .try_talk_to(&id, &mut alices_ios_central)
1334                            .await
1335                            .unwrap();
1336                    })
1337                },
1338            )
1339            .await;
1340        }
1341    }
1342
1343    mod export_secret {
1344        use super::*;
1345        use crate::MlsErrorKind;
1346        use openmls::prelude::ExportSecretError;
1347
1348        #[apply(all_cred_cipher)]
1349        #[wasm_bindgen_test]
1350        pub async fn can_export_secret_key(case: TestCase) {
1351            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1352                Box::pin(async move {
1353                    let id = conversation_id();
1354                    alice_central
1355                        .context
1356                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1357                        .await
1358                        .unwrap();
1359
1360                    let key_length = 128;
1361                    let result = alice_central
1362                        .context
1363                        .conversation(&id)
1364                        .await
1365                        .unwrap()
1366                        .export_secret_key(key_length)
1367                        .await;
1368                    assert!(result.is_ok());
1369                    assert_eq!(result.unwrap().len(), key_length);
1370                })
1371            })
1372            .await
1373        }
1374
1375        #[apply(all_cred_cipher)]
1376        #[wasm_bindgen_test]
1377        pub async fn cannot_export_secret_key_invalid_length(case: TestCase) {
1378            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1379                Box::pin(async move {
1380                    let id = conversation_id();
1381                    alice_central
1382                        .context
1383                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1384                        .await
1385                        .unwrap();
1386
1387                    let result = alice_central
1388                        .context
1389                        .conversation(&id)
1390                        .await
1391                        .unwrap()
1392                        .export_secret_key(usize::MAX)
1393                        .await;
1394                    let error = result.unwrap_err();
1395                    assert!(innermost_source_matches!(
1396                        error,
1397                        MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1398                    ));
1399                })
1400            })
1401            .await
1402        }
1403    }
1404
1405    mod get_client_ids {
1406        use super::*;
1407
1408        #[apply(all_cred_cipher)]
1409        #[wasm_bindgen_test]
1410        pub async fn can_get_client_ids(case: TestCase) {
1411            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1412                Box::pin(async move {
1413                    let id = conversation_id();
1414                    alice_central
1415                        .context
1416                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1417                        .await
1418                        .unwrap();
1419
1420                    assert_eq!(
1421                        alice_central
1422                            .context
1423                            .conversation(&id)
1424                            .await
1425                            .unwrap()
1426                            .get_client_ids()
1427                            .await
1428                            .len(),
1429                        1
1430                    );
1431
1432                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1433                    assert_eq!(
1434                        alice_central
1435                            .context
1436                            .conversation(&id)
1437                            .await
1438                            .unwrap()
1439                            .get_client_ids()
1440                            .await
1441                            .len(),
1442                        2
1443                    );
1444                })
1445            })
1446            .await
1447        }
1448    }
1449
1450    mod external_sender {
1451        use super::*;
1452
1453        #[apply(all_cred_cipher)]
1454        #[wasm_bindgen_test]
1455        pub async fn should_fetch_ext_sender(case: TestCase) {
1456            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1457                Box::pin(async move {
1458                    let id = conversation_id();
1459
1460                    // by default in test no external sender is set. Let's add one
1461                    let mut cfg = case.cfg.clone();
1462                    let external_sender = alice_central.rand_external_sender(&case).await;
1463                    cfg.external_senders = vec![external_sender.clone()];
1464
1465                    alice_central
1466                        .context
1467                        .new_conversation(&id, case.credential_type, cfg)
1468                        .await
1469                        .unwrap();
1470
1471                    let alice_ext_sender = alice_central
1472                        .context
1473                        .conversation(&id)
1474                        .await
1475                        .unwrap()
1476                        .get_external_sender()
1477                        .await
1478                        .unwrap();
1479                    assert!(!alice_ext_sender.is_empty());
1480                    assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1481                })
1482            })
1483            .await
1484        }
1485    }
1486}