1use std::sync::Arc;
2
3use async_lock::RwLock;
4use log::trace;
5
6use crate::prelude::{
7 identifier::ClientIdentifier, key_package::INITIAL_KEYING_MATERIAL_COUNT, Client, ClientId, ConversationId,
8 CoreCryptoCallbacks, CryptoError, CryptoResult, MlsCentralConfiguration, MlsCiphersuite, MlsConversation,
9 MlsConversationConfiguration, MlsCredentialType, MlsError,
10};
11use crate::CoreCrypto;
12use mls_crypto_provider::{EntropySeed, MlsCryptoProvider, MlsCryptoProviderConfiguration};
13use openmls_traits::OpenMlsCryptoProvider;
14
15use crate::context::CentralContext;
16
17pub(crate) mod buffer_external_commit;
18pub(crate) mod ciphersuite;
19pub(crate) mod client;
20pub mod conversation;
21pub(crate) mod credential;
22pub(crate) mod external_commit;
23pub(crate) mod external_proposal;
24pub(crate) mod proposal;
25
26pub(crate) mod config {
28 use mls_crypto_provider::EntropySeed;
29
30 use super::*;
31
32 #[derive(Debug, Clone)]
34 #[non_exhaustive]
35 pub struct MlsCentralConfiguration {
36 pub store_path: String,
38 pub identity_key: String,
40 pub client_id: Option<ClientId>,
42 pub external_entropy: Option<EntropySeed>,
44 pub ciphersuites: Vec<ciphersuite::MlsCiphersuite>,
46 pub nb_init_key_packages: Option<usize>,
48 }
49
50 impl MlsCentralConfiguration {
51 pub fn try_new(
88 store_path: String,
89 identity_key: String,
90 client_id: Option<ClientId>,
91 ciphersuites: Vec<MlsCiphersuite>,
92 entropy: Option<Vec<u8>>,
93 nb_init_key_packages: Option<usize>,
94 ) -> CryptoResult<Self> {
95 if store_path.trim().is_empty() {
97 return Err(CryptoError::MalformedIdentifier("store_path"));
98 }
99 if identity_key.trim().is_empty() {
101 return Err(CryptoError::MalformedIdentifier("identity_key"));
102 }
103 if let Some(client_id) = client_id.as_ref() {
105 if client_id.is_empty() {
106 return Err(CryptoError::MalformedIdentifier("client_id"));
107 }
108 }
109 let external_entropy = entropy
110 .as_deref()
111 .map(|seed| &seed[..EntropySeed::EXPECTED_LEN])
112 .map(EntropySeed::try_from_slice)
113 .transpose()?;
114 Ok(Self {
115 store_path,
116 identity_key,
117 client_id,
118 ciphersuites,
119 external_entropy,
120 nb_init_key_packages,
121 })
122 }
123
124 pub fn set_entropy(&mut self, entropy: EntropySeed) {
126 self.external_entropy = Some(entropy);
127 }
128
129 #[cfg(test)]
130 #[allow(dead_code)]
131 pub(crate) fn tmp_store_path(tmp_dir: &tempfile::TempDir) -> String {
134 let path = tmp_dir.path().join("store.edb");
135 std::fs::File::create(&path).unwrap();
136 path.to_str().unwrap().to_string()
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
144pub struct MlsCentral {
145 pub(crate) mls_client: Client,
146 pub(crate) mls_backend: MlsCryptoProvider,
147 pub(crate) callbacks: Arc<RwLock<Option<std::sync::Arc<dyn CoreCryptoCallbacks + 'static>>>>,
149}
150
151impl MlsCentral {
152 pub async fn try_new(configuration: MlsCentralConfiguration) -> CryptoResult<Self> {
168 let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
170 db_path: &configuration.store_path,
171 identity_key: &configuration.identity_key,
172 in_memory: false,
173 entropy_seed: configuration.external_entropy.clone(),
174 })
175 .await?;
176 Self::new_with_backend(mls_backend, configuration).await
177 }
178
179 pub async fn try_new_in_memory(configuration: MlsCentralConfiguration) -> CryptoResult<Self> {
181 let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
182 db_path: &configuration.store_path,
183 identity_key: &configuration.identity_key,
184 in_memory: true,
185 entropy_seed: configuration.external_entropy.clone(),
186 })
187 .await?;
188 Self::new_with_backend(mls_backend, configuration).await
189 }
190
191 async fn new_with_backend(
192 mls_backend: MlsCryptoProvider,
193 configuration: MlsCentralConfiguration,
194 ) -> CryptoResult<Self> {
195 let mls_client = Client::default();
196
197 let central = Self {
201 mls_backend: mls_backend.clone(),
202 mls_client: mls_client.clone(),
203 callbacks: Arc::new(None.into()),
204 };
205
206 let cc = CoreCrypto::from(central);
207 let context = cc.new_transaction().await?;
208 if let Some(id) = configuration.client_id {
209 mls_client
210 .init(
211 ClientIdentifier::Basic(id),
212 configuration.ciphersuites.as_slice(),
213 &mls_backend,
214 configuration
215 .nb_init_key_packages
216 .unwrap_or(INITIAL_KEYING_MATERIAL_COUNT),
217 )
218 .await?
219 }
220 let central = cc.mls;
221 context.init_pki_env().await?;
222 context.finish().await?;
223 Ok(central)
224 }
225
226 pub async fn callbacks(&self, callbacks: std::sync::Arc<dyn CoreCryptoCallbacks>) {
231 self.callbacks.write().await.replace(callbacks);
232 }
233
234 pub async fn client_public_key(
241 &self,
242 ciphersuite: MlsCiphersuite,
243 credential_type: MlsCredentialType,
244 ) -> CryptoResult<Vec<u8>> {
245 let cb = self
246 .mls_client
247 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
248 .await?;
249 Ok(cb.signature_key.to_public_vec())
250 }
251
252 pub async fn client_id(&self) -> CryptoResult<ClientId> {
254 self.mls_client.id().await
255 }
256
257 pub async fn conversation_exists(&self, id: &ConversationId) -> CryptoResult<bool> {
259 Ok(self.get_conversation(id).await?.is_some())
260 }
261
262 #[cfg_attr(test, crate::idempotent)]
267 pub async fn conversation_epoch(&self, id: &ConversationId) -> CryptoResult<u64> {
268 Ok(self
269 .get_conversation(id)
270 .await?
271 .ok_or_else(|| CryptoError::ConversationNotFound(id.clone()))?
272 .group
273 .epoch()
274 .as_u64())
275 }
276
277 #[cfg_attr(test, crate::idempotent)]
282 pub async fn conversation_ciphersuite(&self, id: &ConversationId) -> CryptoResult<MlsCiphersuite> {
283 Ok(self
284 .get_conversation(id)
285 .await?
286 .ok_or_else(|| CryptoError::ConversationNotFound(id.clone()))?
287 .ciphersuite())
288 }
289
290 pub fn random_bytes(&self, len: usize) -> CryptoResult<Vec<u8>> {
292 use openmls_traits::random::OpenMlsRand as _;
293 Ok(self.mls_backend.rand().random_vec(len)?)
294 }
295
296 pub async fn close(self) -> CryptoResult<()> {
301 self.mls_backend.close().await?;
302 Ok(())
303 }
304
305 pub async fn wipe(self) -> CryptoResult<()> {
310 self.mls_backend.destroy_and_reset().await?;
311 Ok(())
312 }
313
314 pub async fn reseed(&self, seed: Option<EntropySeed>) -> CryptoResult<()> {
316 self.mls_backend.reseed(seed)?;
317 Ok(())
318 }
319}
320
321impl CentralContext {
322 pub async fn mls_init(
326 &self,
327 identifier: ClientIdentifier,
328 ciphersuites: Vec<MlsCiphersuite>,
329 nb_init_key_packages: Option<usize>,
330 ) -> CryptoResult<()> {
331 let nb_key_package = nb_init_key_packages.unwrap_or(INITIAL_KEYING_MATERIAL_COUNT);
332 let mls_client = self.mls_client().await?;
333 mls_client
334 .init(identifier, &ciphersuites, &self.mls_provider().await?, nb_key_package)
335 .await?;
336
337 if mls_client.is_e2ei_capable().await {
338 let client_id = mls_client.id().await?;
339 trace!(client_id:% = client_id; "Initializing PKI environment");
340 self.init_pki_env().await?;
341 }
342
343 Ok(())
344 }
345
346 #[cfg_attr(test, crate::dispotent)]
351 pub async fn mls_generate_keypairs(&self, ciphersuites: Vec<MlsCiphersuite>) -> CryptoResult<Vec<ClientId>> {
352 self.mls_client()
353 .await?
354 .generate_raw_keypairs(&ciphersuites, &self.mls_provider().await?)
355 .await
356 }
357
358 #[cfg_attr(test, crate::dispotent)]
362 pub async fn mls_init_with_client_id(
363 &self,
364 client_id: ClientId,
365 tmp_client_ids: Vec<ClientId>,
366 ciphersuites: Vec<MlsCiphersuite>,
367 ) -> CryptoResult<()> {
368 self.mls_client()
369 .await?
370 .init_with_external_client_id(client_id, tmp_client_ids, &ciphersuites, &self.mls_provider().await?)
371 .await
372 }
373
374 pub async fn client_public_key(
376 &self,
377 ciphersuite: MlsCiphersuite,
378 credential_type: MlsCredentialType,
379 ) -> CryptoResult<Vec<u8>> {
380 let cb = self
381 .mls_client()
382 .await?
383 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
384 .await?;
385 Ok(cb.signature_key.to_public_vec())
386 }
387
388 pub async fn client_id(&self) -> CryptoResult<ClientId> {
390 self.mls_client().await?.id().await
391 }
392
393 #[cfg_attr(test, crate::dispotent)]
405 pub async fn new_conversation(
406 &self,
407 id: &ConversationId,
408 creator_credential_type: MlsCredentialType,
409 config: MlsConversationConfiguration,
410 ) -> CryptoResult<()> {
411 if self.conversation_exists(id).await? || self.pending_group_exists(id).await? {
412 return Err(CryptoError::ConversationAlreadyExists(id.clone()));
413 }
414 let conversation = MlsConversation::create(
415 id.clone(),
416 &self.mls_client().await?,
417 creator_credential_type,
418 config,
419 &self.mls_provider().await?,
420 )
421 .await?;
422
423 self.mls_groups().await?.insert(id.clone(), conversation);
424
425 Ok(())
426 }
427
428 pub async fn conversation_exists(&self, id: &ConversationId) -> CryptoResult<bool> {
430 Ok(self
431 .mls_groups()
432 .await?
433 .get_fetch(id, &self.mls_provider().await?.keystore(), None)
434 .await
435 .ok()
436 .flatten()
437 .is_some())
438 }
439
440 #[cfg_attr(test, crate::idempotent)]
445 pub async fn conversation_epoch(&self, id: &ConversationId) -> CryptoResult<u64> {
446 Ok(self.get_conversation(id).await?.read().await.group.epoch().as_u64())
447 }
448
449 #[cfg_attr(test, crate::idempotent)]
454 pub async fn conversation_ciphersuite(&self, id: &ConversationId) -> CryptoResult<MlsCiphersuite> {
455 Ok(self.get_conversation(id).await?.read().await.ciphersuite())
456 }
457
458 pub async fn random_bytes(&self, len: usize) -> CryptoResult<Vec<u8>> {
460 use openmls_traits::random::OpenMlsRand as _;
461 Ok(self.mls_provider().await?.rand().random_vec(len)?)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use wasm_bindgen_test::*;
468
469 use crate::prelude::{CertificateBundle, ClientIdentifier, MlsCredentialType, INITIAL_KEYING_MATERIAL_COUNT};
470 use crate::{
471 mls::{CryptoError, MlsCentral, MlsCentralConfiguration},
472 test_utils::{x509::X509TestChain, *},
473 CoreCrypto,
474 };
475
476 wasm_bindgen_test_configure!(run_in_browser);
477
478 mod conversation_epoch {
479 use super::*;
480
481 #[apply(all_cred_cipher)]
482 #[wasm_bindgen_test]
483 async fn can_get_newly_created_conversation_epoch(case: TestCase) {
484 run_test_with_central(case.clone(), move |[central]| {
485 Box::pin(async move {
486 let id = conversation_id();
487 central
488 .context
489 .new_conversation(&id, case.credential_type, case.cfg.clone())
490 .await
491 .unwrap();
492 let epoch = central.context.conversation_epoch(&id).await.unwrap();
493 assert_eq!(epoch, 0);
494 })
495 })
496 .await;
497 }
498
499 #[apply(all_cred_cipher)]
500 #[wasm_bindgen_test]
501 async fn can_get_conversation_epoch(case: TestCase) {
502 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
503 Box::pin(async move {
504 let id = conversation_id();
505 alice_central
506 .context
507 .new_conversation(&id, case.credential_type, case.cfg.clone())
508 .await
509 .unwrap();
510 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
511 let epoch = alice_central.context.conversation_epoch(&id).await.unwrap();
512 assert_eq!(epoch, 1);
513 })
514 })
515 .await;
516 }
517
518 #[apply(all_cred_cipher)]
519 #[wasm_bindgen_test]
520 async fn conversation_not_found(case: TestCase) {
521 run_test_with_central(case.clone(), move |[central]| {
522 Box::pin(async move {
523 let id = conversation_id();
524 let err = central.context.conversation_epoch(&id).await.unwrap_err();
525 assert!(matches!(err, CryptoError::ConversationNotFound(conv_id) if conv_id == id));
526 })
527 })
528 .await;
529 }
530 }
531
532 mod invariants {
533 use crate::prelude::MlsCiphersuite;
534
535 use super::*;
536
537 #[apply(all_cred_cipher)]
538 #[wasm_bindgen_test]
539 async fn can_create_from_valid_configuration(case: TestCase) {
540 run_tests(move |[tmp_dir_argument]| {
541 Box::pin(async move {
542 let configuration = MlsCentralConfiguration::try_new(
543 tmp_dir_argument,
544 "test".to_string(),
545 Some("alice".into()),
546 vec![case.ciphersuite()],
547 None,
548 Some(INITIAL_KEYING_MATERIAL_COUNT),
549 )
550 .unwrap();
551
552 let central = MlsCentral::try_new(configuration).await;
553 assert!(central.is_ok())
554 })
555 })
556 .await
557 }
558
559 #[test]
560 #[wasm_bindgen_test]
561 fn store_path_should_not_be_empty_nor_blank() {
562 let ciphersuites = vec![MlsCiphersuite::default()];
563 let configuration = MlsCentralConfiguration::try_new(
564 " ".to_string(),
565 "test".to_string(),
566 Some("alice".into()),
567 ciphersuites,
568 None,
569 Some(INITIAL_KEYING_MATERIAL_COUNT),
570 );
571 assert!(matches!(
572 configuration.unwrap_err(),
573 CryptoError::MalformedIdentifier("store_path")
574 ));
575 }
576
577 #[cfg_attr(not(target_family = "wasm"), async_std::test)]
578 #[wasm_bindgen_test]
579 async fn identity_key_should_not_be_empty_nor_blank() {
580 run_tests(|[tmp_dir_argument]| {
581 Box::pin(async move {
582 let ciphersuites = vec![MlsCiphersuite::default()];
583 let configuration = MlsCentralConfiguration::try_new(
584 tmp_dir_argument,
585 " ".to_string(),
586 Some("alice".into()),
587 ciphersuites,
588 None,
589 Some(INITIAL_KEYING_MATERIAL_COUNT),
590 );
591 assert!(matches!(
592 configuration.unwrap_err(),
593 CryptoError::MalformedIdentifier("identity_key")
594 ));
595 })
596 })
597 .await
598 }
599
600 #[cfg_attr(not(target_family = "wasm"), async_std::test)]
601 #[wasm_bindgen_test]
602 async fn client_id_should_not_be_empty() {
603 run_tests(|[tmp_dir_argument]| {
604 Box::pin(async move {
605 let ciphersuites = vec![MlsCiphersuite::default()];
606 let configuration = MlsCentralConfiguration::try_new(
607 tmp_dir_argument,
608 "test".to_string(),
609 Some("".into()),
610 ciphersuites,
611 None,
612 Some(INITIAL_KEYING_MATERIAL_COUNT),
613 );
614 assert!(matches!(
615 configuration.unwrap_err(),
616 CryptoError::MalformedIdentifier("client_id")
617 ));
618 })
619 })
620 .await
621 }
622 }
623
624 #[apply(all_cred_cipher)]
625 #[wasm_bindgen_test]
626 async fn create_conversation_should_fail_when_already_exists(case: TestCase) {
627 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
628 Box::pin(async move {
629 let id = conversation_id();
630
631 let create = alice_central
632 .context
633 .new_conversation(&id, case.credential_type, case.cfg.clone())
634 .await;
635 assert!(create.is_ok());
636
637 let repeat_create = alice_central
639 .context
640 .new_conversation(&id, case.credential_type, case.cfg.clone())
641 .await;
642 assert!(matches!(repeat_create.unwrap_err(), CryptoError::ConversationAlreadyExists(i) if i == id));
643 })
644 })
645 .await;
646 }
647
648 #[apply(all_cred_cipher)]
649 #[wasm_bindgen_test]
650 async fn can_fetch_client_public_key(case: TestCase) {
651 run_tests(move |[tmp_dir_argument]| {
652 Box::pin(async move {
653 let configuration = MlsCentralConfiguration::try_new(
654 tmp_dir_argument,
655 "test".to_string(),
656 Some("potato".into()),
657 vec![case.ciphersuite()],
658 None,
659 Some(INITIAL_KEYING_MATERIAL_COUNT),
660 )
661 .unwrap();
662
663 let result = MlsCentral::try_new(configuration.clone()).await;
664 assert!(result.is_ok());
665 })
666 })
667 .await
668 }
669
670 #[apply(all_cred_cipher)]
671 #[wasm_bindgen_test]
672 async fn can_2_phase_init_central(case: TestCase) {
673 run_tests(move |[tmp_dir_argument]| {
674 Box::pin(async move {
675 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
676 let configuration = MlsCentralConfiguration::try_new(
677 tmp_dir_argument,
678 "test".to_string(),
679 None,
680 vec![case.ciphersuite()],
681 None,
682 Some(INITIAL_KEYING_MATERIAL_COUNT),
683 )
684 .unwrap();
685 let central = MlsCentral::try_new(configuration).await.unwrap();
687 let cc = CoreCrypto::from(central);
688 let context = cc.new_transaction().await.unwrap();
689 x509_test_chain.register_with_central(&context).await;
690
691 assert!(!context.mls_client().await.unwrap().is_ready().await);
692 let client_id = "alice";
694 let identifier = match case.credential_type {
695 MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
696 MlsCredentialType::X509 => {
697 CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
698 }
699 };
700 context
701 .mls_init(
702 identifier,
703 vec![case.ciphersuite()],
704 Some(INITIAL_KEYING_MATERIAL_COUNT),
705 )
706 .await
707 .unwrap();
708 assert!(context.mls_client().await.unwrap().is_ready().await);
709 assert_eq!(
711 context
712 .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
713 .await
714 .unwrap()
715 .len(),
716 2
717 );
718 })
719 })
720 .await
721 }
722}