1use std::collections::HashSet;
24
25use mls_crypto_provider::DatabaseKey;
26use openmls::prelude::{KeyPackageSecretEncapsulation, SignatureScheme};
27
28use crate::{
29 CoreCrypto, Error, MlsError, RecursiveError, Result,
30 mls::credential::CredentialBundle,
31 prelude::{ClientId, ClientIdentifier, MlsCiphersuite, MlsClientConfiguration, MlsCredentialType, Session},
32};
33
34const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
37
38#[derive(serde::Serialize, serde::Deserialize)]
41pub struct HistorySecret {
42 pub(crate) client_id: ClientId,
43 pub(crate) credential_bundle: CredentialBundle,
44 pub(crate) key_package: KeyPackageSecretEncapsulation,
45}
46
47async fn in_memory_cc_with_ciphersuite(ciphersuite: impl Into<MlsCiphersuite>) -> Result<CoreCrypto> {
51 let ciphersuites = vec![ciphersuite.into()];
52
53 let configuration = MlsClientConfiguration {
54 ciphersuites: ciphersuites.clone(),
56 client_id: None,
59 store_path: String::new(),
61 database_key: DatabaseKey::generate(),
63 external_entropy: None,
65 nb_init_key_packages: Some(0),
67 };
68
69 let session = Session::try_new_in_memory(configuration)
72 .await
73 .map_err(RecursiveError::mls("creating ephemeral session"))?;
74
75 Ok(session.into())
76}
77
78pub(crate) async fn generate_history_secret(ciphersuite: MlsCiphersuite) -> Result<HistorySecret> {
88 let client_id = uuid::Uuid::new_v4();
90 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
91 let client_id = ClientId::from(client_id.into_bytes());
92 let identifier = ClientIdentifier::Basic(client_id);
93
94 let cc = in_memory_cc_with_ciphersuite(ciphersuite).await?;
95 let tx = cc
96 .new_transaction()
97 .await
98 .map_err(RecursiveError::transaction("creating new transaction"))?;
99 cc.init(identifier.clone(), &[ciphersuite], &cc.crypto_provider, 0)
100 .await
101 .map_err(RecursiveError::mls_client("initializing ephemeral cc"))?;
102
103 let mut signature_schemes = HashSet::with_capacity(1);
105 signature_schemes.insert(SignatureScheme::from(ciphersuite.0));
106 let bundles = identifier
107 .generate_credential_bundles(&cc.crypto_provider, signature_schemes)
108 .map_err(RecursiveError::mls_client("generating credential bundles"))?;
109 let [(_signature_scheme, client_id, credential_bundle)] = bundles
110 .try_into()
111 .expect("given exactly 1 signature scheme we must get exactly 1 credential bundle");
112
113 let [key_package] = tx
115 .get_or_create_client_keypackages(ciphersuite, MlsCredentialType::Basic, 1)
116 .await
117 .map_err(RecursiveError::transaction("generating keypackages"))?
118 .try_into()
119 .expect("generating 1 keypackage returns 1 keypackage");
120 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
121 .await
122 .map_err(MlsError::wrap("encapsulating key package"))?;
123
124 Ok(HistorySecret {
127 client_id,
128 credential_bundle,
129 key_package,
130 })
131}
132
133impl CoreCrypto {
134 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
139 if !history_secret
140 .client_id
141 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
142 {
143 return Err(Error::InvalidHistorySecret("client id has invalid format"));
144 }
145
146 let session = in_memory_cc_with_ciphersuite(history_secret.key_package.ciphersuite()).await?;
147 let tx = session
148 .new_transaction()
149 .await
150 .map_err(RecursiveError::transaction("creating new transaction"))?;
151
152 session
153 .restore_from_history_secret(history_secret)
154 .await
155 .map_err(RecursiveError::mls_client(
156 "restoring ephemeral session from history secret",
157 ))?;
158
159 tx.finish()
160 .await
161 .map_err(RecursiveError::transaction("finishing transaction"))?;
162
163 Ok(session)
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use rstest::rstest;
170 use rstest_reuse::apply;
171 use wasm_bindgen_test::wasm_bindgen_test;
172
173 use crate::test_utils::{SessionContext, TestContext, all_cred_cipher, conversation_id};
174
175 use super::*;
176
177 #[apply(all_cred_cipher)]
179 #[wasm_bindgen_test]
180 async fn can_create_ephemeral_client(case: TestContext) {
181 if case.credential_type != MlsCredentialType::Basic {
182 return;
185 }
186
187 use crate::mls::conversation::Conversation as _;
188
189 let [alice] = case.sessions().await;
190 let id = conversation_id();
191 alice
192 .transaction
193 .new_conversation(&id, case.credential_type, case.cfg.clone())
194 .await
195 .unwrap();
196
197 let conversation = alice.session.get_raw_conversation(&id).await.unwrap();
198 let history_secret = conversation.generate_history_secret().await.unwrap();
199
200 let encoded = rmp_serde::to_vec(&history_secret).unwrap();
204 let history_secret = rmp_serde::from_slice::<HistorySecret>(&encoded).unwrap();
205
206 let ephemeral_client = CoreCrypto::history_client(history_secret).await.unwrap();
207
208 let ephemeral_identifier = ClientIdentifier::Basic(ephemeral_client.mls.id().await.unwrap());
216 let ephemeral_session_context = SessionContext::new_with_identifier(&case, ephemeral_identifier, None)
217 .await
218 .unwrap();
219
220 alice
221 .invite_all(&case, &id, [&ephemeral_session_context])
222 .await
223 .unwrap();
224
225 assert!(ephemeral_session_context.try_talk_to(&id, &alice).await.is_ok());
226 }
227
228 #[apply(all_cred_cipher)]
229 #[wasm_bindgen_test]
230 async fn ephemeral_client_can_receive_messages_from_x509(case: TestContext) {
231 if case.credential_type != MlsCredentialType::X509 {
232 return;
234 }
235
236 use crate::{mls::conversation::Conversation as _, test_utils::x509::X509TestChain};
237
238 let mut x509_test_chain = X509TestChain::init_empty(case.signature_scheme());
240 let (alice_identifier, _) = x509_test_chain.issue_simple_certificate_bundle("alice", None);
241 let alice = SessionContext::new_with_identifier(&case, alice_identifier, Some((&x509_test_chain).into()))
242 .await
243 .unwrap();
244
245 let id = conversation_id();
246 alice
247 .transaction
248 .new_conversation(&id, case.credential_type, case.cfg.clone())
249 .await
250 .unwrap();
251
252 let conversation = alice.session.get_raw_conversation(&id).await.unwrap();
254 let history_secret = conversation.generate_history_secret().await.unwrap();
255 let ephemeral_client = CoreCrypto::history_client(history_secret).await.unwrap();
256
257 let ephemeral_identifier = ClientIdentifier::Basic(ephemeral_client.mls.id().await.unwrap());
258 let ephemeral_session_context = SessionContext::new_with_identifier(&case, ephemeral_identifier, None)
259 .await
260 .unwrap();
261
262 let eph_kp = ephemeral_session_context
264 .rand_key_package_of_type(&case, MlsCredentialType::Basic)
265 .await;
266 alice
267 .invite_all_members(&case, &id, [(&ephemeral_session_context, eph_kp)])
268 .await
269 .unwrap();
270
271 assert!(alice.try_talk_to(&id, &ephemeral_session_context).await.is_ok());
272 }
273}