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 MlsError, RecursiveError, Result, Session,
33 mls_provider::{DatabaseKey, MlsCryptoProvider},
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 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(ciphersuite, 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_keypackage(&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(MlsError::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<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 = MlsCryptoProvider::new(database.clone());
150 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
151 let session = Session::new(history_secret.client_id.clone(), mls_backend, database, transport);
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}