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, CredentialType, Error, MlsError,
33 RecursiveError, 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(
99 ciphersuite.signature_algorithm(),
100 client_id.clone(),
101 &cc.mls.crypto_provider,
102 )
103 .map_err(RecursiveError::mls_credential(
104 "generating basic credential for ephemeral client",
105 ))?;
106 cc.add_credential(credential).await.map_err(RecursiveError::mls_client(
107 "adding basic credential to ephemeral client",
108 ))?;
109
110 let [key_package] = tx
112 .get_or_create_client_keypackages(ciphersuite, CredentialType::Basic, 1)
113 .await
114 .map_err(RecursiveError::transaction("generating keypackages"))?
115 .try_into()
116 .expect("generating 1 keypackage returns 1 keypackage");
117 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
118 .await
119 .map_err(MlsError::wrap("encapsulating key package"))?;
120
121 Ok(HistorySecret { client_id, key_package })
124}
125
126pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
127 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
128}
129
130impl CoreCrypto {
131 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
136 if !history_secret
137 .client_id
138 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
139 {
140 return Err(Error::InvalidHistorySecret("client id has invalid format"));
141 }
142
143 let session = in_memory_cc().await?;
144 let tx = session
145 .new_transaction()
146 .await
147 .map_err(RecursiveError::transaction("creating new transaction"))?;
148
149 session
150 .restore_from_history_secret(history_secret)
151 .await
152 .map_err(RecursiveError::mls_client(
153 "restoring ephemeral session from history secret",
154 ))?;
155
156 tx.finish()
157 .await
158 .map_err(RecursiveError::transaction("finishing transaction"))?;
159
160 Ok(session)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use rstest::rstest;
167 use rstest_reuse::apply;
168
169 use crate::test_utils::{TestContext, all_cred_cipher};
170
171 #[apply(all_cred_cipher)]
173 async fn can_create_ephemeral_client(case: TestContext) {
174 let [alice] = case.sessions().await;
175 let conversation = case.create_conversation([&alice]).await;
176 let conversation = conversation.enable_history_sharing_notify().await;
177
178 assert_eq!(
179 conversation.member_count().await,
180 2,
181 "the conversation should now magically have a second member"
182 );
183
184 let ephemeral_client = conversation.members().nth(1).unwrap();
185 assert!(
186 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
187 "alice can send messages to the history client"
188 );
189 assert!(
190 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
191 "the history client cannot send messages"
192 );
193 }
194}