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    /// Generate a new [`HistorySecret`].
241    ///
242    /// This is useful when it's this client's turn to generate a new history client.
243    ///
244    /// The generated secret is cryptographically unrelated to the current CoreCrypto client.
245    async fn generate_history_secret(&'a self) -> Result<crate::prelude::HistorySecret> {
246        let ciphersuite = self.ciphersuite().await;
247        crate::ephemeral::generate_history_secret(ciphersuite)
248            .await
249            .map_err(RecursiveError::root("generating history secret"))
250            .map_err(Into::into)
251    }
252}
253
254impl<'a, T: ConversationWithMls<'a>> Conversation<'a> for T {}
255
256/// A unique identifier for a group/conversation. The identifier must be unique within a client.
257pub type ConversationId = Vec<u8>;
258
259/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
260///
261/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
262/// operations that can be done in a group, such as creating proposals and commits.
263/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
264#[derive(Debug)]
265#[allow(dead_code)]
266pub struct MlsConversation {
267    pub(crate) id: ConversationId,
268    pub(crate) parent_id: Option<ConversationId>,
269    pub(crate) group: MlsGroup,
270    configuration: MlsConversationConfiguration,
271}
272
273impl MlsConversation {
274    /// Creates a new group/conversation
275    ///
276    /// # Arguments
277    /// * `id` - group/conversation identifier
278    /// * `author_client` - the client responsible for creating the group
279    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
280    /// * `config` - group configuration
281    /// * `backend` - MLS Provider that will be used to persist the group
282    ///
283    /// # Errors
284    /// Errors can happen from OpenMls or from the KeyStore
285    pub async fn create(
286        id: ConversationId,
287        author_client: &Session,
288        creator_credential_type: MlsCredentialType,
289        configuration: MlsConversationConfiguration,
290        backend: &MlsCryptoProvider,
291    ) -> Result<Self> {
292        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
293        let cb = author_client
294            .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
295            .await
296            .map_err(RecursiveError::mls_client("getting or creating credential bundle"))?;
297
298        let group = MlsGroup::new_with_group_id(
299            backend,
300            &cb.signature_key,
301            &configuration.as_openmls_default_configuration()?,
302            openmls::prelude::GroupId::from_slice(id.as_slice()),
303            cb.to_mls_credential_with_key(),
304        )
305        .await
306        .map_err(MlsError::wrap("creating group with id"))?;
307
308        let mut conversation = Self {
309            id,
310            group,
311            parent_id: None,
312            configuration,
313        };
314
315        conversation
316            .persist_group_when_changed(&backend.keystore(), true)
317            .await?;
318
319        Ok(conversation)
320    }
321
322    /// Internal API: create a group from an existing conversation. For example by external commit
323    pub(crate) async fn from_mls_group(
324        group: MlsGroup,
325        configuration: MlsConversationConfiguration,
326        backend: &MlsCryptoProvider,
327    ) -> Result<Self> {
328        let id = ConversationId::from(group.group_id().as_slice());
329
330        let mut conversation = Self {
331            id,
332            group,
333            configuration,
334            parent_id: None,
335        };
336
337        conversation
338            .persist_group_when_changed(&backend.keystore(), true)
339            .await?;
340
341        Ok(conversation)
342    }
343
344    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
345    pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> Result<Self> {
346        let group: MlsGroup =
347            core_crypto_keystore::deser(&buf).map_err(KeystoreError::wrap("deserializing group state"))?;
348        let id = ConversationId::from(group.group_id().as_slice());
349        let configuration = MlsConversationConfiguration {
350            ciphersuite: group.ciphersuite().into(),
351            ..Default::default()
352        };
353
354        Ok(Self {
355            id,
356            group,
357            parent_id,
358            configuration,
359        })
360    }
361
362    /// Group/conversation id
363    pub fn id(&self) -> &ConversationId {
364        &self.id
365    }
366
367    pub(crate) fn group(&self) -> &MlsGroup {
368        &self.group
369    }
370
371    /// Returns all members credentials from the group/conversation
372    pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
373        self.group.members().fold(HashMap::new(), |mut acc, kp| {
374            let credential = kp.credential;
375            let id = credential.identity().to_vec();
376            acc.entry(id).or_insert(credential);
377            acc
378        })
379    }
380
381    /// Get actual group members and subtract pending remove proposals
382    pub fn members_in_next_epoch(&self) -> Vec<ClientId> {
383        let pending_removals = self.pending_removals();
384        let existing_clients = self
385            .group
386            .members()
387            .filter_map(|kp| {
388                if !pending_removals.contains(&kp.index) {
389                    Some(kp.credential.identity().into())
390                } else {
391                    trace!(client_index:% = kp.index; "Client is pending removal");
392                    None
393                }
394            })
395            .collect::<HashSet<_>>();
396        existing_clients.into_iter().collect()
397    }
398
399    /// Gather pending remove proposals
400    fn pending_removals(&self) -> Vec<LeafNodeIndex> {
401        self.group
402            .pending_proposals()
403            .filter_map(|proposal| match proposal.proposal() {
404                Proposal::Remove(remove) => Some(remove.removed()),
405                _ => None,
406            })
407            .collect::<Vec<_>>()
408    }
409
410    /// Returns all members credentials with their signature public key from the group/conversation
411    pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
412        self.group.members().fold(HashMap::new(), |mut acc, kp| {
413            let credential = kp.credential;
414            let id = credential.identity().to_vec();
415            let signature_key = SignaturePublicKey::from(kp.signature_key);
416            let credential = CredentialWithKey {
417                credential,
418                signature_key,
419            };
420            acc.entry(id).or_insert(credential);
421            acc
422        })
423    }
424
425    pub(crate) async fn persist_group_when_changed(&mut self, keystore: &CryptoKeystore, force: bool) -> Result<()> {
426        if force || self.group.state_changed() == openmls::group::InnerState::Changed {
427            keystore
428                .mls_group_persist(
429                    &self.id,
430                    &core_crypto_keystore::ser(&self.group).map_err(KeystoreError::wrap("serializing group state"))?,
431                    self.parent_id.as_deref(),
432                )
433                .await
434                .map_err(KeystoreError::wrap("persisting mls group"))?;
435
436            self.group.set_state(openmls::group::InnerState::Persisted);
437        }
438
439        Ok(())
440    }
441
442    pub(crate) fn own_credential_type(&self) -> Result<MlsCredentialType> {
443        Ok(self
444            .group
445            .own_leaf_node()
446            .ok_or(Error::MlsGroupInvalidState("own_leaf_node not present in group"))?
447            .credential()
448            .credential_type()
449            .into())
450    }
451
452    pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
453        self.configuration.ciphersuite
454    }
455
456    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
457        self.ciphersuite().signature_algorithm()
458    }
459
460    pub(crate) async fn find_current_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
461        let own_leaf = self.group.own_leaf().ok_or(LeafError::InternalMlsError)?;
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_credential_bundle_by_public_key(sc, ct, own_leaf.signature_key())
469            .await
470            .map_err(RecursiveError::mls_client("finding current credential bundle"))
471            .map_err(Into::into)
472    }
473
474    pub(crate) async fn find_most_recent_credential_bundle(&self, client: &Session) -> Result<Arc<CredentialBundle>> {
475        let sc = self.ciphersuite().signature_algorithm();
476        let ct = self
477            .own_credential_type()
478            .map_err(RecursiveError::mls_conversation("getting own credential type"))?;
479
480        client
481            .find_most_recent_credential_bundle(sc, ct)
482            .await
483            .map_err(RecursiveError::mls_client("finding most recent credential bundle"))
484            .map_err(Into::into)
485    }
486}
487
488#[cfg(test)]
489pub mod test_utils {
490    use super::*;
491
492    impl MlsConversation {
493        pub fn signature_keys(&self) -> impl Iterator<Item = SignaturePublicKey> + '_ {
494            self.group
495                .members()
496                .map(|m| m.signature_key)
497                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
498        }
499
500        pub fn encryption_keys(&self) -> impl Iterator<Item = Vec<u8>> + '_ {
501            self.group.members().map(|m| m.encryption_key)
502        }
503
504        pub fn extensions(&self) -> &openmls::prelude::Extensions {
505            self.group.export_group_context().extensions()
506        }
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::test_utils::*;
514    use wasm_bindgen_test::*;
515
516    wasm_bindgen_test_configure!(run_in_browser);
517
518    #[apply(all_cred_cipher)]
519    #[wasm_bindgen_test]
520    pub async fn create_self_conversation_should_succeed(case: TestContext) {
521        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
522            Box::pin(async move {
523                let id = conversation_id();
524                alice_central
525                    .transaction
526                    .new_conversation(&id, case.credential_type, case.cfg.clone())
527                    .await
528                    .unwrap();
529                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
530                assert_eq!(
531                    alice_central
532                        .get_conversation_unchecked(&id)
533                        .await
534                        .group
535                        .group_id()
536                        .as_slice(),
537                    id
538                );
539                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
540                let alice_can_send_message = alice_central
541                    .transaction
542                    .conversation(&id)
543                    .await
544                    .unwrap()
545                    .encrypt_message(b"me")
546                    .await;
547                assert!(alice_can_send_message.is_ok());
548            })
549        })
550        .await;
551    }
552
553    #[apply(all_cred_cipher)]
554    #[wasm_bindgen_test]
555    pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
556        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
557            Box::pin(async move {
558                let id = conversation_id();
559
560                alice_central
561                    .transaction
562                    .new_conversation(&id, case.credential_type, case.cfg.clone())
563                    .await
564                    .unwrap();
565
566                let bob = bob_central.rand_key_package(&case).await;
567                alice_central
568                    .transaction
569                    .conversation(&id)
570                    .await
571                    .unwrap()
572                    .add_members(vec![bob])
573                    .await
574                    .unwrap();
575
576                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
577                assert_eq!(
578                    alice_central
579                        .get_conversation_unchecked(&id)
580                        .await
581                        .group
582                        .group_id()
583                        .as_slice(),
584                    id
585                );
586                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
587
588                let welcome = alice_central.mls_transport.latest_welcome_message().await;
589                bob_central
590                    .transaction
591                    .process_welcome_message(welcome.into(), case.custom_cfg())
592                    .await
593                    .unwrap();
594
595                assert_eq!(
596                    bob_central.get_conversation_unchecked(&id).await.id(),
597                    alice_central.get_conversation_unchecked(&id).await.id()
598                );
599                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
600            })
601        })
602        .await;
603    }
604
605    #[apply(all_cred_cipher)]
606    #[wasm_bindgen_test]
607    pub async fn create_many_people_conversation(case: TestContext) {
608        use crate::e2e_identity::enrollment::test_utils::failsafe_ctx;
609
610        run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
611            Box::pin(async move {
612                let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
613                let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
614
615                let id = conversation_id();
616                alice_central
617                    .transaction
618                    .new_conversation(&id, case.credential_type, case.cfg.clone())
619                    .await
620                    .unwrap();
621                let bob_and_friends = case.sessions_x509::<GROUP_SAMPLE_SIZE>(Some(x509_test_chain)).await;
622
623                let mut bob_and_friends_kps = vec![];
624                for c in &bob_and_friends {
625                    bob_and_friends_kps.push(c.rand_key_package(&case).await);
626                }
627
628                alice_central
629                    .transaction
630                    .conversation(&id)
631                    .await
632                    .unwrap()
633                    .add_members(bob_and_friends_kps)
634                    .await
635                    .unwrap();
636                let welcome = alice_central.mls_transport.latest_welcome_message().await;
637
638                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
639                assert_eq!(
640                    alice_central
641                        .get_conversation_unchecked(&id)
642                        .await
643                        .group
644                        .group_id()
645                        .as_slice(),
646                    id
647                );
648                assert_eq!(
649                    alice_central.get_conversation_unchecked(&id).await.members().len(),
650                    1 + GROUP_SAMPLE_SIZE
651                );
652
653                let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
654                for c in bob_and_friends {
655                    c.transaction
656                        .process_welcome_message(welcome.clone().into(), case.custom_cfg())
657                        .await
658                        .unwrap();
659                    assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
660                    bob_and_friends_groups.push(c);
661                }
662
663                assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
664            })
665        })
666        .await;
667    }
668
669    mod wire_identity_getters {
670        use wasm_bindgen_test::*;
671
672        use super::Error;
673        use crate::mls::conversation::Conversation as _;
674        use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
675        use crate::transaction_context::TransactionContext;
676        use crate::{
677            prelude::{DeviceStatus, E2eiConversationState},
678            test_utils::*,
679        };
680
681        wasm_bindgen_test_configure!(run_in_browser);
682
683        async fn all_identities_check<const N: usize>(
684            central: &TransactionContext,
685            id: &ConversationId,
686            user_ids: &[String; N],
687            expected_sizes: [usize; N],
688        ) {
689            let all_identities = central
690                .conversation(id)
691                .await
692                .unwrap()
693                .get_user_identities(user_ids)
694                .await
695                .unwrap();
696            assert_eq!(all_identities.len(), N);
697            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
698                let alice_identities = all_identities.get(user_id).unwrap();
699                assert_eq!(alice_identities.len(), expected_size);
700            }
701            // Not found
702            let not_found = central
703                .conversation(id)
704                .await
705                .unwrap()
706                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
707                .await
708                .unwrap();
709            assert!(not_found.is_empty());
710
711            // Invalid usage
712            let invalid = central.conversation(id).await.unwrap().get_user_identities(&[]).await;
713            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
714        }
715
716        async fn check_identities_device_status<const N: usize>(
717            central: &TransactionContext,
718            id: &ConversationId,
719            client_ids: &[ClientId; N],
720            name_status: &[(&'static str, DeviceStatus); N],
721        ) {
722            let mut identities = central
723                .conversation(id)
724                .await
725                .unwrap()
726                .get_device_identities(client_ids)
727                .await
728                .unwrap();
729
730            for j in 0..N {
731                let client_identity = identities.remove(
732                    identities
733                        .iter()
734                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
735                        .unwrap(),
736                );
737                assert_eq!(client_identity.status, name_status[j].1);
738            }
739            assert!(identities.is_empty());
740
741            assert_eq!(
742                central
743                    .conversation(id)
744                    .await
745                    .unwrap()
746                    .e2ei_conversation_state()
747                    .await
748                    .unwrap(),
749                E2eiConversationState::NotVerified
750            );
751        }
752
753        #[async_std::test]
754        #[wasm_bindgen_test]
755        async fn should_read_device_identities() {
756            let case = TestContext::default_x509();
757            run_test_with_client_ids(
758                case.clone(),
759                ["alice_android", "alice_ios"],
760                move |[alice_android_central, alice_ios_central]| {
761                    Box::pin(async move {
762                        let id = conversation_id();
763                        alice_android_central
764                            .transaction
765                            .new_conversation(&id, case.credential_type, case.cfg.clone())
766                            .await
767                            .unwrap();
768                        alice_android_central
769                            .invite_all(&case, &id, [&alice_ios_central])
770                            .await
771                            .unwrap();
772
773                        let (android_id, ios_id) = (
774                            alice_android_central.get_client_id().await,
775                            alice_ios_central.get_client_id().await,
776                        );
777
778                        let mut android_ids = alice_android_central
779                            .transaction
780                            .conversation(&id)
781                            .await
782                            .unwrap()
783                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
784                            .await
785                            .unwrap();
786                        android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
787                        assert_eq!(android_ids.len(), 2);
788                        let mut ios_ids = alice_ios_central
789                            .transaction
790                            .conversation(&id)
791                            .await
792                            .unwrap()
793                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
794                            .await
795                            .unwrap();
796                        ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
797                        assert_eq!(ios_ids.len(), 2);
798
799                        assert_eq!(android_ids, ios_ids);
800
801                        let android_identities = alice_android_central
802                            .transaction
803                            .conversation(&id)
804                            .await
805                            .unwrap()
806                            .get_device_identities(&[android_id])
807                            .await
808                            .unwrap();
809                        let android_id = android_identities.first().unwrap();
810                        assert_eq!(
811                            android_id.client_id.as_bytes(),
812                            alice_android_central
813                                .transaction
814                                .client_id()
815                                .await
816                                .unwrap()
817                                .0
818                                .as_slice()
819                        );
820
821                        let ios_identities = alice_android_central
822                            .transaction
823                            .conversation(&id)
824                            .await
825                            .unwrap()
826                            .get_device_identities(&[ios_id])
827                            .await
828                            .unwrap();
829                        let ios_id = ios_identities.first().unwrap();
830                        assert_eq!(
831                            ios_id.client_id.as_bytes(),
832                            alice_ios_central.transaction.client_id().await.unwrap().0.as_slice()
833                        );
834
835                        let invalid = alice_android_central
836                            .transaction
837                            .conversation(&id)
838                            .await
839                            .unwrap()
840                            .get_device_identities(&[])
841                            .await;
842                        assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
843                    })
844                },
845            )
846            .await
847        }
848
849        #[async_std::test]
850        #[wasm_bindgen_test]
851        async fn should_read_revoked_device_cross_signed() {
852            let case = TestContext::default_x509();
853            run_test_with_client_ids_and_revocation(
854                case.clone(),
855                ["alice", "bob", "rupert"],
856                ["john", "dilbert"],
857                &["rupert", "dilbert"],
858                move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
859                    Box::pin(async move {
860                        let id = conversation_id();
861                        alice
862                            .transaction
863                            .new_conversation(&id, case.credential_type, case.cfg.clone())
864                            .await
865                            .unwrap();
866                        alice
867                            .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
868                            .await
869                            .unwrap();
870
871                        let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
872                            alice.get_client_id().await,
873                            bob.get_client_id().await,
874                            rupert.get_client_id().await,
875                            dilbert.get_client_id().await,
876                            john.get_client_id().await,
877                        );
878
879                        let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
880                        let name_status = [
881                            ("alice", DeviceStatus::Valid),
882                            ("bob", DeviceStatus::Valid),
883                            ("rupert", DeviceStatus::Revoked),
884                            ("john", DeviceStatus::Valid),
885                            ("dilbert", DeviceStatus::Revoked),
886                        ];
887                        // Do it a multiple times to avoid WPB-6904 happening again
888                        for _ in 0..2 {
889                            check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
890                                .await;
891                            check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
892                            check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
893                                .await;
894                            check_identities_device_status(&mut john.transaction, &id, &client_ids, &name_status).await;
895                            check_identities_device_status(&mut dilbert.transaction, &id, &client_ids, &name_status)
896                                .await;
897                        }
898                    })
899                },
900            )
901            .await
902        }
903
904        #[async_std::test]
905        #[wasm_bindgen_test]
906        async fn should_read_revoked_device() {
907            let case = TestContext::default_x509();
908            run_test_with_client_ids_and_revocation(
909                case.clone(),
910                ["alice", "bob", "rupert"],
911                [],
912                &["rupert"],
913                move |[mut alice, mut bob, mut rupert], []| {
914                    Box::pin(async move {
915                        let id = conversation_id();
916                        alice
917                            .transaction
918                            .new_conversation(&id, case.credential_type, case.cfg.clone())
919                            .await
920                            .unwrap();
921                        alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
922
923                        let (alice_id, bob_id, rupert_id) = (
924                            alice.get_client_id().await,
925                            bob.get_client_id().await,
926                            rupert.get_client_id().await,
927                        );
928
929                        let client_ids = [alice_id, bob_id, rupert_id];
930                        let name_status = [
931                            ("alice", DeviceStatus::Valid),
932                            ("bob", DeviceStatus::Valid),
933                            ("rupert", DeviceStatus::Revoked),
934                        ];
935
936                        // Do it a multiple times to avoid WPB-6904 happening again
937                        for _ in 0..2 {
938                            check_identities_device_status(&mut alice.transaction, &id, &client_ids, &name_status)
939                                .await;
940                            check_identities_device_status(&mut bob.transaction, &id, &client_ids, &name_status).await;
941                            check_identities_device_status(&mut rupert.transaction, &id, &client_ids, &name_status)
942                                .await;
943                        }
944                    })
945                },
946            )
947            .await
948        }
949
950        #[async_std::test]
951        #[wasm_bindgen_test]
952        async fn should_not_fail_when_basic() {
953            let case = TestContext::default();
954            run_test_with_client_ids(
955                case.clone(),
956                ["alice_android", "alice_ios"],
957                move |[alice_android_central, alice_ios_central]| {
958                    Box::pin(async move {
959                        let id = conversation_id();
960                        alice_android_central
961                            .transaction
962                            .new_conversation(&id, case.credential_type, case.cfg.clone())
963                            .await
964                            .unwrap();
965                        alice_android_central
966                            .invite_all(&case, &id, [&alice_ios_central])
967                            .await
968                            .unwrap();
969
970                        let (android_id, ios_id) = (
971                            alice_android_central.get_client_id().await,
972                            alice_ios_central.get_client_id().await,
973                        );
974
975                        let mut android_ids = alice_android_central
976                            .transaction
977                            .conversation(&id)
978                            .await
979                            .unwrap()
980                            .get_device_identities(&[android_id.clone(), ios_id.clone()])
981                            .await
982                            .unwrap();
983                        android_ids.sort();
984
985                        let mut ios_ids = alice_ios_central
986                            .transaction
987                            .conversation(&id)
988                            .await
989                            .unwrap()
990                            .get_device_identities(&[android_id, ios_id])
991                            .await
992                            .unwrap();
993                        ios_ids.sort();
994
995                        assert_eq!(ios_ids.len(), 2);
996                        assert_eq!(ios_ids, android_ids);
997
998                        assert!(ios_ids.iter().all(|i| {
999                            matches!(i.credential_type, MlsCredentialType::Basic)
1000                                && matches!(i.status, DeviceStatus::Valid)
1001                                && i.x509_identity.is_none()
1002                                && !i.thumbprint.is_empty()
1003                                && !i.client_id.is_empty()
1004                        }));
1005                    })
1006                },
1007            )
1008            .await
1009        }
1010
1011        // this test is a duplicate of its counterpart but taking federation into account
1012        // The heavy lifting of cross-signing the certificates is being done by the test utils.
1013        #[async_std::test]
1014        #[wasm_bindgen_test]
1015        async fn should_read_users_cross_signed() {
1016            let case = TestContext::default_x509();
1017
1018            let (alice_android, alice_ios) = (
1019                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1020                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1021            );
1022            let (alicem_android, alicem_ios) = (
1023                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
1024                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
1025            );
1026            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1027            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
1028
1029            run_test_with_deterministic_client_ids_and_revocation(
1030                case.clone(),
1031                [
1032                    [alice_android, "alice_wire", "Alice Smith"],
1033                    [alice_ios, "alice_wire", "Alice Smith"],
1034                    [bob_android, "bob_wire", "Bob Doe"],
1035                ],
1036                [
1037                    [alicem_android, "alice_zeta", "Alice Muller"],
1038                    [alicem_ios, "alice_zeta", "Alice Muller"],
1039                    [bobt_android, "bob_zeta", "Bob Tables"],
1040                ],
1041                &[],
1042                move |[alice_android_central, alice_ios_central, bob_android_central],
1043                      [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1044                    Box::pin(async move {
1045                        let id = conversation_id();
1046                        alice_android_central
1047                            .transaction
1048                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1049                            .await
1050                            .unwrap();
1051                        alice_android_central
1052                            .invite_all(
1053                                &case,
1054                                &id,
1055                                [
1056                                    &alice_ios_central,
1057                                    &bob_android_central,
1058                                    &bobt_android_central,
1059                                    &alicem_ios_central,
1060                                    &alicem_android_central,
1061                                ],
1062                            )
1063                            .await
1064                            .unwrap();
1065
1066                        let nb_members = alice_android_central
1067                            .get_conversation_unchecked(&id)
1068                            .await
1069                            .members()
1070                            .len();
1071                        assert_eq!(nb_members, 6);
1072
1073                        assert_eq!(
1074                            alice_android_central.get_user_id().await,
1075                            alice_ios_central.get_user_id().await
1076                        );
1077
1078                        let alicem_user_id = alicem_ios_central.get_user_id().await;
1079                        let bobt_user_id = bobt_android_central.get_user_id().await;
1080
1081                        // Finds both Alice's devices
1082                        let alice_user_id = alice_android_central.get_user_id().await;
1083                        let alice_identities = alice_android_central
1084                            .transaction
1085                            .conversation(&id)
1086                            .await
1087                            .unwrap()
1088                            .get_user_identities(&[alice_user_id.clone()])
1089                            .await
1090                            .unwrap();
1091                        assert_eq!(alice_identities.len(), 1);
1092                        let identities = alice_identities.get(&alice_user_id).unwrap();
1093                        assert_eq!(identities.len(), 2);
1094
1095                        // Finds Bob only device
1096                        let bob_user_id = bob_android_central.get_user_id().await;
1097                        let bob_identities = alice_android_central
1098                            .transaction
1099                            .conversation(&id)
1100                            .await
1101                            .unwrap()
1102                            .get_user_identities(&[bob_user_id.clone()])
1103                            .await
1104                            .unwrap();
1105                        assert_eq!(bob_identities.len(), 1);
1106                        let identities = bob_identities.get(&bob_user_id).unwrap();
1107                        assert_eq!(identities.len(), 1);
1108
1109                        // Finds all devices
1110                        let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
1111                        let expected_sizes = [2, 1, 2, 1];
1112
1113                        all_identities_check(&alice_android_central.transaction, &id, &user_ids, expected_sizes).await;
1114                        all_identities_check(&alicem_android_central.transaction, &id, &user_ids, expected_sizes).await;
1115                        all_identities_check(&alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1116                        all_identities_check(&alicem_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1117                        all_identities_check(&bob_android_central.transaction, &id, &user_ids, expected_sizes).await;
1118                        all_identities_check(&bobt_android_central.transaction, &id, &user_ids, expected_sizes).await;
1119                    })
1120                },
1121            )
1122            .await
1123        }
1124
1125        #[async_std::test]
1126        #[wasm_bindgen_test]
1127        async fn should_read_users() {
1128            let case = TestContext::default_x509();
1129
1130            let (alice_android, alice_ios) = (
1131                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
1132                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
1133            );
1134            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
1135
1136            run_test_with_deterministic_client_ids(
1137                case.clone(),
1138                [
1139                    [alice_android, "alice_wire", "Alice Smith"],
1140                    [alice_ios, "alice_wire", "Alice Smith"],
1141                    [bob_android, "bob_wire", "Bob Doe"],
1142                ],
1143                move |[
1144                    mut alice_android_central,
1145                    mut alice_ios_central,
1146                    mut bob_android_central,
1147                ]| {
1148                    Box::pin(async move {
1149                        let id = conversation_id();
1150                        alice_android_central
1151                            .transaction
1152                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1153                            .await
1154                            .unwrap();
1155                        alice_android_central
1156                            .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
1157                            .await
1158                            .unwrap();
1159
1160                        let nb_members = alice_android_central
1161                            .get_conversation_unchecked(&id)
1162                            .await
1163                            .members()
1164                            .len();
1165                        assert_eq!(nb_members, 3);
1166
1167                        assert_eq!(
1168                            alice_android_central.get_user_id().await,
1169                            alice_ios_central.get_user_id().await
1170                        );
1171
1172                        // Finds both Alice's devices
1173                        let alice_user_id = alice_android_central.get_user_id().await;
1174                        let alice_identities = alice_android_central
1175                            .transaction
1176                            .conversation(&id)
1177                            .await
1178                            .unwrap()
1179                            .get_user_identities(&[alice_user_id.clone()])
1180                            .await
1181                            .unwrap();
1182                        assert_eq!(alice_identities.len(), 1);
1183                        let identities = alice_identities.get(&alice_user_id).unwrap();
1184                        assert_eq!(identities.len(), 2);
1185
1186                        // Finds Bob only device
1187                        let bob_user_id = bob_android_central.get_user_id().await;
1188                        let bob_identities = alice_android_central
1189                            .transaction
1190                            .conversation(&id)
1191                            .await
1192                            .unwrap()
1193                            .get_user_identities(&[bob_user_id.clone()])
1194                            .await
1195                            .unwrap();
1196                        assert_eq!(bob_identities.len(), 1);
1197                        let identities = bob_identities.get(&bob_user_id).unwrap();
1198                        assert_eq!(identities.len(), 1);
1199
1200                        let user_ids = [alice_user_id, bob_user_id];
1201                        let expected_sizes = [2, 1];
1202
1203                        all_identities_check(&mut alice_android_central.transaction, &id, &user_ids, expected_sizes)
1204                            .await;
1205                        all_identities_check(&mut alice_ios_central.transaction, &id, &user_ids, expected_sizes).await;
1206                        all_identities_check(&mut bob_android_central.transaction, &id, &user_ids, expected_sizes)
1207                            .await;
1208                    })
1209                },
1210            )
1211            .await
1212        }
1213
1214        #[async_std::test]
1215        #[wasm_bindgen_test]
1216        async fn should_exchange_messages_cross_signed() {
1217            let (alice_android, alice_ios) = (
1218                "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
1219                "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
1220            );
1221            let (alicem_android, alicem_ios) = (
1222                "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
1223                "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
1224            );
1225            let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
1226            let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
1227
1228            let case = TestContext::default_x509();
1229
1230            run_cross_signed_tests_with_client_ids(
1231                case.clone(),
1232                [
1233                    [alice_android, "alice_wire", "Alice Smith"],
1234                    [alice_ios, "alice_wire", "Alice Smith"],
1235                    [bob_android, "bob_wire", "Bob Doe"],
1236                ],
1237                [
1238                    [alicem_android, "alice_zeta", "Alice Muller"],
1239                    [alicem_ios, "alice_zeta", "Alice Muller"],
1240                    [bobt_android, "bob_zeta", "Bob Tables"],
1241                ],
1242                ("wire.com", "zeta.com"),
1243                move |[alices_android_central, alices_ios_central, bob_android_central],
1244                      [alicem_android_central, alicem_ios_central, bobt_android_central]| {
1245                    Box::pin(async move {
1246                        let id = conversation_id();
1247                        alices_ios_central
1248                            .transaction
1249                            .new_conversation(&id, case.credential_type, case.cfg.clone())
1250                            .await
1251                            .unwrap();
1252
1253                        alices_ios_central
1254                            .invite_all(
1255                                &case,
1256                                &id,
1257                                [
1258                                    &alices_android_central,
1259                                    &bob_android_central,
1260                                    &alicem_android_central,
1261                                    &alicem_ios_central,
1262                                    &bobt_android_central,
1263                                ],
1264                            )
1265                            .await
1266                            .unwrap();
1267
1268                        let nb_members = alices_android_central
1269                            .get_conversation_unchecked(&id)
1270                            .await
1271                            .members()
1272                            .len();
1273                        assert_eq!(nb_members, 6);
1274
1275                        assert_eq!(
1276                            alicem_android_central.get_user_id().await,
1277                            alicem_ios_central.get_user_id().await
1278                        );
1279
1280                        // cross server communication
1281                        bobt_android_central
1282                            .try_talk_to(&id, &alices_ios_central)
1283                            .await
1284                            .unwrap();
1285
1286                        // same server communication
1287                        bob_android_central.try_talk_to(&id, &alices_ios_central).await.unwrap();
1288                    })
1289                },
1290            )
1291            .await;
1292        }
1293    }
1294
1295    mod export_secret {
1296        use super::*;
1297        use crate::MlsErrorKind;
1298        use openmls::prelude::ExportSecretError;
1299
1300        #[apply(all_cred_cipher)]
1301        #[wasm_bindgen_test]
1302        pub async fn can_export_secret_key(case: TestContext) {
1303            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1304                Box::pin(async move {
1305                    let id = conversation_id();
1306                    alice_central
1307                        .transaction
1308                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1309                        .await
1310                        .unwrap();
1311
1312                    let key_length = 128;
1313                    let result = alice_central
1314                        .transaction
1315                        .conversation(&id)
1316                        .await
1317                        .unwrap()
1318                        .export_secret_key(key_length)
1319                        .await;
1320                    assert!(result.is_ok());
1321                    assert_eq!(result.unwrap().len(), key_length);
1322                })
1323            })
1324            .await
1325        }
1326
1327        #[apply(all_cred_cipher)]
1328        #[wasm_bindgen_test]
1329        pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
1330            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1331                Box::pin(async move {
1332                    let id = conversation_id();
1333                    alice_central
1334                        .transaction
1335                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1336                        .await
1337                        .unwrap();
1338
1339                    let result = alice_central
1340                        .transaction
1341                        .conversation(&id)
1342                        .await
1343                        .unwrap()
1344                        .export_secret_key(usize::MAX)
1345                        .await;
1346                    let error = result.unwrap_err();
1347                    assert!(innermost_source_matches!(
1348                        error,
1349                        MlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
1350                    ));
1351                })
1352            })
1353            .await
1354        }
1355    }
1356
1357    mod get_client_ids {
1358        use super::*;
1359
1360        #[apply(all_cred_cipher)]
1361        #[wasm_bindgen_test]
1362        pub async fn can_get_client_ids(case: TestContext) {
1363            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1364                Box::pin(async move {
1365                    let id = conversation_id();
1366                    alice_central
1367                        .transaction
1368                        .new_conversation(&id, case.credential_type, case.cfg.clone())
1369                        .await
1370                        .unwrap();
1371
1372                    assert_eq!(
1373                        alice_central
1374                            .transaction
1375                            .conversation(&id)
1376                            .await
1377                            .unwrap()
1378                            .get_client_ids()
1379                            .await
1380                            .len(),
1381                        1
1382                    );
1383
1384                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1385                    assert_eq!(
1386                        alice_central
1387                            .transaction
1388                            .conversation(&id)
1389                            .await
1390                            .unwrap()
1391                            .get_client_ids()
1392                            .await
1393                            .len(),
1394                        2
1395                    );
1396                })
1397            })
1398            .await
1399        }
1400    }
1401
1402    mod external_sender {
1403        use super::*;
1404
1405        #[apply(all_cred_cipher)]
1406        #[wasm_bindgen_test]
1407        pub async fn should_fetch_ext_sender(case: TestContext) {
1408            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
1409                Box::pin(async move {
1410                    let id = conversation_id();
1411
1412                    // by default in test no external sender is set. Let's add one
1413                    let mut cfg = case.cfg.clone();
1414                    let external_sender = alice_central.rand_external_sender(&case).await;
1415                    cfg.external_senders = vec![external_sender.clone()];
1416
1417                    alice_central
1418                        .transaction
1419                        .new_conversation(&id, case.credential_type, cfg)
1420                        .await
1421                        .unwrap();
1422
1423                    let alice_ext_sender = alice_central
1424                        .transaction
1425                        .conversation(&id)
1426                        .await
1427                        .unwrap()
1428                        .get_external_sender()
1429                        .await
1430                        .unwrap();
1431                    assert!(!alice_ext_sender.is_empty());
1432                    assert_eq!(alice_ext_sender, external_sender.signature_key().as_slice().to_vec());
1433                })
1434            })
1435            .await
1436        }
1437    }
1438}