1use std::{borrow::Borrow, sync::Arc};
25
26use core_crypto_keystore::{ConnectionType, 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,
33 mls_provider::{CryptoProvider, DatabaseKey},
34};
35
36pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
39
40#[derive(serde::Serialize, serde::Deserialize)]
43pub struct HistorySecret {
44 pub client_id: ClientId,
46 pub(crate) key_package: KeyPackageSecretEncapsulation,
47}
48
49impl Obfuscate for HistorySecret {
50 fn obfuscate(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
51 f.debug_struct("HistorySecret")
52 .field("client_id", &self.client_id)
53 .field("key_package", &Obfuscated::from(&self.key_package))
54 .finish()
55 }
56}
57
58pub(crate) async fn generate_history_secret(cipher_suite: CipherSuite) -> Result<HistorySecret> {
68 let session_id = uuid::Uuid::new_v4();
70 let session_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{session_id}");
71 let session_id = ClientId::from(session_id.into_bytes());
72
73 let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
74 .await
75 .unwrap();
76
77 let cc = CoreCrypto::new(database.clone());
78 let tx = cc
79 .new_transaction()
80 .await
81 .map_err(RecursiveError::transaction("creating new transaction"))?;
82
83 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
84 tx.mls_init(session_id.clone(), transport)
85 .await
86 .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
87 let session = tx
88 .session()
89 .await
90 .map_err(RecursiveError::transaction("Getting mls session"))?;
91 let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::mls_credential(
92 "generating basic credential for ephemeral client",
93 ))?;
94 let credential_ref = tx
95 .add_credential(credential)
96 .await
97 .map_err(RecursiveError::transaction(
98 "adding basic credential to ephemeral client",
99 ))?;
100
101 let key_package = tx
103 .generate_key_package(&credential_ref, None)
104 .await
105 .map_err(RecursiveError::transaction("generating keypackage"))?;
106 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
107 .await
108 .map_err(OpenMlsError::wrap("encapsulating key package"))?;
109
110 let _ = tx.abort().await;
113
114 Ok(HistorySecret {
115 client_id: session_id,
116 key_package,
117 })
118}
119
120pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
121 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
122}
123
124impl CoreCrypto {
125 pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<Self>> {
130 if !history_secret
131 .client_id
132 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
133 {
134 return Err(Error::InvalidHistorySecret("client id has invalid format"));
135 }
136
137 let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
139 .await
140 .unwrap();
141
142 let cc = CoreCrypto::new(database.clone());
143 let tx = cc
144 .new_transaction()
145 .await
146 .map_err(RecursiveError::transaction("creating new transaction"))?;
147
148 let mls_backend = CryptoProvider::new(database.clone());
150 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
151 let session = Session::new(
152 history_secret.client_id.clone(),
153 mls_backend,
154 database.into(),
155 transport,
156 );
157
158 session
159 .restore_from_history_secret(history_secret)
160 .await
161 .map_err(RecursiveError::mls_client(
162 "restoring ephemeral session from history secret",
163 ))?;
164
165 tx.set_mls_session(session)
166 .await
167 .map_err(RecursiveError::transaction("Setting mls session"))?;
168
169 tx.finish()
170 .await
171 .map_err(RecursiveError::transaction("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}