core_crypto/mls/conversation/
mod.rs

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