1use std::borrow::Borrow;
25
26use core_crypto_keystore::{ConnectionType, Database};
27use mls_crypto_provider::DatabaseKey;
28use obfuscate::{Obfuscate, Obfuscated};
29use openmls::prelude::KeyPackageSecretEncapsulation;
30
31use crate::{
32 Ciphersuite, ClientId, ClientIdRef, ClientIdentifier, CoreCrypto, Credential, Error, MlsError, RecursiveError,
33 Result, Session,
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
58async fn in_memory_cc() -> Result<CoreCrypto> {
62 let db = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
63 .await
64 .unwrap();
65
66 let session = Session::try_new(&db)
67 .await
68 .map_err(RecursiveError::mls("creating ephemeral session"))?;
69
70 Ok(session.into())
71}
72
73pub(crate) async fn generate_history_secret(ciphersuite: Ciphersuite) -> Result<HistorySecret> {
83 let client_id = uuid::Uuid::new_v4();
85 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
86 let client_id = ClientId::from(client_id.into_bytes());
87 let identifier = ClientIdentifier::Basic(client_id.clone());
88
89 let cc = in_memory_cc().await?;
90 let tx = cc
91 .new_transaction()
92 .await
93 .map_err(RecursiveError::transaction("creating new transaction"))?;
94 cc.init(identifier, &[ciphersuite.signature_algorithm()])
95 .await
96 .map_err(RecursiveError::mls_client("initializing ephemeral cc"))?;
97
98 let credential = Credential::basic(ciphersuite, client_id.clone(), &cc.mls.crypto_provider).map_err(
99 RecursiveError::mls_credential("generating basic credential for ephemeral client"),
100 )?;
101 let credential_ref = cc.add_credential(credential).await.map_err(RecursiveError::mls_client(
102 "adding basic credential to ephemeral client",
103 ))?;
104
105 let key_package = tx
107 .generate_keypackage(&credential_ref, None)
108 .await
109 .map_err(RecursiveError::transaction("generating keypackage"))?;
110 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
111 .await
112 .map_err(MlsError::wrap("encapsulating key package"))?;
113
114 let _ = tx.abort().await;
117
118 Ok(HistorySecret { client_id, key_package })
119}
120
121pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
122 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
123}
124
125impl CoreCrypto {
126 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
131 if !history_secret
132 .client_id
133 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
134 {
135 return Err(Error::InvalidHistorySecret("client id has invalid format"));
136 }
137
138 let session = in_memory_cc().await?;
139 let tx = session
140 .new_transaction()
141 .await
142 .map_err(RecursiveError::transaction("creating new transaction"))?;
143
144 session
145 .restore_from_history_secret(history_secret)
146 .await
147 .map_err(RecursiveError::mls_client(
148 "restoring ephemeral session from history secret",
149 ))?;
150
151 tx.finish()
152 .await
153 .map_err(RecursiveError::transaction("finishing transaction"))?;
154
155 Ok(session)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use rstest::rstest;
162 use rstest_reuse::apply;
163
164 use crate::test_utils::{TestContext, all_cred_cipher};
165
166 #[apply(all_cred_cipher)]
168 async fn can_create_ephemeral_client(case: TestContext) {
169 let [alice] = case.sessions().await;
170 let conversation = case.create_conversation([&alice]).await;
171 let conversation = conversation.enable_history_sharing_notify().await;
172
173 assert_eq!(
174 conversation.member_count().await,
175 2,
176 "the conversation should now magically have a second member"
177 );
178
179 let ephemeral_client = conversation.members().nth(1).unwrap();
180 assert!(
181 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
182 "alice can send messages to the history client"
183 );
184 assert!(
185 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
186 "the history client cannot send messages"
187 );
188 }
189}