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 [Client].
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 [MlsCentral]
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. Default to [INITIAL_KEYING_MATERIAL_COUNT]
40        pub nb_init_key_packages: Option<usize>,
41    }
42
43    impl MlsClientConfiguration {
44        /// Creates a new instance of the configuration.
45        ///
46        /// # Arguments
47        /// * `store_path` - location where the SQLite/IndexedDB database will be stored
48        /// * `database_key` - key to be used to instantiate the [MlsCryptoProvider]
49        /// * `client_id` - identifier for the client to be used by [MlsCentral]
50        /// * `ciphersuites` - Ciphersuites supported by this device
51        /// * `entropy` - External source of entropy for platforms where default source insufficient
52        ///
53        /// # Errors
54        /// Any empty string parameter will result in a [Error::MalformedIdentifier] error.
55        ///
56        /// # Examples
57        ///
58        /// ```
59        /// use core_crypto::prelude::{MlsClientConfiguration, MlsCiphersuite};
60        /// use core_crypto::DatabaseKey;
61        ///
62        /// let result = MlsClientConfiguration::try_new(
63        ///     "/tmp/crypto".to_string(),
64        ///     DatabaseKey::generate(),
65        ///     Some(b"MY_CLIENT_ID".to_vec().into()),
66        ///     vec![MlsCiphersuite::default()],
67        ///     None,
68        ///     Some(100),
69        /// );
70        /// assert!(result.is_ok());
71        /// ```
72        pub fn try_new(
73            store_path: String,
74            database_key: DatabaseKey,
75            client_id: Option<ClientId>,
76            ciphersuites: Vec<MlsCiphersuite>,
77            entropy: Option<Vec<u8>>,
78            nb_init_key_packages: Option<usize>,
79        ) -> Result<Self> {
80            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
81            if store_path.trim().is_empty() {
82                return Err(Error::MalformedIdentifier("store_path"));
83            }
84            // TODO: probably more complex rules to enforce. Tracking issue: WPB-9598
85            if let Some(client_id) = client_id.as_ref() {
86                if client_id.is_empty() {
87                    return Err(Error::MalformedIdentifier("client_id"));
88                }
89            }
90            let external_entropy = entropy
91                .as_deref()
92                .map(|seed| &seed[..EntropySeed::EXPECTED_LEN])
93                .map(EntropySeed::try_from_slice)
94                .transpose()
95                .map_err(MlsError::wrap("gathering external entropy"))?;
96            Ok(Self {
97                store_path,
98                database_key,
99                client_id,
100                ciphersuites,
101                external_entropy,
102                nb_init_key_packages,
103            })
104        }
105
106        /// Sets the entropy seed
107        pub fn set_entropy(&mut self, entropy: EntropySeed) {
108            self.external_entropy = Some(entropy);
109        }
110
111        #[cfg(test)]
112        #[allow(dead_code)]
113        /// Creates temporary file to prevent test collisions which would happen with hardcoded file path
114        /// Intended to be used only in tests.
115        pub(crate) fn tmp_store_path(tmp_dir: &tempfile::TempDir) -> String {
116            let path = tmp_dir.path().join("store.edb");
117            std::fs::File::create(&path).unwrap();
118            path.to_str().unwrap().to_string()
119        }
120    }
121}
122
123#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
124#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
125pub(crate) trait HasSessionAndCrypto: Send {
126    async fn session(&self) -> Result<Session>;
127    async fn crypto_provider(&self) -> Result<MlsCryptoProvider>;
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::transaction_context::Error as TransactionError;
133    use wasm_bindgen_test::*;
134
135    use crate::prelude::{
136        CertificateBundle, ClientIdentifier, INITIAL_KEYING_MATERIAL_COUNT, MlsClientConfiguration, MlsCredentialType,
137    };
138    use crate::{
139        CoreCrypto,
140        mls::Session,
141        test_utils::{x509::X509TestChain, *},
142    };
143
144    wasm_bindgen_test_configure!(run_in_browser);
145
146    use core_crypto_keystore::DatabaseKey;
147
148    mod conversation_epoch {
149        use super::*;
150        use crate::mls::conversation::Conversation as _;
151
152        #[apply(all_cred_cipher)]
153        #[wasm_bindgen_test]
154        async fn can_get_newly_created_conversation_epoch(case: TestContext) {
155            let [session] = case.sessions().await;
156            let id = conversation_id();
157            session
158                .transaction
159                .new_conversation(&id, case.credential_type, case.cfg.clone())
160                .await
161                .unwrap();
162            let epoch = session.transaction.conversation(&id).await.unwrap().epoch().await;
163            assert_eq!(epoch, 0);
164        }
165
166        #[apply(all_cred_cipher)]
167        #[wasm_bindgen_test]
168        async fn can_get_conversation_epoch(case: TestContext) {
169            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
170                Box::pin(async move {
171                    let id = conversation_id();
172                    alice_central
173                        .transaction
174                        .new_conversation(&id, case.credential_type, case.cfg.clone())
175                        .await
176                        .unwrap();
177                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
178                    let epoch = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
179                    assert_eq!(epoch, 1);
180                })
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(case: TestContext) {
207            run_tests(move |[tmp_dir_argument]| {
208                Box::pin(async move {
209                    let configuration = MlsClientConfiguration::try_new(
210                        tmp_dir_argument,
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            })
223            .await
224        }
225
226        #[test]
227        #[wasm_bindgen_test]
228        fn store_path_should_not_be_empty_nor_blank() {
229            let ciphersuites = vec![MlsCiphersuite::default()];
230            let configuration = MlsClientConfiguration::try_new(
231                " ".to_string(),
232                DatabaseKey::generate(),
233                Some("alice".into()),
234                ciphersuites,
235                None,
236                Some(INITIAL_KEYING_MATERIAL_COUNT),
237            );
238            assert!(matches!(
239                configuration.unwrap_err(),
240                mls::Error::MalformedIdentifier("store_path")
241            ));
242        }
243
244        #[cfg_attr(not(target_family = "wasm"), async_std::test)]
245        #[wasm_bindgen_test]
246        async fn client_id_should_not_be_empty() {
247            run_tests(|[tmp_dir_argument]| {
248                Box::pin(async move {
249                    let ciphersuites = vec![MlsCiphersuite::default()];
250                    let configuration = MlsClientConfiguration::try_new(
251                        tmp_dir_argument,
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            })
264            .await
265        }
266    }
267
268    #[apply(all_cred_cipher)]
269    #[wasm_bindgen_test]
270    async fn create_conversation_should_fail_when_already_exists(case: TestContext) {
271        use crate::LeafError;
272
273        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
274            Box::pin(async move {
275                let id = conversation_id();
276
277                let create = alice_central
278                    .transaction
279                    .new_conversation(&id, case.credential_type, case.cfg.clone())
280                    .await;
281                assert!(create.is_ok());
282
283                // creating a conversation should first verify that the conversation does not already exist ; only then create it
284                let repeat_create = alice_central
285                    .transaction
286                    .new_conversation(&id, case.credential_type, case.cfg.clone())
287                    .await;
288                assert!(matches!(repeat_create.unwrap_err(), TransactionError::Leaf(LeafError::ConversationAlreadyExists(i)) if i == id));
289            })
290        })
291        .await;
292    }
293
294    #[apply(all_cred_cipher)]
295    #[wasm_bindgen_test]
296    async fn can_fetch_client_public_key(case: TestContext) {
297        run_tests(move |[tmp_dir_argument]| {
298            Box::pin(async move {
299                let configuration = MlsClientConfiguration::try_new(
300                    tmp_dir_argument,
301                    DatabaseKey::generate(),
302                    Some("potato".into()),
303                    vec![case.ciphersuite()],
304                    None,
305                    Some(INITIAL_KEYING_MATERIAL_COUNT),
306                )
307                .unwrap();
308
309                let result = Session::try_new(configuration.clone()).await;
310                println!("{:?}", result);
311                assert!(result.is_ok());
312            })
313        })
314        .await
315    }
316
317    #[apply(all_cred_cipher)]
318    #[wasm_bindgen_test]
319    async fn can_2_phase_init_central(case: TestContext) {
320        run_tests(move |[tmp_dir_argument]| {
321            Box::pin(async move {
322                let x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
323                let configuration = MlsClientConfiguration::try_new(
324                    tmp_dir_argument,
325                    DatabaseKey::generate(),
326                    None,
327                    vec![case.ciphersuite()],
328                    None,
329                    Some(INITIAL_KEYING_MATERIAL_COUNT),
330                )
331                .unwrap();
332                // phase 1: init without initialized mls_client
333                let client = Session::try_new(configuration).await.unwrap();
334                let cc = CoreCrypto::from(client);
335                let context = cc.new_transaction().await.unwrap();
336                x509_test_chain.register_with_central(&context).await;
337
338                assert!(!context.session().await.unwrap().is_ready().await);
339                // phase 2: init mls_client
340                let client_id = "alice";
341                let identifier = match case.credential_type {
342                    MlsCredentialType::Basic => ClientIdentifier::Basic(client_id.into()),
343                    MlsCredentialType::X509 => {
344                        CertificateBundle::rand_identifier(client_id, &[x509_test_chain.find_local_intermediate_ca()])
345                    }
346                };
347                context
348                    .mls_init(
349                        identifier,
350                        vec![case.ciphersuite()],
351                        Some(INITIAL_KEYING_MATERIAL_COUNT),
352                    )
353                    .await
354                    .unwrap();
355                assert!(context.session().await.unwrap().is_ready().await);
356                // expect mls_client to work
357                assert_eq!(
358                    context
359                        .get_or_create_client_keypackages(case.ciphersuite(), case.credential_type, 2)
360                        .await
361                        .unwrap()
362                        .len(),
363                    2
364                );
365            })
366        })
367        .await
368    }
369}