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
18pub(crate) mod config {
20 use ciphersuite::MlsCiphersuite;
21 use mls_crypto_provider::EntropySeed;
22
23 use super::*;
24
25 #[derive(Debug, Clone)]
27 #[non_exhaustive]
28 pub struct MlsClientConfiguration {
29 pub store_path: String,
31 pub database_key: DatabaseKey,
33 pub client_id: Option<ClientId>,
35 pub external_entropy: Option<EntropySeed>,
37 pub ciphersuites: Vec<ciphersuite::MlsCiphersuite>,
39 pub nb_init_key_packages: Option<usize>,
42 }
43
44 impl MlsClientConfiguration {
45 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 if store_path.trim().is_empty() {
83 return Err(Error::MalformedIdentifier("store_path"));
84 }
85 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 pub fn set_entropy(&mut self, entropy: EntropySeed) {
109 self.external_entropy = Some(entropy);
110 }
111
112 #[cfg(test)]
113 #[allow(dead_code)]
114 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 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 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 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 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}