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 })
125}
126
127pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
128 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
129}
130
131impl CoreCrypto {
132 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
137 if !history_secret
138 .client_id
139 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
140 {
141 return Err(Error::InvalidHistorySecret("client id has invalid format"));
142 }
143
144 let session = in_memory_cc().await?;
145 let tx = session
146 .new_transaction()
147 .await
148 .map_err(RecursiveError::transaction("creating new transaction"))?;
149
150 session
151 .restore_from_history_secret(history_secret)
152 .await
153 .map_err(RecursiveError::mls_client(
154 "restoring ephemeral session from history secret",
155 ))?;
156
157 tx.finish()
158 .await
159 .map_err(RecursiveError::transaction("finishing transaction"))?;
160
161 Ok(session)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use rstest::rstest;
168 use rstest_reuse::apply;
169
170 use crate::test_utils::{TestContext, all_cred_cipher};
171
172 #[apply(all_cred_cipher)]
174 async fn can_create_ephemeral_client(case: TestContext) {
175 let [alice] = case.sessions().await;
176 let conversation = case.create_conversation([&alice]).await;
177 let conversation = conversation.enable_history_sharing_notify().await;
178
179 assert_eq!(
180 conversation.member_count().await,
181 2,
182 "the conversation should now magically have a second member"
183 );
184
185 let ephemeral_client = conversation.members().nth(1).unwrap();
186 assert!(
187 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
188 "alice can send messages to the history client"
189 );
190 assert!(
191 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
192 "the history client cannot send messages"
193 );
194 }
195}