core_crypto/mls/conversation/
mod.rs1mod 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 #[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 let not_found = conversation.get_user_identities(&[Uuid::new_v4()]).await.unwrap();
119 assert!(not_found.is_empty());
120
121 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 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 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 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}