1use mls_crypto_provider::DatabaseKey;
25use openmls::prelude::KeyPackageSecretEncapsulation;
26
27use crate::{
28 CoreCrypto, Error, MlsError, RecursiveError, Result,
29 prelude::{ClientId, ClientIdentifier, MlsCiphersuite, MlsCredentialType, Session, SessionConfig},
30};
31use obfuscate::{Obfuscate, Obfuscated};
32
33pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
36
37#[derive(serde::Serialize, serde::Deserialize)]
40pub struct HistorySecret {
41 pub client_id: ClientId,
43 pub(crate) key_package: KeyPackageSecretEncapsulation,
44}
45
46impl Obfuscate for HistorySecret {
47 fn obfuscate(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
48 f.debug_struct("HistorySecret")
49 .field("client_id", &self.client_id)
50 .field("key_package", &Obfuscated::from(&self.key_package))
51 .finish()
52 }
53}
54
55async fn in_memory_cc_with_ciphersuite(ciphersuite: impl Into<MlsCiphersuite>) -> Result<CoreCrypto> {
59 let config = SessionConfig::builder()
60 .in_memory()
61 .ciphersuites([ciphersuite.into()])
62 .database_key(DatabaseKey::generate())
63 .nb_key_packages(Some(0)) .build()
65 .validate()
66 .map_err(RecursiveError::mls("validating ephemeral session configuration"))?;
67
68 let session = Session::try_new(config)
71 .await
72 .map_err(RecursiveError::mls("creating ephemeral session"))?;
73
74 Ok(session.into())
75}
76
77pub(crate) async fn generate_history_secret(ciphersuite: MlsCiphersuite) -> Result<HistorySecret> {
87 let client_id = uuid::Uuid::new_v4();
89 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
90 let client_id = ClientId::from(client_id.into_bytes());
91 let identifier = ClientIdentifier::Basic(client_id.clone());
92
93 let cc = in_memory_cc_with_ciphersuite(ciphersuite).await?;
94 let tx = cc
95 .new_transaction()
96 .await
97 .map_err(RecursiveError::transaction("creating new transaction"))?;
98 cc.init(identifier, &[ciphersuite], &cc.crypto_provider, 0)
99 .await
100 .map_err(RecursiveError::mls_client("initializing ephemeral cc"))?;
101
102 let [key_package] = tx
104 .get_or_create_client_keypackages(ciphersuite, MlsCredentialType::Basic, 1)
105 .await
106 .map_err(RecursiveError::transaction("generating keypackages"))?
107 .try_into()
108 .expect("generating 1 keypackage returns 1 keypackage");
109 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
110 .await
111 .map_err(MlsError::wrap("encapsulating key package"))?;
112
113 Ok(HistorySecret { client_id, key_package })
116}
117
118pub(crate) fn is_history_client(client_id: &ClientId) -> bool {
119 client_id.starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
120}
121
122impl CoreCrypto {
123 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
128 if !history_secret
129 .client_id
130 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
131 {
132 return Err(Error::InvalidHistorySecret("client id has invalid format"));
133 }
134
135 let session = in_memory_cc_with_ciphersuite(history_secret.key_package.ciphersuite()).await?;
136 let tx = session
137 .new_transaction()
138 .await
139 .map_err(RecursiveError::transaction("creating new transaction"))?;
140
141 session
142 .restore_from_history_secret(history_secret)
143 .await
144 .map_err(RecursiveError::mls_client(
145 "restoring ephemeral session from history secret",
146 ))?;
147
148 tx.finish()
149 .await
150 .map_err(RecursiveError::transaction("finishing transaction"))?;
151
152 Ok(session)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use rstest::rstest;
159 use rstest_reuse::apply;
160
161 use crate::test_utils::{TestContext, all_cred_cipher};
162
163 #[apply(all_cred_cipher)]
165 async fn can_create_ephemeral_client(case: TestContext) {
166 let [alice] = case.sessions().await;
167 let conversation = case
168 .create_conversation([&alice])
169 .await
170 .enable_history_sharing_notify()
171 .await;
172
173 assert_eq!(
174 conversation.member_count().await,
175 2,
176 "the convesation 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}