1use std::sync::Arc;
2
3use async_lock::RwLock;
4use log::trace;
5
6use crate::{
7 CoreCrypto, LeafError, MlsError, RecursiveError,
8 prelude::{
9 Client, ClientId, ConversationId, MlsCentralConfiguration, MlsCiphersuite, MlsConversation,
10 MlsConversationConfiguration, MlsCredentialType, MlsTransport, identifier::ClientIdentifier,
11 key_package::INITIAL_KEYING_MATERIAL_COUNT,
12 },
13};
14use mls_crypto_provider::{EntropySeed, MlsCryptoProvider, MlsCryptoProviderConfiguration};
15use openmls_traits::OpenMlsCryptoProvider;
16
17use crate::context::CentralContext;
18
19pub(crate) mod ciphersuite;
20pub(crate) mod client;
21pub mod conversation;
22pub(crate) mod credential;
23mod error;
24pub(crate) mod external_commit;
25pub(crate) mod external_proposal;
26pub(crate) mod proposal;
27
28pub use client::EpochObserver;
29pub use error::{Error, Result};
30
31pub(crate) mod config {
33 use ciphersuite::MlsCiphersuite;
34 use mls_crypto_provider::EntropySeed;
35
36 use super::*;
37
38 #[derive(Debug, Clone)]
40 #[non_exhaustive]
41 pub struct MlsCentralConfiguration {
42 pub store_path: String,
44 pub identity_key: String,
46 pub client_id: Option<ClientId>,
48 pub external_entropy: Option<EntropySeed>,
50 pub ciphersuites: Vec<ciphersuite::MlsCiphersuite>,
52 pub nb_init_key_packages: Option<usize>,
54 }
55
56 impl MlsCentralConfiguration {
57 pub fn try_new(
94 store_path: String,
95 identity_key: String,
96 client_id: Option<ClientId>,
97 ciphersuites: Vec<MlsCiphersuite>,
98 entropy: Option<Vec<u8>>,
99 nb_init_key_packages: Option<usize>,
100 ) -> Result<Self> {
101 if store_path.trim().is_empty() {
103 return Err(Error::MalformedIdentifier("store_path"));
104 }
105 if identity_key.trim().is_empty() {
107 return Err(Error::MalformedIdentifier("identity_key"));
108 }
109 if let Some(client_id) = client_id.as_ref() {
111 if client_id.is_empty() {
112 return Err(Error::MalformedIdentifier("client_id"));
113 }
114 }
115 let external_entropy = entropy
116 .as_deref()
117 .map(|seed| &seed[..EntropySeed::EXPECTED_LEN])
118 .map(EntropySeed::try_from_slice)
119 .transpose()
120 .map_err(MlsError::wrap("gathering external entropy"))?;
121 Ok(Self {
122 store_path,
123 identity_key,
124 client_id,
125 ciphersuites,
126 external_entropy,
127 nb_init_key_packages,
128 })
129 }
130
131 pub fn set_entropy(&mut self, entropy: EntropySeed) {
133 self.external_entropy = Some(entropy);
134 }
135
136 #[cfg(test)]
137 #[allow(dead_code)]
138 pub(crate) fn tmp_store_path(tmp_dir: &tempfile::TempDir) -> String {
141 let path = tmp_dir.path().join("store.edb");
142 std::fs::File::create(&path).unwrap();
143 path.to_str().unwrap().to_string()
144 }
145 }
146}
147
148#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
149#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
150
151pub(crate) trait HasClientAndProvider: Send {
152 async fn client(&self) -> Result<Client>;
153 async fn mls_provider(&self) -> Result<MlsCryptoProvider>;
154}
155
156#[derive(Debug, Clone)]
159pub struct MlsCentral {
160 pub(crate) mls_client: Client,
161 pub(crate) mls_backend: MlsCryptoProvider,
162 pub(crate) transport: Arc<RwLock<Option<Arc<dyn MlsTransport + 'static>>>>,
164}
165
166#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
167#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
168impl HasClientAndProvider for MlsCentral {
169 async fn client(&self) -> Result<Client> {
170 Ok(self.mls_client.clone())
171 }
172
173 async fn mls_provider(&self) -> Result<MlsCryptoProvider> {
174 Ok(self.mls_backend.clone())
175 }
176}
177
178impl MlsCentral {
179 pub async fn try_new(configuration: MlsCentralConfiguration) -> Result<Self> {
195 let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
197 db_path: &configuration.store_path,
198 identity_key: &configuration.identity_key,
199 in_memory: false,
200 entropy_seed: configuration.external_entropy.clone(),
201 })
202 .await
203 .map_err(MlsError::wrap("trying to initialize mls crypto provider object"))?;
204 Self::new_with_backend(mls_backend, configuration).await
205 }
206
207 pub async fn try_new_in_memory(configuration: MlsCentralConfiguration) -> Result<Self> {
209 let mls_backend = MlsCryptoProvider::try_new_with_configuration(MlsCryptoProviderConfiguration {
210 db_path: &configuration.store_path,
211 identity_key: &configuration.identity_key,
212 in_memory: true,
213 entropy_seed: configuration.external_entropy.clone(),
214 })
215 .await
216 .map_err(MlsError::wrap(
217 "trying to initialize mls crypto provider object (in memory)",
218 ))?;
219 Self::new_with_backend(mls_backend, configuration).await
220 }
221
222 async fn new_with_backend(mls_backend: MlsCryptoProvider, configuration: MlsCentralConfiguration) -> Result<Self> {
223 let mls_client = Client::default();
224
225 let central = Self {
229 mls_backend: mls_backend.clone(),
230 mls_client: mls_client.clone(),
231 transport: Arc::new(None.into()),
232 };
233
234 let cc = CoreCrypto::from(central);
235 let context = cc
236 .new_transaction()
237 .await
238 .map_err(RecursiveError::root("starting new transaction"))?;
239 if let Some(id) = configuration.client_id {
240 mls_client
241 .init(
242 ClientIdentifier::Basic(id),
243 configuration.ciphersuites.as_slice(),
244 &mls_backend,
245 configuration
246 .nb_init_key_packages
247 .unwrap_or(INITIAL_KEYING_MATERIAL_COUNT),
248 )
249 .await
250 .map_err(RecursiveError::mls_client("initializing mls client"))?
251 }
252 let central = cc.mls;
253 context
254 .init_pki_env()
255 .await
256 .map_err(RecursiveError::e2e_identity("initializing pki environment"))?;
257 context
258 .finish()
259 .await
260 .map_err(RecursiveError::root("finishing transaction"))?;
261 Ok(central)
262 }
263
264 pub async fn provide_transport(&self, transport: Arc<dyn MlsTransport>) {
267 self.transport.write().await.replace(transport);
268 }
269
270 pub async fn client_public_key(
277 &self,
278 ciphersuite: MlsCiphersuite,
279 credential_type: MlsCredentialType,
280 ) -> Result<Vec<u8>> {
281 let cb = self
282 .mls_client
283 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
284 .await
285 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
286 Ok(cb.signature_key.to_public_vec())
287 }
288
289 pub async fn client_id(&self) -> Result<ClientId> {
291 self.mls_client
292 .id()
293 .await
294 .map_err(RecursiveError::mls_client("getting client id"))
295 .map_err(Into::into)
296 }
297
298 pub async fn conversation_exists(&self, id: &ConversationId) -> Result<bool> {
300 match self.get_raw_conversation(id).await {
301 Ok(_) => Ok(true),
302 Err(conversation::Error::Leaf(LeafError::ConversationNotFound(_))) => Ok(false),
303 Err(e) => {
304 Err(RecursiveError::mls_conversation("getting conversation by id to check for existence")(e).into())
305 }
306 }
307 }
308
309 pub fn random_bytes(&self, len: usize) -> Result<Vec<u8>> {
311 use openmls_traits::random::OpenMlsRand as _;
312 self.mls_backend
313 .rand()
314 .random_vec(len)
315 .map_err(MlsError::wrap("generating random vector"))
316 .map_err(Into::into)
317 }
318
319 pub async fn close(self) -> Result<()> {
324 self.mls_backend
325 .close()
326 .await
327 .map_err(MlsError::wrap("closing connection with keystore"))
328 .map_err(Into::into)
329 }
330
331 pub async fn reseed(&self, seed: Option<EntropySeed>) -> Result<()> {
333 self.mls_backend
334 .reseed(seed)
335 .map_err(MlsError::wrap("reseeding mls backend"))
336 .map_err(Into::into)
337 }
338}
339
340impl CentralContext {
341 pub async fn mls_init(
345 &self,
346 identifier: ClientIdentifier,
347 ciphersuites: Vec<MlsCiphersuite>,
348 nb_init_key_packages: Option<usize>,
349 ) -> Result<()> {
350 let nb_key_package = nb_init_key_packages.unwrap_or(INITIAL_KEYING_MATERIAL_COUNT);
351 let mls_client = self
352 .mls_client()
353 .await
354 .map_err(RecursiveError::root("getting mls client"))?;
355 mls_client
356 .init(
357 identifier,
358 &ciphersuites,
359 &self
360 .mls_provider()
361 .await
362 .map_err(RecursiveError::root("getting mls provider"))?,
363 nb_key_package,
364 )
365 .await
366 .map_err(RecursiveError::mls_client("initializing mls client"))?;
367
368 if mls_client.is_e2ei_capable().await {
369 let client_id = mls_client
370 .id()
371 .await
372 .map_err(RecursiveError::mls_client("getting client id"))?;
373 trace!(client_id:% = client_id; "Initializing PKI environment");
374 self.init_pki_env()
375 .await
376 .map_err(RecursiveError::e2e_identity("initializing pki env"))?;
377 }
378
379 Ok(())
380 }
381
382 #[cfg_attr(test, crate::dispotent)]
387 pub async fn mls_generate_keypairs(&self, ciphersuites: Vec<MlsCiphersuite>) -> Result<Vec<ClientId>> {
388 self.mls_client()
389 .await
390 .map_err(RecursiveError::root("getting mls client"))?
391 .generate_raw_keypairs(
392 &ciphersuites,
393 &self
394 .mls_provider()
395 .await
396 .map_err(RecursiveError::root("getting mls provider"))?,
397 )
398 .await
399 .map_err(RecursiveError::mls_client("generating raw keypairs"))
400 .map_err(Into::into)
401 }
402
403 #[cfg_attr(test, crate::dispotent)]
407 pub async fn mls_init_with_client_id(
408 &self,
409 client_id: ClientId,
410 tmp_client_ids: Vec<ClientId>,
411 ciphersuites: Vec<MlsCiphersuite>,
412 ) -> Result<()> {
413 self.mls_client()
414 .await
415 .map_err(RecursiveError::root("getting mls client"))?
416 .init_with_external_client_id(
417 client_id,
418 tmp_client_ids,
419 &ciphersuites,
420 &self
421 .mls_provider()
422 .await
423 .map_err(RecursiveError::root("getting mls provider"))?,
424 )
425 .await
426 .map_err(RecursiveError::mls_client(
427 "initializing mls client with external client id",
428 ))
429 .map_err(Into::into)
430 }
431
432 pub async fn client_public_key(
434 &self,
435 ciphersuite: MlsCiphersuite,
436 credential_type: MlsCredentialType,
437 ) -> Result<Vec<u8>> {
438 let cb = self
439 .mls_client()
440 .await
441 .map_err(RecursiveError::root("getting mls client"))?
442 .find_most_recent_credential_bundle(ciphersuite.signature_algorithm(), credential_type)
443 .await
444 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
445 Ok(cb.signature_key.to_public_vec())
446 }
447
448 pub async fn client_id(&self) -> Result<ClientId> {
450 self.mls_client()
451 .await
452 .map_err(RecursiveError::root("getting mls client"))?
453 .id()
454 .await
455 .map_err(RecursiveError::mls_client("getting client id"))
456 .map_err(Into::into)
457 }
458
459 #[cfg_attr(test, crate::dispotent)]
471 pub async fn new_conversation(
472 &self,
473 id: &ConversationId,
474 creator_credential_type: MlsCredentialType,
475 config: MlsConversationConfiguration,
476 ) -> Result<()> {
477 if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
478 return Err(LeafError::ConversationAlreadyExists(id.clone()).into());
479 }
480 let conversation = MlsConversation::create(
481 id.clone(),
482 &self
483 .mls_client()
484 .await
485 .map_err(RecursiveError::root("getting mls client"))?,
486 creator_credential_type,
487 config,
488 &self
489 .mls_provider()
490 .await
491 .map_err(RecursiveError::root("getting mls provider"))?,
492 )
493 .await
494 .map_err(RecursiveError::mls_conversation("creating conversation"))?;
495
496 self.mls_groups()
497 .await
498 .map_err(RecursiveError::root("getting mls groups"))?
499 .insert(id.clone(), conversation);
500
501 Ok(())
502 }
503
504 pub async fn conversation_exists(&self, id: &ConversationId) -> Result<bool> {
506 Ok(self
507 .mls_groups()
508 .await
509 .map_err(RecursiveError::root("getting mls groups"))?
510 .get_fetch(
511 id,
512 &self
513 .mls_provider()
514 .await
515 .map_err(RecursiveError::root("getting mls provider"))?
516 .keystore(),
517 None,
518 )
519 .await
520 .ok()
521 .flatten()
522 .is_some())
523 }
524
525 pub async fn random_bytes(&self, len: usize) -> Result<Vec<u8>> {
527 use openmls_traits::random::OpenMlsRand as _;
528 self.mls_provider()
529 .await
530 .map_err(RecursiveError::root("getting mls provider"))?
531 .rand()
532 .random_vec(len)
533 .map_err(MlsError::wrap("generating random vector"))
534 .map_err(Into::into)
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use wasm_bindgen_test::*;
541
542 use crate::prelude::{CertificateBundle, ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsCredentialType};
543 use crate::{
544 CoreCrypto,
545 mls::{MlsCentral, MlsCentralConfiguration},
546 test_utils::{x509::X509TestChain, *},
547 };
548
549 wasm_bindgen_test_configure!(run_in_browser);
550
551 mod conversation_epoch {
552 use super::*;
553 use crate::mls::conversation::Conversation as _;
554
555 #[apply(all_cred_cipher)]
556 #[wasm_bindgen_test]
557 async fn can_get_newly_created_conversation_epoch(case: TestCase) {
558 run_test_with_central(case.clone(), move |[central]| {
559 Box::pin(async move {
560 let id = conversation_id();
561 central
562 .context
563 .new_conversation(&id, case.credential_type, case.cfg.clone())
564 .await
565 .unwrap();
566 let epoch = central.context.conversation(&id).await.unwrap().epoch().await;
567 assert_eq!(epoch, 0);
568 })
569 })
570 .await;
571 }
572
573 #[apply(all_cred_cipher)]
574 #[wasm_bindgen_test]
575 async fn can_get_conversation_epoch(case: TestCase) {
576 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
577 Box::pin(async move {
578 let id = conversation_id();
579 alice_central
580 .context
581 .new_conversation(&id, case.credential_type, case.cfg.clone())
582 .await
583 .unwrap();
584 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
585 let epoch = alice_central.context.conversation(&id).await.unwrap().epoch().await;
586 assert_eq!(epoch, 1);
587 })
588 })
589 .await;
590 }
591
592 #[apply(all_cred_cipher)]
593 #[wasm_bindgen_test]
594 async fn conversation_not_found(case: TestCase) {
595 use crate::{LeafError, mls};
596
597 run_test_with_central(case.clone(), move |[central]| {
598 Box::pin(async move {
599 let id = conversation_id();
600 let err = central.context.conversation(&id).await.unwrap_err();
601 assert!(matches!(
602 err,
603 mls::conversation::Error::Leaf(LeafError::ConversationNotFound(i)) if i == id
604 ));
605 })
606 })
607 .await;
608 }
609 }
610
611 mod invariants {
612 use crate::{mls, prelude::MlsCiphersuite};
613
614 use super::*;
615
616 #[apply(all_cred_cipher)]
617 #[wasm_bindgen_test]
618 async fn can_create_from_valid_configuration(case: TestCase) {
619 run_tests(move |[tmp_dir_argument]| {
620 Box::pin(async move {
621 let configuration = MlsCentralConfiguration::try_new(
622 tmp_dir_argument,
623 "test".to_string(),
624 Some("alice".into()),
625 vec![case.ciphersuite()],
626 None,
627 Some(INITIAL_KEYING_MATERIAL_COUNT),
628 )
629 .unwrap();
630
631 let central = MlsCentral::try_new(configuration).await;
632 assert!(central.is_ok())
633 })
634 })
635 .await
636 }
637
638 #[test]
639 #[wasm_bindgen_test]
640 fn store_path_should_not_be_empty_nor_blank() {
641 let ciphersuites = vec![MlsCiphersuite::default()];
642 let configuration = MlsCentralConfiguration::try_new(
643 " ".to_string(),
644 "test".to_string(),
645 Some("alice".into()),
646 ciphersuites,
647 None,
648 Some(INITIAL_KEYING_MATERIAL_COUNT),
649 );
650 assert!(matches!(
651 configuration.unwrap_err(),
652 mls::Error::MalformedIdentifier("store_path")
653 ));
654 }
655
656 #[cfg_attr(not(target_family = "wasm"), async_std::test)]
657 #[wasm_bindgen_test]
658 async fn identity_key_should_not_be_empty_nor_blank() {
659 run_tests(|[tmp_dir_argument]| {
660 Box::pin(async move {
661 let ciphersuites = vec![MlsCiphersuite::default()];
662 let configuration = MlsCentralConfiguration::try_new(
663 tmp_dir_argument,
664 " ".to_string(),
665 Some("alice".into()),
666 ciphersuites,
667 None,
668 Some(INITIAL_KEYING_MATERIAL_COUNT),
669 );
670 assert!(matches!(
671 configuration.unwrap_err(),
672 mls::Error::MalformedIdentifier("identity_key")
673 ));
674 })
675 })
676 .await
677 }
678
679 #[cfg_attr(not(target_family = "wasm"), async_std::test)]
680 #[wasm_bindgen_test]
681 async fn client_id_should_not_be_empty() {
682 run_tests(|[tmp_dir_argument]| {
683 Box::pin(async move {
684 let ciphersuites = vec![MlsCiphersuite::default()];
685 let configuration = MlsCentralConfiguration::try_new(
686 tmp_dir_argument,
687 "test".to_string(),
688 Some("".into()),
689 ciphersuites,
690 None,
691 Some(INITIAL_KEYING_MATERIAL_COUNT),
692 );
693 assert!(matches!(
694 configuration.unwrap_err(),
695 mls::Error::MalformedIdentifier("client_id")
696 ));
697 })
698 })
699 .await
700 }
701 }
702
703 #[apply(all_cred_cipher)]
704 #[wasm_bindgen_test]
705 async fn create_conversation_should_fail_when_already_exists(case: TestCase) {
706 use crate::{LeafError, mls};
707
708 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
709 Box::pin(async move {
710 let id = conversation_id();
711
712 let create = alice_central
713 .context
714 .new_conversation(&id, case.credential_type, case.cfg.clone())
715 .await;
716 assert!(create.is_ok());
717
718 let repeat_create = alice_central
720 .context
721 .new_conversation(&id, case.credential_type, case.cfg.clone())
722 .await;
723 assert!(matches!(repeat_create.unwrap_err(), mls::Error::Leaf(LeafError::ConversationAlreadyExists(i)) if i == id));
724 })
725 })
726 .await;
727 }
728
729 #[apply(all_cred_cipher)]
730 #[wasm_bindgen_test]
731 async fn can_fetch_client_public_key(case: TestCase) {
732 run_tests(move |[tmp_dir_argument]| {
733 Box::pin(async move {
734 let configuration = MlsCentralConfiguration::try_new(
735 tmp_dir_argument,
736 "test".to_string(),
737 Some("potato".into()),
738 vec![case.ciphersuite()],
739 None,
740 Some(INITIAL_KEYING_MATERIAL_COUNT),
741 )
742 .unwrap();
743
744 let result = MlsCentral::try_new(configuration.clone()).await;
745 assert!(result.is_ok());
746 })
747 })
748 .await
749 }
750
751 #[apply(all_cred_cipher)]
752 #[wasm_bindgen_test]
753 async fn can_2_phase_init_central(case: TestCase) {
754 run_tests(move |[tmp_dir_argument]| {
755 Box::pin(async move {
756 let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
757 let configuration = MlsCentralConfiguration::try_new(
758 tmp_dir_argument,
759 "test".to_string(),
760 None,
761 vec![case.ciphersuite()],
762 None,
763 Some(INITIAL_KEYING_MATERIAL_COUNT),
764 )
765 .unwrap();
766 let central = MlsCentral::try_new(configuration).await.unwrap();
768 let cc = CoreCrypto::from(central);
769 let context = cc.new_transaction().await.unwrap();
770 x509_test_chain.register_with_central(&context).await;
771
772 assert!(!context.mls_client().await.unwrap().is_ready().await);
773 let client_id = "alice";
775 let identifier = match case.credential_type {
776 MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
777 MlsCredentialType::X509 => {
778 CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
779 }
780 };
781 context
782 .mls_init(
783 identifier,
784 vec![case.ciphersuite()],
785 Some(INITIAL_KEYING_MATERIAL_COUNT),
786 )
787 .await
788 .unwrap();
789 assert!(context.mls_client().await.unwrap().is_ready().await);
790 assert_eq!(
792 context
793 .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
794 .await
795 .unwrap()
796 .len(),
797 2
798 );
799 })
800 })
801 .await
802 }
803}