1use std::{borrow::Borrow, sync::Arc};
25
26use core_crypto_keystore::{ConnectionType, Database};
27use mls_crypto_provider::{DatabaseKey, MlsCryptoProvider};
28use obfuscate::{Obfuscate, Obfuscated};
29use openmls::prelude::KeyPackageSecretEncapsulation;
30
31use crate::{
32 Ciphersuite, ClientId, ClientIdRef, ClientIdentifier, CoreCrypto, CoreCryptoTransportNotImplementedProvider,
33 Credential, Error, MlsError, RecursiveError, Result, Session, mls::session::identities::Identities,
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(ciphersuite: Ciphersuite) -> Result<HistorySecret> {
68 let client_id = uuid::Uuid::new_v4();
70 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
71 let client_id = ClientId::from(client_id.into_bytes());
72 let identifier = ClientIdentifier::Basic(client_id.clone());
73
74 let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
75 .await
76 .unwrap();
77
78 let cc = CoreCrypto::new(database.clone());
79 let tx = cc
80 .new_transaction()
81 .await
82 .map_err(RecursiveError::transaction("creating new transaction"))?;
83
84 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
85 tx.mls_init(identifier, &[ciphersuite], transport)
86 .await
87 .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
88 let session = tx
89 .session()
90 .await
91 .map_err(RecursiveError::transaction("Getting mls session"))?;
92 let credential = Credential::basic(ciphersuite, client_id.clone(), &session.crypto_provider).map_err(
93 RecursiveError::mls_credential("generating basic credential for ephemeral client"),
94 )?;
95 let credential_ref = session
96 .add_credential(credential)
97 .await
98 .map_err(RecursiveError::mls_client(
99 "adding basic credential to ephemeral client",
100 ))?;
101
102 let key_package = tx
104 .generate_keypackage(&credential_ref, None)
105 .await
106 .map_err(RecursiveError::transaction("generating keypackage"))?;
107 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
108 .await
109 .map_err(MlsError::wrap("encapsulating key package"))?;
110
111 let _ = tx.abort().await;
114
115 Ok(HistorySecret { client_id, key_package })
116}
117
118pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
119 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
120}
121
122impl CoreCrypto {
123 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
128 if !history_secret
129 .client_id
130 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
131 {
132 return Err(Error::InvalidHistorySecret("client id has invalid format"));
133 }
134
135 let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
137 .await
138 .unwrap();
139
140 let cc = CoreCrypto::new(database.clone());
141 let tx = cc
142 .new_transaction()
143 .await
144 .map_err(RecursiveError::transaction("creating new transaction"))?;
145
146 let mls_backend = MlsCryptoProvider::new(database);
148 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
149 let session = Session::new(
150 history_secret.client_id.clone(),
151 Identities::new(0),
152 mls_backend,
153 transport,
154 );
155
156 session
157 .restore_from_history_secret(history_secret)
158 .await
159 .map_err(RecursiveError::mls_client(
160 "restoring ephemeral session from history secret",
161 ))?;
162
163 tx.set_mls_session(session)
164 .await
165 .map_err(RecursiveError::transaction("Setting mls session"))?;
166
167 tx.finish()
168 .await
169 .map_err(RecursiveError::transaction("finishing transaction"))?;
170
171 Ok(cc)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use rstest::rstest;
178 use rstest_reuse::apply;
179
180 use crate::test_utils::{TestContext, all_cred_cipher};
181
182 #[apply(all_cred_cipher)]
184 async fn can_create_ephemeral_client(case: TestContext) {
185 let [alice] = case.sessions().await;
186 let conversation = case.create_conversation([&alice]).await;
187 let conversation = conversation.enable_history_sharing_notify().await;
188
189 assert_eq!(
190 conversation.member_count().await,
191 2,
192 "the conversation should now magically have a second member"
193 );
194
195 let ephemeral_client = conversation.members().nth(1).unwrap();
196 assert!(
197 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
198 "alice can send messages to the history client"
199 );
200 assert!(
201 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
202 "the history client cannot send messages"
203 );
204 }
205}