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