1use core_crypto_keystore::{ConnectionType, Database};
25use mls_crypto_provider::DatabaseKey;
26use obfuscate::{Obfuscate, Obfuscated};
27use openmls::prelude::KeyPackageSecretEncapsulation;
28
29use crate::{
30 ClientId, ClientIdentifier, CoreCrypto, Error, MlsCiphersuite, MlsCredentialType, MlsError, RecursiveError, Result,
31 Session, SessionConfig,
32};
33
34pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
37
38#[derive(serde::Serialize, serde::Deserialize)]
41pub struct HistorySecret {
42 pub client_id: ClientId,
44 pub(crate) key_package: KeyPackageSecretEncapsulation,
45}
46
47impl Obfuscate for HistorySecret {
48 fn obfuscate(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
49 f.debug_struct("HistorySecret")
50 .field("client_id", &self.client_id)
51 .field("key_package", &Obfuscated::from(&self.key_package))
52 .finish()
53 }
54}
55
56async fn in_memory_cc_with_ciphersuite(ciphersuite: impl Into<MlsCiphersuite>) -> Result<CoreCrypto> {
60 let db = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
61 .await
62 .unwrap();
63 let config = SessionConfig::builder()
64 .ciphersuites([ciphersuite.into()])
65 .database(db)
66 .build()
67 .validate()
68 .map_err(RecursiveError::mls("validating ephemeral session configuration"))?;
69
70 let session = Session::try_new(config)
73 .await
74 .map_err(RecursiveError::mls("creating ephemeral session"))?;
75
76 Ok(session.into())
77}
78
79pub(crate) async fn generate_history_secret(ciphersuite: MlsCiphersuite) -> Result<HistorySecret> {
89 let client_id = uuid::Uuid::new_v4();
91 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
92 let client_id = ClientId::from(client_id.into_bytes());
93 let identifier = ClientIdentifier::Basic(client_id.clone());
94
95 let cc = in_memory_cc_with_ciphersuite(ciphersuite).await?;
96 let tx = cc
97 .new_transaction()
98 .await
99 .map_err(RecursiveError::transaction("creating new transaction"))?;
100 cc.init(identifier, &[ciphersuite], &cc.crypto_provider)
101 .await
102 .map_err(RecursiveError::mls_client("initializing ephemeral cc"))?;
103
104 let [key_package] = tx
106 .get_or_create_client_keypackages(ciphersuite, MlsCredentialType::Basic, 1)
107 .await
108 .map_err(RecursiveError::transaction("generating keypackages"))?
109 .try_into()
110 .expect("generating 1 keypackage returns 1 keypackage");
111 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
112 .await
113 .map_err(MlsError::wrap("encapsulating key package"))?;
114
115 Ok(HistorySecret { client_id, key_package })
118}
119
120pub(crate) fn is_history_client(client_id: &ClientId) -> bool {
121 client_id.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 session = in_memory_cc_with_ciphersuite(history_secret.key_package.ciphersuite()).await?;
138 let tx = session
139 .new_transaction()
140 .await
141 .map_err(RecursiveError::transaction("creating new transaction"))?;
142
143 session
144 .restore_from_history_secret(history_secret)
145 .await
146 .map_err(RecursiveError::mls_client(
147 "restoring ephemeral session from history secret",
148 ))?;
149
150 tx.finish()
151 .await
152 .map_err(RecursiveError::transaction("finishing transaction"))?;
153
154 Ok(session)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161 use rstest_reuse::apply;
162
163 use crate::test_utils::{TestContext, all_cred_cipher};
164
165 #[apply(all_cred_cipher)]
167 async fn can_create_ephemeral_client(case: TestContext) {
168 let [alice] = case.sessions().await;
169 let conversation = case
170 .create_conversation([&alice])
171 .await
172 .enable_history_sharing_notify()
173 .await;
174
175 assert_eq!(
176 conversation.member_count().await,
177 2,
178 "the convesation should now magically have a second member"
179 );
180
181 let ephemeral_client = conversation.members().nth(1).unwrap();
182 assert!(
183 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
184 "alice can send messages to the history client"
185 );
186 assert!(
187 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
188 "the history client cannot send messages"
189 );
190 }
191}