core_crypto/mls/conversation/
mod.rs1use std::collections::HashMap;
31
32use openmls::prelude::{CredentialWithKey, SignaturePublicKey};
33use openmls::{group::MlsGroup, prelude::Credential};
34use openmls_traits::types::SignatureScheme;
35
36use core_crypto_keystore::{Connection, CryptoKeystoreMls};
37use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
38
39use config::MlsConversationConfiguration;
40
41use crate::{
42 group_store::{GroupStore, GroupStoreValue},
43 mls::{client::Client, MlsCentral},
44 prelude::{CryptoError, CryptoResult, MlsCiphersuite, MlsCredentialType, MlsError},
45};
46
47use crate::context::CentralContext;
48
49mod buffer_messages;
50pub(crate) mod commit;
51mod commit_delay;
52pub(crate) mod config;
53#[cfg(test)]
54mod db_count;
55pub mod decrypt;
56mod duplicate;
57#[cfg(test)]
58mod durability;
59pub(crate) mod encrypt;
60pub(crate) mod export;
61pub(crate) mod external_sender;
62pub(crate) mod group_info;
63mod leaf_node_validation;
64pub(crate) mod merge;
65mod orphan_welcome;
66mod own_commit;
67pub(crate) mod proposal;
68mod renew;
69pub(crate) mod welcome;
70mod wipe;
71pub type ConversationId = Vec<u8>;
73
74#[derive(Debug)]
80#[allow(dead_code)]
81pub struct MlsConversation {
82 pub(crate) id: ConversationId,
83 pub(crate) parent_id: Option<ConversationId>,
84 pub(crate) group: MlsGroup,
85 configuration: MlsConversationConfiguration,
86}
87
88impl MlsConversation {
89 pub async fn create(
101 id: ConversationId,
102 author_client: &Client,
103 creator_credential_type: MlsCredentialType,
104 configuration: MlsConversationConfiguration,
105 backend: &MlsCryptoProvider,
106 ) -> CryptoResult<Self> {
107 let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
108 let cb = author_client
109 .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
110 .await?;
111
112 let group = MlsGroup::new_with_group_id(
113 backend,
114 &cb.signature_key,
115 &configuration.as_openmls_default_configuration()?,
116 openmls::prelude::GroupId::from_slice(id.as_slice()),
117 cb.to_mls_credential_with_key(),
118 )
119 .await
120 .map_err(MlsError::from)?;
121
122 let mut conversation = Self {
123 id,
124 group,
125 parent_id: None,
126 configuration,
127 };
128
129 conversation
130 .persist_group_when_changed(&backend.keystore(), true)
131 .await?;
132
133 Ok(conversation)
134 }
135
136 pub(crate) async fn from_mls_group(
138 group: MlsGroup,
139 configuration: MlsConversationConfiguration,
140 backend: &MlsCryptoProvider,
141 ) -> CryptoResult<Self> {
142 let id = ConversationId::from(group.group_id().as_slice());
143
144 let mut conversation = Self {
145 id,
146 group,
147 configuration,
148 parent_id: None,
149 };
150
151 conversation
152 .persist_group_when_changed(&backend.keystore(), true)
153 .await?;
154
155 Ok(conversation)
156 }
157
158 pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> CryptoResult<Self> {
160 let group: MlsGroup = core_crypto_keystore::deser(&buf)?;
161 let id = ConversationId::from(group.group_id().as_slice());
162 let configuration = MlsConversationConfiguration {
163 ciphersuite: group.ciphersuite().into(),
164 ..Default::default()
165 };
166
167 Ok(Self {
168 id,
169 group,
170 parent_id,
171 configuration,
172 })
173 }
174
175 pub fn id(&self) -> &ConversationId {
177 &self.id
178 }
179
180 pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
182 self.group.members().fold(HashMap::new(), |mut acc, kp| {
183 let credential = kp.credential;
184 let id = credential.identity().to_vec();
185 acc.entry(id).or_insert(credential);
186 acc
187 })
188 }
189
190 pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
192 self.group.members().fold(HashMap::new(), |mut acc, kp| {
193 let credential = kp.credential;
194 let id = credential.identity().to_vec();
195 let signature_key = SignaturePublicKey::from(kp.signature_key);
196 let credential = CredentialWithKey {
197 credential,
198 signature_key,
199 };
200 acc.entry(id).or_insert(credential);
201 acc
202 })
203 }
204
205 pub(crate) async fn persist_group_when_changed(
206 &mut self,
207 keystore: &CryptoKeystore,
208 force: bool,
209 ) -> CryptoResult<()> {
210 if force || self.group.state_changed() == openmls::group::InnerState::Changed {
211 keystore
212 .mls_group_persist(
213 &self.id,
214 &core_crypto_keystore::ser(&self.group)?,
215 self.parent_id.as_deref(),
216 )
217 .await?;
218
219 self.group.set_state(openmls::group::InnerState::Persisted);
220 }
221
222 Ok(())
223 }
224
225 pub async fn mark_as_child_of(&mut self, parent_id: &ConversationId, keystore: &Connection) -> CryptoResult<()> {
228 if keystore.mls_group_exists(parent_id).await {
229 self.parent_id = Some(parent_id.clone());
230 self.persist_group_when_changed(keystore, true).await?;
231 Ok(())
232 } else {
233 Err(CryptoError::ParentGroupNotFound)
234 }
235 }
236
237 pub(crate) fn own_credential_type(&self) -> CryptoResult<MlsCredentialType> {
238 Ok(self
239 .group
240 .own_leaf_node()
241 .ok_or(CryptoError::InternalMlsError)?
242 .credential()
243 .credential_type()
244 .into())
245 }
246
247 pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
248 self.configuration.ciphersuite
249 }
250
251 pub(crate) fn signature_scheme(&self) -> SignatureScheme {
252 self.ciphersuite().signature_algorithm()
253 }
254}
255
256impl MlsCentral {
257 pub(crate) async fn get_conversation(&self, id: &ConversationId) -> CryptoResult<Option<MlsConversation>> {
258 GroupStore::fetch_from_keystore(id, &self.mls_backend.keystore(), None).await
259 }
260}
261
262impl CentralContext {
263 pub(crate) async fn get_conversation(&self, id: &ConversationId) -> CryptoResult<GroupStoreValue<MlsConversation>> {
264 let keystore = self.mls_provider().await?.keystore();
265 self.mls_groups()
266 .await?
267 .get_fetch(id, &keystore, None)
268 .await?
269 .ok_or_else(|| CryptoError::ConversationNotFound(id.clone()))
270 }
271
272 pub(crate) async fn get_parent_conversation(
273 &self,
274 conversation: &GroupStoreValue<MlsConversation>,
275 ) -> CryptoResult<Option<GroupStoreValue<MlsConversation>>> {
276 let conversation_lock = conversation.read().await;
277 if let Some(parent_id) = conversation_lock.parent_id.as_ref() {
278 Ok(Some(
279 self.get_conversation(parent_id)
280 .await
281 .map_err(|_| CryptoError::ParentGroupNotFound)?,
282 ))
283 } else {
284 Ok(None)
285 }
286 }
287
288 pub(crate) async fn get_all_conversations(&self) -> CryptoResult<Vec<GroupStoreValue<MlsConversation>>> {
289 let keystore = self.mls_provider().await?.keystore();
290 self.mls_groups().await?.get_fetch_all(&keystore).await
291 }
292
293 #[cfg_attr(test, crate::idempotent)]
296 pub async fn mark_conversation_as_child_of(
297 &self,
298 child_id: &ConversationId,
299 parent_id: &ConversationId,
300 ) -> CryptoResult<()> {
301 let conversation = self.get_conversation(child_id).await?;
302 conversation
303 .write()
304 .await
305 .mark_as_child_of(parent_id, &self.keystore().await?)
306 .await?;
307
308 Ok(())
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use crate::e2e_identity::rotate::tests::all::failsafe_ctx;
315
316 use wasm_bindgen_test::*;
317
318 use crate::{
319 prelude::{
320 ClientIdentifier, MlsCentralConfiguration, MlsConversationCreationMessage, INITIAL_KEYING_MATERIAL_COUNT,
321 },
322 test_utils::*,
323 CoreCrypto,
324 };
325
326 use super::*;
327
328 wasm_bindgen_test_configure!(run_in_browser);
329
330 #[apply(all_cred_cipher)]
331 #[wasm_bindgen_test]
332 pub async fn create_self_conversation_should_succeed(case: TestCase) {
333 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
334 Box::pin(async move {
335 let id = conversation_id();
336 alice_central
337 .context
338 .new_conversation(&id, case.credential_type, case.cfg.clone())
339 .await
340 .unwrap();
341 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
342 assert_eq!(
343 alice_central
344 .get_conversation_unchecked(&id)
345 .await
346 .group
347 .group_id()
348 .as_slice(),
349 id
350 );
351 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
352 let alice_can_send_message = alice_central.context.encrypt_message(&id, b"me").await;
353 assert!(alice_can_send_message.is_ok());
354 })
355 })
356 .await;
357 }
358
359 #[apply(all_cred_cipher)]
360 #[wasm_bindgen_test]
361 pub async fn create_1_1_conversation_should_succeed(case: TestCase) {
362 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
363 Box::pin(async move {
364 let id = conversation_id();
365
366 alice_central
367 .context
368 .new_conversation(&id, case.credential_type, case.cfg.clone())
369 .await
370 .unwrap();
371
372 let bob = bob_central.rand_key_package(&case).await;
373 let MlsConversationCreationMessage { welcome, .. } = alice_central
374 .context
375 .add_members_to_conversation(&id, vec![bob])
376 .await
377 .unwrap();
378 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
380 alice_central.context.commit_accepted(&id).await.unwrap();
381
382 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
383 assert_eq!(
384 alice_central
385 .get_conversation_unchecked(&id)
386 .await
387 .group
388 .group_id()
389 .as_slice(),
390 id
391 );
392 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
393
394 bob_central
395 .context
396 .process_welcome_message(welcome.into(), case.custom_cfg())
397 .await
398 .unwrap();
399
400 assert_eq!(
401 bob_central.get_conversation_unchecked(&id).await.id(),
402 alice_central.get_conversation_unchecked(&id).await.id()
403 );
404 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
405 })
406 })
407 .await;
408 }
409
410 #[apply(all_cred_cipher)]
411 #[wasm_bindgen_test]
412 pub async fn create_many_people_conversation(case: TestCase) {
413 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
414 Box::pin(async move {
415 let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
416 let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
417
418 let id = conversation_id();
419 alice_central
420 .context
421 .new_conversation(&id, case.credential_type, case.cfg.clone())
422 .await
423 .unwrap();
424
425 let mut bob_and_friends: Vec<ClientContext> = Vec::with_capacity(GROUP_SAMPLE_SIZE);
426 for _ in 0..GROUP_SAMPLE_SIZE {
427 let uuid = uuid::Uuid::new_v4();
428 let name = uuid.hyphenated().to_string();
429 let path = tmp_db_file();
430 let config = MlsCentralConfiguration::try_new(
431 path.0,
432 name.clone(),
433 None,
434 vec![case.ciphersuite()],
435 None,
436 Some(INITIAL_KEYING_MATERIAL_COUNT),
437 )
438 .unwrap();
439 let central = MlsCentral::try_new(config).await.unwrap();
440 let cc = CoreCrypto::from(central);
441 let friend_context = cc.new_transaction().await.unwrap();
442 let central = cc.mls;
443
444 x509_test_chain.register_with_central(&friend_context).await;
445
446 let client_id: crate::prelude::ClientId = name.as_str().into();
447 let identity = match case.credential_type {
448 MlsCredentialType::Basic => ClientIdentifier::Basic(client_id),
449 MlsCredentialType::X509 => {
450 let x509_test_chain = alice_central
451 .x509_test_chain
452 .as_ref()
453 .as_ref()
454 .expect("No x509 test chain");
455 let cert = crate::prelude::CertificateBundle::rand(
456 &client_id,
457 x509_test_chain.find_local_intermediate_ca(),
458 );
459 ClientIdentifier::X509(HashMap::from([(case.cfg.ciphersuite.signature_algorithm(), cert)]))
460 }
461 };
462 friend_context
463 .mls_init(
464 identity,
465 vec![case.cfg.ciphersuite],
466 Some(INITIAL_KEYING_MATERIAL_COUNT),
467 )
468 .await
469 .unwrap();
470
471 let context = ClientContext {
472 context: friend_context,
473 central,
474 x509_test_chain: x509_test_chain_arc.clone(),
475 };
476 bob_and_friends.push(context);
477 }
478
479 let number_of_friends = bob_and_friends.len();
480
481 let mut bob_and_friends_kps = vec![];
482 for c in &bob_and_friends {
483 bob_and_friends_kps.push(c.rand_key_package(&case).await);
484 }
485
486 let MlsConversationCreationMessage { welcome, .. } = alice_central
487 .context
488 .add_members_to_conversation(&id, bob_and_friends_kps)
489 .await
490 .unwrap();
491 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
493 alice_central.context.commit_accepted(&id).await.unwrap();
494
495 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
496 assert_eq!(
497 alice_central
498 .get_conversation_unchecked(&id)
499 .await
500 .group
501 .group_id()
502 .as_slice(),
503 id
504 );
505 assert_eq!(
506 alice_central.get_conversation_unchecked(&id).await.members().len(),
507 1 + number_of_friends
508 );
509
510 let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
511 for c in bob_and_friends {
513 c.context
514 .process_welcome_message(welcome.clone().into(), case.custom_cfg())
515 .await
516 .unwrap();
517 assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
518 bob_and_friends_groups.push(c);
519 }
520
521 assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
522 })
523 })
524 .await;
525 }
526}