core_crypto/mls/
mod.rs

1use crate::{
2    MlsError,
3    prelude::{ClientId, MlsConversation, Session},
4};
5use core_crypto_keystore::DatabaseKey;
6use mls_crypto_provider::MlsCryptoProvider;
7
8pub(crate) mod ciphersuite;
9pub mod conversation;
10pub(crate) mod credential;
11mod error;
12pub(crate) mod proposal;
13pub(crate) mod session;
14
15pub use error::{Error, Result};
16pub use session::EpochObserver;
17
18/// Prevents direct instantiation of [MlsClientConfiguration]
19pub(crate) mod config {
20    use ciphersuite::MlsCiphersuite;
21    use mls_crypto_provider::EntropySeed;
22
23    use super::*;
24
25    /// Configuration parameters for [Session].
26    #[derive(Debug, Clone)]
27    #[non_exhaustive]
28    pub struct MlsClientConfiguration {
29        /// Location where the SQLite/IndexedDB database will be stored
30        pub store_path: String,
31        /// Database key to be used to instantiate the [MlsCryptoProvider]
32        pub database_key: DatabaseKey,
33        /// Identifier for the client to be used by [Session]
34        pub client_id: Option<ClientId>,
35        /// Entropy pool seed for the internal PRNG
36        pub external_entropy: Option<EntropySeed>,
37        /// All supported ciphersuites
38        pub ciphersuites: Vec<ciphersuite::MlsCiphersuite>,
39        /// Number of [openmls::prelude::KeyPackage] to create when creating a MLS client.
40        /// Defaults to [crate::prelude::INITIAL_KEYING_MATERIAL_COUNT].
41        pub nb_init_key_packages: Option<usize>,
42    }
43
44    impl MlsClientConfiguration {
45        /// Creates a new instance of the configuration.
46        ///
47        /// # Arguments
48        /// * `store_path` - location where the SQLite/IndexedDB database will be stored
49        /// * `database_key` - key to be used to instantiate the [MlsCryptoProvider]
50        /// * `client_id` - identifier for the client to be used by [Session]
51        /// * `ciphersuites` - Ciphersuites supported by this device
52        /// * `entropy` - External source of entropy for platforms where default source insufficient
53        ///
54        /// # Errors
55        /// Any empty string parameter will result in a [Error::MalformedIdentifier] error.
56        ///
57        /// # Examples
58        ///
59        /// ```
60        /// use core_crypto::prelude::{MlsClientConfiguration, MlsCiphersuite};
61        /// use core_crypto::DatabaseKey;
62        ///
63        /// let result = MlsClientConfiguration::try_new(
64        ///     "/tmp/crypto".to_string(),
65        ///     DatabaseKey::generate(),
66        ///     Some(b"MY_CLIENT_ID".to_vec().into()),
67        ///     vec![MlsCiphersuite::default()],
68        ///     None,
69        ///     Some(100),
70        /// );
71        /// assert!(result.is_ok());
72        /// ```
73        pub fn try_new(
74            store_path: String,
75            database_key: DatabaseKey,
76            client_id: Option<ClientId>,
77            ciphersuites: Vec<MlsCiphersuite>,
78            entropy: Option<Vec<u8>>,
79            nb_init_key_packages: Option<usize>,
80        ) -> Result<Self> {
81            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
82            if store_path.trim().is_empty() {
83                return Err(Error::MalformedIdentifier("store_path"));
84            }
85            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
86            if let Some(client_id) = client_id.as_ref() {
87                if client_id.is_empty() {
88                    return Err(Error::MalformedIdentifier("client_id"));
89                }
90            }
91            let external_entropy = entropy
92                .as_deref()
93                .map(|seed| &seed[..EntropySeed::EXPECTED_LEN])
94                .map(EntropySeed::try_from_slice)
95                .transpose()
96                .map_err(MlsError::wrap("gathering external entropy"))?;
97            Ok(Self {
98                store_path,
99                database_key,
100                client_id,
101                ciphersuites,
102                external_entropy,
103                nb_init_key_packages,
104            })
105        }
106
107        /// Sets the entropy seed
108        pub fn set_entropy(&mut self, entropy: EntropySeed) {
109            self.external_entropy = Some(entropy);
110        }
111
112        #[cfg(test)]
113        #[allow(dead_code)]
114        /// Creates temporary file to prevent test collisions which would happen with hardcoded file path
115        /// Intended to be used only in tests.
116        pub(crate) fn tmp_store_path(tmp_dir: &tempfile::TempDir) -> String {
117            let path = tmp_dir.path().join("store.edb");
118            std::fs::File::create(&path).unwrap();
119            path.to_str().unwrap().to_string()
120        }
121    }
122}
123
124#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
125#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
126pub(crate) trait HasSessionAndCrypto: Send {
127    async fn session(&self) -> Result<Session>;
128    async fn crypto_provider(&self) -> Result<MlsCryptoProvider>;
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::transaction_context::Error as TransactionError;
134    use wasm_bindgen_test::*;
135
136    use crate::prelude::{
137        CertificateBundle, ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsClientConfiguration, MlsCredentialType,
138    };
139    use crate::{
140        CoreCrypto,
141        mls::Session,
142        test_utils::{x509::X509TestChain, *},
143    };
144
145    wasm_bindgen_test_configure!(run_in_browser);
146
147    use core_crypto_keystore::DatabaseKey;
148
149    mod conversation_epoch {
150        use super::*;
151        use crate::mls::conversation::Conversation as _;
152
153        #[apply(all_cred_cipher)]
154        #[wasm_bindgen_test]
155        async fn can_get_newly_created_conversation_epoch(case: TestContext) {
156            let [session] = case.sessions().await;
157            let conversation = case.create_conversation([&session]).await;
158            let epoch = conversation.guard().await.epoch().await;
159            assert_eq!(epoch, 0);
160        }
161
162        #[apply(all_cred_cipher)]
163        #[wasm_bindgen_test]
164        async fn can_get_conversation_epoch(case: TestContext) {
165            let [alice, bob] = case.sessions().await;
166            Box::pin(async move {
167                let conversation = case.create_conversation([&alice, &bob]).await;
168                let epoch = conversation.guard().await.epoch().await;
169                assert_eq!(epoch, 1);
170            })
171            .await;
172        }
173
174        #[apply(all_cred_cipher)]
175        #[wasm_bindgen_test]
176        async fn conversation_not_found(case: TestContext) {
177            use crate::LeafError;
178            let [session] = case.sessions().await;
179            let id = conversation_id();
180            let err = session.transaction.conversation(&id).await.unwrap_err();
181            assert!(matches!(
182                err,
183                TransactionError::Leaf(LeafError::ConversationNotFound(i)) if i == id
184            ));
185        }
186    }
187
188    mod invariants {
189        use crate::{mls, prelude::MlsCiphersuite};
190
191        use super::*;
192
193        #[apply(all_cred_cipher)]
194        #[wasm_bindgen_test]
195        async fn can_create_from_valid_configuration(mut case: TestContext) {
196            let tmp_dir = case.tmp_dir().await;
197            Box::pin(async move {
198                let configuration = MlsClientConfiguration::try_new(
199                    tmp_dir,
200                    DatabaseKey::generate(),
201                    Some("alice".into()),
202                    vec![case.ciphersuite()],
203                    None,
204                    Some(INITIAL_KEYING_MATERIAL_COUNT),
205                )
206                .unwrap();
207
208                let new_client_result = Session::try_new(configuration).await;
209                assert!(new_client_result.is_ok())
210            })
211            .await
212        }
213
214        #[test]
215        #[wasm_bindgen_test]
216        fn store_path_should_not_be_empty_nor_blank() {
217            let ciphersuites = vec![MlsCiphersuite::default()];
218            let configuration = MlsClientConfiguration::try_new(
219                " ".to_string(),
220                DatabaseKey::generate(),
221                Some("alice".into()),
222                ciphersuites,
223                None,
224                Some(INITIAL_KEYING_MATERIAL_COUNT),
225            );
226            assert!(matches!(
227                configuration.unwrap_err(),
228                mls::Error::MalformedIdentifier("store_path")
229            ));
230        }
231
232        #[cfg_attr(not(target_family = "wasm"), async_std::test)]
233        #[wasm_bindgen_test]
234        async fn client_id_should_not_be_empty() {
235            let mut case = TestContext::default();
236            let tmp_dir = case.tmp_dir().await;
237            Box::pin(async move {
238                let ciphersuites = vec![MlsCiphersuite::default()];
239                let configuration = MlsClientConfiguration::try_new(
240                    tmp_dir,
241                    DatabaseKey::generate(),
242                    Some("".into()),
243                    ciphersuites,
244                    None,
245                    Some(INITIAL_KEYING_MATERIAL_COUNT),
246                );
247                assert!(matches!(
248                    configuration.unwrap_err(),
249                    mls::Error::MalformedIdentifier("client_id")
250                ));
251            })
252            .await
253        }
254    }
255
256    #[apply(all_cred_cipher)]
257    #[wasm_bindgen_test]
258    async fn create_conversation_should_fail_when_already_exists(case: TestContext) {
259        use crate::LeafError;
260
261        let [alice_central] = case.sessions().await;
262        Box::pin(async move {
263                let id = conversation_id();
264
265                let create = alice_central
266                    .transaction
267                    .new_conversation(&id, case.credential_type, case.cfg.clone())
268                    .await;
269                assert!(create.is_ok());
270
271                // creating a conversation should first verify that the conversation does not already exist ; only then create it
272                let repeat_create = alice_central
273                    .transaction
274                    .new_conversation(&id, case.credential_type, case.cfg.clone())
275                    .await;
276                assert!(matches!(repeat_create.unwrap_err(), TransactionError::Leaf(LeafError::ConversationAlreadyExists(i)) if i == id));
277            })
278        .await;
279    }
280
281    #[apply(all_cred_cipher)]
282    #[wasm_bindgen_test]
283    async fn can_fetch_client_public_key(mut case: TestContext) {
284        let tmp_dir = case.tmp_dir().await;
285        Box::pin(async move {
286            let configuration = MlsClientConfiguration::try_new(
287                tmp_dir,
288                DatabaseKey::generate(),
289                Some("potato".into()),
290                vec![case.ciphersuite()],
291                None,
292                Some(INITIAL_KEYING_MATERIAL_COUNT),
293            )
294            .unwrap();
295
296            let result = Session::try_new(configuration.clone()).await;
297            println!("{:?}", result);
298            assert!(result.is_ok());
299        })
300        .await
301    }
302
303    #[apply(all_cred_cipher)]
304    #[wasm_bindgen_test]
305    async fn can_2_phase_init_central(mut case: TestContext) {
306        let tmp_dir = case.tmp_dir().await;
307        Box::pin(async move {
308            let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
309            let configuration = MlsClientConfiguration::try_new(
310                tmp_dir,
311                DatabaseKey::generate(),
312                None,
313                vec![case.ciphersuite()],
314                None,
315                Some(INITIAL_KEYING_MATERIAL_COUNT),
316            )
317            .unwrap();
318            // phase 1: init without initialized mls_client
319            let client = Session::try_new(configuration).await.unwrap();
320            let cc = CoreCrypto::from(client);
321            let context = cc.new_transaction().await.unwrap();
322            x509_test_chain.register_with_central(&context).await;
323
324            assert!(!context.session().await.unwrap().is_ready().await);
325            // phase 2: init mls_client
326            let client_id = "alice";
327            let identifier = match case.credential_type {
328                MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
329                MlsCredentialType::X509 => {
330                    CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
331                }
332            };
333            context
334                .mls_init(
335                    identifier,
336                    vec![case.ciphersuite()],
337                    Some(INITIAL_KEYING_MATERIAL_COUNT),
338                )
339                .await
340                .unwrap();
341            assert!(context.session().await.unwrap().is_ready().await);
342            // expect mls_client to work
343            assert_eq!(
344                context
345                    .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
346                    .await
347                    .unwrap()
348                    .len(),
349                2
350            );
351        })
352        .await
353    }
354}