core_crypto/mls/conversation/
mod.rs1mod commit;
15mod config;
16mod error;
17mod group_info;
18mod group_metadata;
19mod id;
20mod immutable;
21mod mutable;
22mod orphan_welcome;
23mod pending;
24mod welcome;
25
26pub(crate) use immutable::MlsGroupState;
27pub(crate) use mutable::TntMessageCounter;
28pub(crate) use pending::PendingConversation;
29
30pub use self::{
31 commit::CommitBundle,
32 config::{ConversationConfiguration, CustomConfiguration, WirePolicy},
33 error::{Error, Result},
34 group_info::{GroupInfoBundle, GroupInfoEncryptionType, GroupInfoPayload, RatchetTreeType},
35 id::{ConversationId, ConversationIdRef},
36 immutable::Conversation,
37 mutable::{
38 ConversationMut, TargetedMessagePolicy,
39 decrypt::{BufferedCommit, BufferedDecryptedMessage, Commit, DecryptedBytes, DecryptedMessage, Proposal},
40 },
41 welcome::WelcomeMessage,
42};
43use crate::bytes_wrapper;
44
45bytes_wrapper!(
46 #[derive(Clone)]
50 SecretKey
51);
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::test_utils::*;
57
58 #[apply(all_cred_cipher)]
59 pub async fn create_self_conversation_should_succeed(case: TestContext) {
60 let [alice] = case.sessions().await;
61 Box::pin(async move {
62 let conversation = case.create_conversation([&alice]).await;
63 assert_eq!(1, conversation.member_count().await);
64 let alice_can_send_message = conversation.guard().await.encrypt_message(b"me").await;
65 assert!(alice_can_send_message.is_ok());
66 })
67 .await;
68 }
69
70 #[apply(all_cred_cipher)]
71 pub async fn create_1_1_conversation_should_succeed(case: TestContext) {
72 let [alice, bob] = case.sessions().await;
73 Box::pin(async move {
74 let conversation = case.create_conversation([&alice, &bob]).await;
75 assert_eq!(2, conversation.member_count().await);
76 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
77 })
78 .await;
79 }
80
81 #[apply(all_cred_cipher)]
82 pub async fn create_many_people_conversation(mut case: TestContext) {
83 case.sessions_in_memory = true;
84 const SIZE_PLUS_1: usize = GROUP_SAMPLE_SIZE + 1;
85 let alice_and_friends = case.sessions::<SIZE_PLUS_1>().await;
86 Box::pin(async move {
87 let alice = &alice_and_friends[0];
88 let conversation = case.create_conversation([alice]).await;
89
90 let bob_and_friends = &alice_and_friends[1..];
91 let conversation = conversation.invite_notify(bob_and_friends).await;
92
93 assert_eq!(conversation.member_count().await, 1 + GROUP_SAMPLE_SIZE);
94 assert!(conversation.is_functional_and_contains(&alice_and_friends).await);
95 })
96 .await;
97 }
98
99 mod wire_identity_getters {
100 use uuid::Uuid;
101 use wire_e2e_identity::IdentityStatus;
102
103 use super::Error;
104 use crate::{ClientId, CredentialType, E2eiConversationState, mls::conversation::Conversation, test_utils::*};
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: &[IdentityStatus; 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 = [IdentityStatus::Valid, IdentityStatus::Valid, IdentityStatus::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 let [alice_android, alice_ios] = case.sessions().await;
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_by_key(|id| id.client_id.clone());
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_by_key(|id| id.client_id.clone());
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, IdentityStatus::Valid)
281 && i.x509_identity.is_none()
282 && !i.thumbprint.is_empty()
283 && i.client_id.is_some()
284 }));
285 }
286
287 #[macro_rules_attribute::apply(smol_macros::test)]
288 async fn should_read_users() {
289 let case = TestContext::default_x509();
290 let [alice_android, alice_ios] = case.client_ids_for_user(uuid::Uuid::new_v4());
291 let [bob_android] = case.client_ids();
292
293 let sessions = case
294 .sessions_x509_with_client_ids([alice_android, alice_ios, bob_android])
295 .await;
296
297 Box::pin(async move {
298 let conversation = case.create_conversation(&sessions).await;
299
300 let nb_members = conversation.member_count().await;
301 assert_eq!(nb_members, 3);
302
303 let [alice_android, alice_ios, bob_android] = &sessions;
304 assert_eq!(alice_android.get_user_id().await, alice_ios.get_user_id().await);
305
306 let alice_user_id = alice_android.get_user_id().await;
308 let alice_identities = conversation
309 .guard()
310 .await
311 .get_user_identities(std::slice::from_ref(&alice_user_id))
312 .await
313 .unwrap();
314 assert_eq!(alice_identities.len(), 1);
315 let identities = alice_identities.get(&alice_user_id).unwrap();
316 assert_eq!(identities.len(), 2);
317
318 let bob_user_id = bob_android.get_user_id().await;
320 let bob_identities = conversation
321 .guard()
322 .await
323 .get_user_identities(std::slice::from_ref(&bob_user_id))
324 .await
325 .unwrap();
326 assert_eq!(bob_identities.len(), 1);
327 let identities = bob_identities.get(&bob_user_id).unwrap();
328 assert_eq!(identities.len(), 1);
329
330 let user_ids = [alice_user_id, bob_user_id];
331 let expected_sizes = [2, 1];
332
333 for session in &sessions {
334 all_identities_check(&*conversation.guard_of(session).await, &user_ids, expected_sizes).await;
335 }
336 })
337 .await
338 }
339 }
340
341 mod export_secret {
342 use openmls::prelude::ExportSecretError;
343
344 use super::*;
345 use crate::OpenMlsErrorKind;
346
347 #[apply(all_cred_cipher)]
348 pub async fn can_export_secret_key(case: TestContext) {
349 let [alice] = case.sessions().await;
350 Box::pin(async move {
351 let conversation = case.create_conversation([&alice]).await;
352
353 let key_length = 128;
354 let result = conversation.guard().await.export_secret_key(key_length).await;
355 assert!(result.is_ok());
356 assert_eq!(result.unwrap().len(), key_length);
357 })
358 .await
359 }
360
361 #[apply(all_cred_cipher)]
362 pub async fn cannot_export_secret_key_invalid_length(case: TestContext) {
363 let [alice] = case.sessions().await;
364 Box::pin(async move {
365 let conversation = case.create_conversation([&alice]).await;
366
367 let result = conversation.guard().await.export_secret_key(usize::MAX).await;
368 let error = result.unwrap_err();
369 assert!(innermost_source_matches!(
370 error,
371 OpenMlsErrorKind::MlsExportSecretError(ExportSecretError::KeyLengthTooLong)
372 ));
373 })
374 .await
375 }
376 }
377
378 mod get_client_ids {
379 use super::*;
380
381 #[apply(all_cred_cipher)]
382 pub async fn can_get_client_ids(case: TestContext) {
383 let [alice, bob] = case.sessions().await;
384 Box::pin(async move {
385 let conversation = case.create_conversation([&alice]).await;
386
387 assert_eq!(conversation.guard().await.get_client_ids().await.unwrap().len(), 1);
388
389 let conversation = conversation.invite_notify([&bob]).await;
390
391 assert_eq!(conversation.guard().await.get_client_ids().await.unwrap().len(), 2);
392 })
393 .await
394 }
395 }
396
397 mod external_sender {
398 use super::*;
399
400 #[apply(all_cred_cipher)]
401 pub async fn should_fetch_ext_sender(mut case: TestContext) {
402 let [alice, external_sender] = case.sessions().await;
403 Box::pin(async move {
404 use core_crypto_keystore::Sha256Hash;
405
406 let conversation = case
407 .create_conversation_with_external_sender(&external_sender, [&alice])
408 .await;
409
410 let alice_ext_sender = conversation.guard().await.get_external_sender().await.unwrap();
411 let signature_key: Vec<u8> = alice_ext_sender.signature_key().as_slice().to_vec();
412 assert!(!signature_key.is_empty());
413 assert_eq!(
414 Sha256Hash::hash_from(&signature_key),
415 external_sender.initial_credential.public_key_hash()
416 );
417 })
418 .await
419 }
420 }
421}