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::context("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::context("initializing ephemeral cc"))?;
83 let session = tx
84 .session()
85 .await
86 .map_err(RecursiveError::context("Getting mls session"))?;
87 let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::context(
88 "generating basic credential for ephemeral client",
89 ))?;
90 let credential_ref = tx
91 .add_credential(credential)
92 .await
93 .map_err(RecursiveError::context("adding basic credential to ephemeral client"))?;
94
95 let key_package = tx
97 .generate_key_package(&credential_ref, None)
98 .await
99 .map_err(RecursiveError::context("generating keypackage"))?;
100 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
101 .await
102 .map_err(OpenMlsError::wrap("encapsulating key package"))?;
103
104 let _ = tx.abort().await;
107
108 Ok(HistorySecret {
109 client_id: session_id,
110 key_package,
111 })
112}
113
114pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
115 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
116}
117
118impl CoreCrypto {
119 pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<Self>> {
124 if !history_secret
125 .client_id
126 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
127 {
128 return Err(Error::InvalidHistorySecret("client id has invalid format"));
129 }
130
131 let database =
133 Database::open_in_memory().expect("Opening an in-memory database for a history client cannot fail");
134
135 let cc = CoreCrypto::new(database.clone());
136 let tx = cc
137 .new_transaction()
138 .await
139 .map_err(RecursiveError::context("creating new transaction"))?;
140
141 let mls_backend = CryptoProvider::new(database.clone());
143 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
144 let session = Session::new(
145 history_secret.client_id.clone(),
146 mls_backend,
147 database.into(),
148 transport,
149 );
150
151 let credential = session
152 .restore_from_history_secret(history_secret)
153 .await
154 .map_err(RecursiveError::context(
155 "restoring ephemeral session from history secret",
156 ))?;
157
158 tx.set_mls_session(session)
159 .await
160 .map_err(RecursiveError::context("Setting mls session"))?;
161
162 tx.add_credential(credential)
166 .await
167 .map_err(RecursiveError::context("adding credential to history client"))?;
168
169 tx.finish()
170 .await
171 .map_err(RecursiveError::context("finishing transaction"))?;
172
173 Ok(cc)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use rstest::rstest;
180 use rstest_reuse::apply;
181
182 use crate::test_utils::{TestContext, all_cred_cipher};
183
184 #[apply(all_cred_cipher)]
186 async fn can_create_ephemeral_client(case: TestContext) {
187 let [alice] = case.sessions().await;
188 let conversation = case.create_conversation([&alice]).await;
189 let conversation = conversation.enable_history_sharing_notify().await;
190
191 assert_eq!(
192 conversation.member_count().await,
193 2,
194 "the conversation should now magically have a second member"
195 );
196
197 let ephemeral_client = conversation.members().nth(1).unwrap();
198 assert!(
199 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
200 "alice can send messages to the history client"
201 );
202 assert!(
203 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
204 "the history client cannot send messages"
205 );
206 }
207}