1use std::{borrow::Borrow, sync::Arc};
25
26use core_crypto_keystore::Database;
27use obfuscate::{Obfuscate, Obfuscated};
28use openmls::prelude::KeyPackageSecretEncapsulation;
29
30use crate::{
31 CipherSuite, ClientId, ClientIdRef, CoreCrypto, CoreCryptoTransportNotImplementedProvider, Credential, Error,
32 OpenMlsError, RecursiveError, Result, Session, mls_provider::CryptoProvider,
33};
34
35pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
38
39#[derive(serde::Serialize, serde::Deserialize)]
42pub struct HistorySecret {
43 pub client_id: ClientId,
45 pub(crate) key_package: KeyPackageSecretEncapsulation,
46}
47
48impl Obfuscate for HistorySecret {
49 fn obfuscate(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
50 f.debug_struct("HistorySecret")
51 .field("client_id", &self.client_id)
52 .field("key_package", &Obfuscated::from(&self.key_package))
53 .finish()
54 }
55}
56
57pub(crate) async fn generate_history_secret(cipher_suite: CipherSuite) -> Result<HistorySecret> {
67 let session_id = ClientId::new_ephemeral();
69
70 let database =
71 Database::open_in_memory().expect("Opening an in-memory database to generate a history secret cannot fail");
72
73 let cc = CoreCrypto::new(database.clone());
74 let tx = cc
75 .new_transaction()
76 .await
77 .map_err(RecursiveError::transaction("creating new transaction"))?;
78
79 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
80 tx.mls_init(session_id.clone(), transport)
81 .await
82 .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
83 let session = tx
84 .session()
85 .await
86 .map_err(RecursiveError::transaction("Getting mls session"))?;
87 let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::mls_credential(
88 "generating basic credential for ephemeral client",
89 ))?;
90 let credential_ref = tx
91 .add_credential(credential)
92 .await
93 .map_err(RecursiveError::transaction(
94 "adding basic credential to ephemeral client",
95 ))?;
96
97 let key_package = tx
99 .generate_key_package(&credential_ref, None)
100 .await
101 .map_err(RecursiveError::transaction("generating keypackage"))?;
102 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
103 .await
104 .map_err(OpenMlsError::wrap("encapsulating key package"))?;
105
106 let _ = tx.abort().await;
109
110 Ok(HistorySecret {
111 client_id: session_id,
112 key_package,
113 })
114}
115
116pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
117 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
118}
119
120impl CoreCrypto {
121 pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<Self>> {
126 if !history_secret
127 .client_id
128 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
129 {
130 return Err(Error::InvalidHistorySecret("client id has invalid format"));
131 }
132
133 let database =
135 Database::open_in_memory().expect("Opening an in-memory database for a history client cannot fail");
136
137 let cc = CoreCrypto::new(database.clone());
138 let tx = cc
139 .new_transaction()
140 .await
141 .map_err(RecursiveError::transaction("creating new transaction"))?;
142
143 let mls_backend = CryptoProvider::new(database.clone());
145 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
146 let session = Session::new(
147 history_secret.client_id.clone(),
148 mls_backend,
149 database.into(),
150 transport,
151 );
152
153 session
154 .restore_from_history_secret(history_secret)
155 .await
156 .map_err(RecursiveError::mls_client(
157 "restoring ephemeral session from history secret",
158 ))?;
159
160 tx.set_mls_session(session)
161 .await
162 .map_err(RecursiveError::transaction("Setting mls session"))?;
163
164 tx.finish()
165 .await
166 .map_err(RecursiveError::transaction("finishing transaction"))?;
167
168 Ok(cc)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use rstest::rstest;
175 use rstest_reuse::apply;
176
177 use crate::test_utils::{TestContext, all_cred_cipher};
178
179 #[apply(all_cred_cipher)]
181 async fn can_create_ephemeral_client(case: TestContext) {
182 let [alice] = case.sessions().await;
183 let conversation = case.create_conversation([&alice]).await;
184 let conversation = conversation.enable_history_sharing_notify().await;
185
186 assert_eq!(
187 conversation.member_count().await,
188 2,
189 "the conversation should now magically have a second member"
190 );
191
192 let ephemeral_client = conversation.members().nth(1).unwrap();
193 assert!(
194 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
195 "alice can send messages to the history client"
196 );
197 assert!(
198 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
199 "the history client cannot send messages"
200 );
201 }
202}