Skip to main content

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
14mod commit;
15mod config;
16mod error;
17mod group_info;
18mod id;
19mod immutable;
20mod mutable;
21mod orphan_welcome;
22mod pending;
23mod welcome;
24
25pub(crate) use pending::PendingConversation;
26
27pub use self::{
28    commit::CommitBundle,
29    config::{ConversationConfiguration, CustomConfiguration, WirePolicy},
30    error::{Error, Result},
31    group_info::{GroupInfoBundle, GroupInfoEncryptionType, GroupInfoPayload, RatchetTreeType},
32    id::{ConversationId, ConversationIdRef},
33    immutable::Conversation,
34    mutable::{
35        ConversationMut,
36        decrypt::{BufferedDecryptedMessage, DecryptedMessage},
37    },
38    welcome::WelcomeMessage,
39};
40use crate::bytes_wrapper;
41
42bytes_wrapper!(
43    /// A secret key derived from the group secret.
44    ///
45    /// This is intended to be used for AVS.
46    #[derive(Clone)]
47    SecretKey
48);
49
50bytes_wrapper!(
51    /// The raw public key of an external sender.
52    ///
53    /// This can be used to initialize a subconversation.
54    #[derive(Clone)]
55    ExternalSenderKey
56);
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::test_utils::*;
62
63    #[apply(all_cred_cipher)]
64    pub async fn create_self_conversation_should_succeed(case: TestContext) {
65        let [alice] = case.sessions().await;
66        Box::pin(async move {
67            let conversation = case.create_conversation([&alice]).await;
68            assert_eq!(1, conversation.member_count().await);
69            let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
70            assert!(alice_can_send_message.is_ok());
71        })
72        .await;
73    }
74
75    #[apply(all_cred_cipher)]
76    pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
77        let [alice, bob] = case.sessions().await;
78        Box::pin(async move {
79            let conversation = case.create_conversation([&alice, &bob]).await;
80            assert_eq!(2, conversation.member_count().await);
81            assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
82        })
83        .await;
84    }
85
86    #[apply(all_cred_cipher)]
87    pub async fn create_many_people_conversation(case: TestContext) {
88        const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
89        let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
90        Box::pin(async move {
91            let alice = &alice_and_friends[0];
92            let conversation = case.create_conversation([alice]).await;
93
94            let bob_and_friends = &alice_and_friends[1..];
95            let conversation = conversation.invite_notify(bob_and_friends).await;
96
97            assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
98            assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
99        })
100        .await;
101    }
102
103    mod wire_identity_getters {
104        use super::Error;
105        use crate::{
106            ClientId, CredentialType, DeviceStatus, E2eiConversationState, mls::conversation::Conversation,
107            test_utils::*,
108        };
109
110        async fn all_identities_check<const N: usize>(
111            conversation: &Conversation,
112            user_ids: &[String; N],
113            expected_sizes: [usize; N],
114        ) {
115            let all_identities = conversation.get_user_identities(user_ids).await.unwrap();
116            assert_eq!(all_identities.len(), N);
117            for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
118                let alice_identities = all_identities.get(user_id).unwrap();
119                assert_eq!(alice_identities.len(), expected_size);
120            }
121            // Not found
122            let not_found = conversation
123                .get_user_identities(&["aaaaaaaaaaaaa".to_string()])
124                .await
125                .unwrap();
126            assert!(not_found.is_empty());
127
128            // Invalid usage
129            let invalid = conversation.get_user_identities(&[]).await;
130            assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
131        }
132
133        async fn check_identities_device_status<const N: usize>(
134            conversation: &Conversation,
135            client_ids: &[ClientId; N],
136            name_status: &[(impl ToString, DeviceStatus); N],
137        ) {
138            let mut identities = conversation.get_device_identities(client_ids).await.unwrap();
139
140            for (user_name, status) in name_status.iter() {
141                let client_identity = identities.remove(
142                    identities
143                        .iter()
144                        .position(|i| i.x509_identity.as_ref().unwrap().display_name == user_name.to_string())
145                        .unwrap(),
146                );
147                assert_eq!(client_identity.status, *status);
148            }
149            assert!(identities.is_empty());
150
151            assert_eq!(
152                conversation.e2ei_conversation_state().await.unwrap(),
153                E2eiConversationState::NotVerified
154            );
155        }
156
157        // TODO: ignore this test for now, until we fix the test suite (WPB-25356)
158        #[ignore]
159        #[macro_rules_attribute::apply(smol_macros::test)]
160        async fn should_read_device_identities() {
161            let case = TestContext::default_x509();
162
163            let [alice_android, alice_ios] = case.sessions().await;
164            Box::pin(async move {
165                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
166
167                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
168
169                let mut android_ids = conversation
170                    .guard()
171                    .await
172                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
173                    .await
174                    .unwrap();
175                android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
176                assert_eq!(android_ids.len(), 2);
177                let mut ios_ids = conversation
178                    .guard_of(&alice_ios)
179                    .await
180                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
181                    .await
182                    .unwrap();
183                ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
184                assert_eq!(ios_ids.len(), 2);
185
186                assert_eq!(android_ids, ios_ids);
187
188                let android_identities = conversation
189                    .guard()
190                    .await
191                    .get_device_identities(&[android_id])
192                    .await
193                    .unwrap();
194                let android_id = android_identities.first().unwrap();
195                assert_eq!(
196                    android_id.client_id.as_bytes(),
197                    alice_android.transaction.client_id().await.unwrap().0.as_slice()
198                );
199
200                let ios_identities = conversation
201                    .guard()
202                    .await
203                    .get_device_identities(&[ios_id])
204                    .await
205                    .unwrap();
206                let ios_id = ios_identities.first().unwrap();
207                assert_eq!(
208                    ios_id.client_id.as_bytes(),
209                    alice_ios.transaction.client_id().await.unwrap().0.as_slice()
210                );
211
212                let empty_slice: &[ClientId] = &[];
213                let invalid = conversation.guard().await.get_device_identities(empty_slice).await;
214                assert!(matches!(invalid.unwrap_err(), Error::CallerError(_)));
215            })
216            .await
217        }
218
219        // TODO: ignore this test for now, until we fix the test suite (WPB-25356)
220        #[ignore]
221        #[macro_rules_attribute::apply(smol_macros::test)]
222        async fn should_read_revoked_device() {
223            let case = TestContext::default_x509();
224            let rupert_user_id = uuid::Uuid::new_v4();
225            let bob_user_id = uuid::Uuid::new_v4();
226            let alice_user_id = uuid::Uuid::new_v4();
227
228            let [rupert_client_id] = case.x509_client_ids_for_user(&rupert_user_id);
229            let [alice_client_id] = case.x509_client_ids_for_user(&alice_user_id);
230            let [bob_client_id] = case.x509_client_ids_for_user(&bob_user_id);
231
232            let sessions = case
233                .sessions_x509_with_client_ids_and_revocation(
234                    [alice_client_id.clone(), bob_client_id.clone(), rupert_client_id.clone()],
235                    &[rupert_user_id.to_string()],
236                )
237                .await;
238
239            Box::pin(async move {
240                let [alice, bob, rupert] = &sessions;
241                let conversation = case.create_conversation(&sessions).await;
242
243                let (alice_id, bob_id, rupert_id) = (
244                    alice.get_client_id().await,
245                    bob.get_client_id().await,
246                    rupert.get_client_id().await,
247                );
248
249                let client_ids = [alice_id, bob_id, rupert_id];
250                let name_status = [
251                    (alice_user_id, DeviceStatus::Valid),
252                    (bob_user_id, DeviceStatus::Valid),
253                    (rupert_user_id, DeviceStatus::Revoked),
254                ];
255
256                // Do it a multiple times to avoid WPB-6904 happening again
257                for _ in 0..2 {
258                    for session in sessions.iter() {
259                        let conversation = conversation.guard_of(session).await;
260                        check_identities_device_status(&conversation, &client_ids, &name_status).await;
261                    }
262                }
263            })
264            .await
265        }
266
267        #[macro_rules_attribute::apply(smol_macros::test)]
268        async fn should_not_fail_when_basic() {
269            let case = TestContext::default();
270
271            let [alice_android, alice_ios] = case.sessions().await;
272            Box::pin(async move {
273                let conversation = case.create_conversation([&alice_android, &alice_ios]).await;
274
275                let (android_id, ios_id) = (alice_android.get_client_id().await, alice_ios.get_client_id().await);
276
277                let mut android_ids = conversation
278                    .guard()
279                    .await
280                    .get_device_identities(&[android_id.clone(), ios_id.clone()])
281                    .await
282                    .unwrap();
283                android_ids.sort();
284
285                let mut ios_ids = conversation
286                    .guard_of(&alice_ios)
287                    .await
288                    .get_device_identities(&[android_id, ios_id])
289                    .await
290                    .unwrap();
291                ios_ids.sort();
292
293                assert_eq!(ios_ids.len(), 2);
294                assert_eq!(ios_ids, android_ids);
295
296                assert!(ios_ids.iter().all(|i| {
297                    matches!(i.credential_type, CredentialType::Basic)
298                        && matches!(i.status, DeviceStatus::Valid)
299                        && i.x509_identity.is_none()
300                        && !i.thumbprint.is_empty()
301                        && !i.client_id.is_empty()
302                }));
303            })
304            .await
305        }
306
307        // TODO: ignore this test for now, until we fix the test suite (WPB-25356)
308        #[ignore]
309        #[macro_rules_attribute::apply(smol_macros::test)]
310        async fn should_read_users() {
311            let case = TestContext::default_x509();
312            let [alice_android, alice_ios] = case.x509_client_ids_for_user(&uuid::Uuid::new_v4());
313            let [bob_android] = case.x509_client_ids();
314
315            let sessions = case
316                .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
317                .await;
318
319            Box::pin(async move {
320                let conversation = case.create_conversation(&sessions).await;
321
322                let nb_members = conversation.member_count().await;
323                assert_eq!(nb_members, 3);
324
325                let [alice_android, alice_ios, bob_android] = &sessions;
326                assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
327
328                // Finds both Alice's devices
329                let alice_user_id = alice_android.get_user_id().await;
330                let alice_identities = conversation
331                    .guard()
332                    .await
333                    .get_user_identities(std::slice::from_ref(&alice_user_id))
334                    .await
335                    .unwrap();
336                assert_eq!(alice_identities.len(), 1);
337                let identities = alice_identities.get(&alice_user_id).unwrap();
338                assert_eq!(identities.len(), 2);
339
340                // Finds Bob only device
341                let bob_user_id = bob_android.get_user_id().await;
342                let bob_identities = conversation
343                    .guard()
344                    .await
345                    .get_user_identities(std::slice::from_ref(&bob_user_id))
346                    .await
347                    .unwrap();
348                assert_eq!(bob_identities.len(), 1);
349                let identities = bob_identities.get(&bob_user_id).unwrap();
350                assert_eq!(identities.len(), 1);
351
352                let user_ids = [alice_user_id, bob_user_id];
353                let expected_sizes = [2, 1];
354
355                for session in &sessions {
356                    all_identities_check(&*conversation.guard_of(session).await, &user_ids, expected_sizes).await;
357                }
358            })
359            .await
360        }
361    }
362
363    mod export_secret {
364        use openmls::prelude::ExportSecretError;
365
366        use super::*;
367        use crate::OpenMlsErrorKind;
368
369        #[apply(all_cred_cipher)]
370        pub async fn can_export_secret_key(case: TestContext) {
371            let [alice] = case.sessions().await;
372            Box::pin(async move {
373                let conversation = case.create_conversation([&alice]).await;
374
375                let key_length = 128;
376                let result = conversation.guard().await.export_secret_key(key_length).await;
377                assert!(result.is_ok());
378                assert_eq!(result.unwrap().len(), key_length);
379            })
380            .await
381        }
382
383        #[apply(all_cred_cipher)]
384        pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
385            let [alice] = case.sessions().await;
386            Box::pin(async move {
387                let conversation = case.create_conversation([&alice]).await;
388
389                let result = conversation.guard().await.export_secret_key(usize::MAX).await;
390                let error = result.unwrap_err();
391                assert!(innermost_source_matches!(
392                    error,
393                    OpenMlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
394                ));
395            })
396            .await
397        }
398    }
399
400    mod get_client_ids {
401        use super::*;
402
403        #[apply(all_cred_cipher)]
404        pub async fn can_get_client_ids(case: TestContext) {
405            let [alice, bob] = case.sessions().await;
406            Box::pin(async move {
407                let conversation = case.create_conversation([&alice]).await;
408
409                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 1);
410
411                let conversation = conversation.invite_notify([&bob]).await;
412
413                assert_eq!(conversation.guard().await.get_client_ids().await.len(), 2);
414            })
415            .await
416        }
417    }
418
419    mod external_sender {
420        use super::*;
421
422        #[apply(all_cred_cipher)]
423        pub async fn should_fetch_ext_sender(mut case: TestContext) {
424            let [alice, external_sender] = case.sessions().await;
425            Box::pin(async move {
426                use core_crypto_keystore::Sha256Hash;
427
428                let conversation = case
429                    .create_conversation_with_external_sender(&external_sender, [&alice])
430                    .await;
431
432                let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
433                assert!(!alice_ext_sender.is_empty());
434                assert_eq!(
435                    Sha256Hash::hash_from(alice_ext_sender),
436                    external_sender.initial_credential.public_key_hash()
437                );
438            })
439            .await
440        }
441    }
442}