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 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 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 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 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 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}