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