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 id = conversation_id();
158            session
159                .transaction
160                .new_conversation(&id, case.credential_type, case.cfg.clone())
161                .await
162                .unwrap();
163            let epoch = session.transaction.conversation(&id).await.unwrap().epoch().await;
164            assert_eq!(epoch, 0);
165        }
166
167        #[apply(all_cred_cipher)]
168        #[wasm_bindgen_test]
169        async fn can_get_conversation_epoch(case: TestContext) {
170            let [alice_central, bob_central] = case.sessions().await;
171            Box::pin(async move {
172                let id = conversation_id();
173                alice_central
174                    .transaction
175                    .new_conversation(&id, case.credential_type, case.cfg.clone())
176                    .await
177                    .unwrap();
178                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
179                let epoch = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
180                assert_eq!(epoch, 1);
181            })
182            .await;
183        }
184
185        #[apply(all_cred_cipher)]
186        #[wasm_bindgen_test]
187        async fn conversation_not_found(case: TestContext) {
188            use crate::LeafError;
189            let [session] = case.sessions().await;
190            let id = conversation_id();
191            let err = session.transaction.conversation(&id).await.unwrap_err();
192            assert!(matches!(
193                err,
194                TransactionError::Leaf(LeafError::ConversationNotFound(i)) if i == id
195            ));
196        }
197    }
198
199    mod invariants {
200        use crate::{mls, prelude::MlsCiphersuite};
201
202        use super::*;
203
204        #[apply(all_cred_cipher)]
205        #[wasm_bindgen_test]
206        async fn can_create_from_valid_configuration(mut case: TestContext) {
207            let tmp_dir = case.tmp_dir().await;
208            Box::pin(async move {
209                let configuration = MlsClientConfiguration::try_new(
210                    tmp_dir,
211                    DatabaseKey::generate(),
212                    Some("alice".into()),
213                    vec![case.ciphersuite()],
214                    None,
215                    Some(INITIAL_KEYING_MATERIAL_COUNT),
216                )
217                .unwrap();
218
219                let new_client_result = Session::try_new(configuration).await;
220                assert!(new_client_result.is_ok())
221            })
222            .await
223        }
224
225        #[test]
226        #[wasm_bindgen_test]
227        fn store_path_should_not_be_empty_nor_blank() {
228            let ciphersuites = vec![MlsCiphersuite::default()];
229            let configuration = MlsClientConfiguration::try_new(
230                " ".to_string(),
231                DatabaseKey::generate(),
232                Some("alice".into()),
233                ciphersuites,
234                None,
235                Some(INITIAL_KEYING_MATERIAL_COUNT),
236            );
237            assert!(matches!(
238                configuration.unwrap_err(),
239                mls::Error::MalformedIdentifier("store_path")
240            ));
241        }
242
243        #[cfg_attr(not(target_family = "wasm"), async_std::test)]
244        #[wasm_bindgen_test]
245        async fn client_id_should_not_be_empty() {
246            let mut case = TestContext::default();
247            let tmp_dir = case.tmp_dir().await;
248            Box::pin(async move {
249                let ciphersuites = vec![MlsCiphersuite::default()];
250                let configuration = MlsClientConfiguration::try_new(
251                    tmp_dir,
252                    DatabaseKey::generate(),
253                    Some("".into()),
254                    ciphersuites,
255                    None,
256                    Some(INITIAL_KEYING_MATERIAL_COUNT),
257                );
258                assert!(matches!(
259                    configuration.unwrap_err(),
260                    mls::Error::MalformedIdentifier("client_id")
261                ));
262            })
263            .await
264        }
265    }
266
267    #[apply(all_cred_cipher)]
268    #[wasm_bindgen_test]
269    async fn create_conversation_should_fail_when_already_exists(case: TestContext) {
270        use crate::LeafError;
271
272        let [alice_central] = case.sessions().await;
273        Box::pin(async move {
274                let id = conversation_id();
275
276                let create = alice_central
277                    .transaction
278                    .new_conversation(&id, case.credential_type, case.cfg.clone())
279                    .await;
280                assert!(create.is_ok());
281
282                // creating a conversation should first verify that the conversation does not already exist ; only then create it
283                let repeat_create = alice_central
284                    .transaction
285                    .new_conversation(&id, case.credential_type, case.cfg.clone())
286                    .await;
287                assert!(matches!(repeat_create.unwrap_err(), TransactionError::Leaf(LeafError::ConversationAlreadyExists(i)) if i == id));
288            })
289        .await;
290    }
291
292    #[apply(all_cred_cipher)]
293    #[wasm_bindgen_test]
294    async fn can_fetch_client_public_key(mut case: TestContext) {
295        let tmp_dir = case.tmp_dir().await;
296        Box::pin(async move {
297            let configuration = MlsClientConfiguration::try_new(
298                tmp_dir,
299                DatabaseKey::generate(),
300                Some("potato".into()),
301                vec![case.ciphersuite()],
302                None,
303                Some(INITIAL_KEYING_MATERIAL_COUNT),
304            )
305            .unwrap();
306
307            let result = Session::try_new(configuration.clone()).await;
308            println!("{:?}", result);
309            assert!(result.is_ok());
310        })
311        .await
312    }
313
314    #[apply(all_cred_cipher)]
315    #[wasm_bindgen_test]
316    async fn can_2_phase_init_central(mut case: TestContext) {
317        let tmp_dir = case.tmp_dir().await;
318        Box::pin(async move {
319            let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
320            let configuration = MlsClientConfiguration::try_new(
321                tmp_dir,
322                DatabaseKey::generate(),
323                None,
324                vec![case.ciphersuite()],
325                None,
326                Some(INITIAL_KEYING_MATERIAL_COUNT),
327            )
328            .unwrap();
329            // phase 1: init without initialized mls_client
330            let client = Session::try_new(configuration).await.unwrap();
331            let cc = CoreCrypto::from(client);
332            let context = cc.new_transaction().await.unwrap();
333            x509_test_chain.register_with_central(&context).await;
334
335            assert!(!context.session().await.unwrap().is_ready().await);
336            // phase 2: init mls_client
337            let client_id = "alice";
338            let identifier = match case.credential_type {
339                MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
340                MlsCredentialType::X509 => {
341                    CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
342                }
343            };
344            context
345                .mls_init(
346                    identifier,
347                    vec![case.ciphersuite()],
348                    Some(INITIAL_KEYING_MATERIAL_COUNT),
349                )
350                .await
351                .unwrap();
352            assert!(context.session().await.unwrap().is_ready().await);
353            // expect mls_client to work
354            assert_eq!(
355                context
356                    .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
357                    .await
358                    .unwrap()
359                    .len(),
360                2
361            );
362        })
363        .await
364    }
365}