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