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 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 #[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 let not_found = conversation.get_user_identities(&[Uuid::new_v4()]).await.unwrap();
117 assert!(not_found.is_empty());
118
119 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 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 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 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}