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>,
41 }
42
43 impl MlsClientConfiguration {
44 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 if store_path.trim().is_empty() {
82 return Err(Error::MalformedIdentifier("store_path"));
83 }
84 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 pub fn set_entropy(&mut self, entropy: EntropySeed) {
108 self.external_entropy = Some(entropy);
109 }
110
111 #[cfg(test)]
112 #[allow(dead_code)]
113 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 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 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 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 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}