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