1use std::{borrow::Borrow, sync::Arc};
25
26use core_crypto_keystore::Database;
27use obfuscate::{Obfuscate, Obfuscated};
28use openmls::prelude::KeyPackageSecretEncapsulation;
29
30use crate::{
31 CipherSuite, ClientId, ClientIdRef, CoreCrypto, CoreCryptoTransportNotImplementedProvider, Credential, Error,
32 OpenMlsError, RecursiveError, Result, Session, mls_provider::CryptoProvider,
33};
34
35pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
38
39#[derive(serde::Serialize, serde::Deserialize)]
42pub struct HistorySecret {
43 pub client_id: ClientId,
45 pub(crate) key_package: KeyPackageSecretEncapsulation,
46}
47
48impl Obfuscate for HistorySecret {
49 fn obfuscate(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
50 f.debug_struct("HistorySecret")
51 .field("client_id", &self.client_id)
52 .field("key_package", &Obfuscated::from(&self.key_package))
53 .finish()
54 }
55}
56
57pub(crate) async fn generate_history_secret(cipher_suite: CipherSuite) -> Result<HistorySecret> {
67 let session_id = ClientId::new_ephemeral();
69
70 let database =
71 Database::open_in_memory().expect("Opening an in-memory database to generate a history secret cannot fail");
72
73 let cc = CoreCrypto::new(database.clone());
74 let tx = cc
75 .new_transaction()
76 .await
77 .map_err(RecursiveError::context("creating new transaction"))?;
78
79 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
80 tx.mls_init(session_id.clone(), transport)
81 .await
82 .map_err(RecursiveError::context("initializing ephemeral cc"))?;
83 let session = tx
84 .session()
85 .await
86 .map_err(RecursiveError::context("Getting mls session"))?;
87 let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::context(
88 "generating basic credential for ephemeral client",
89 ))?;
90 let credential_ref = tx
91 .add_credential(credential)
92 .await
93 .map_err(RecursiveError::context("adding basic credential to ephemeral client"))?;
94
95 let key_package = tx
97 .generate_key_package(&credential_ref, None)
98 .await
99 .map_err(RecursiveError::context("generating keypackage"))?;
100 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
101 .await
102 .map_err(OpenMlsError::wrap("encapsulating key package"))?;
103
104 let _ = tx.abort().await;
107
108 Ok(HistorySecret {
109 client_id: session_id,
110 key_package,
111 })
112}
113
114pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
115 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
116}
117
118impl CoreCrypto {
119 pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<Self>> {
124 if !history_secret
125 .client_id
126 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
127 {
128 return Err(Error::InvalidHistorySecret("client id has invalid format"));
129 }
130
131 let database =
133 Database::open_in_memory().expect("Opening an in-memory database for a history client cannot fail");
134
135 let cc = CoreCrypto::new(database.clone());
136 let tx = cc
137 .new_transaction()
138 .await
139 .map_err(RecursiveError::context("creating new transaction"))?;
140
141 let mls_backend = CryptoProvider::new(database.clone());
143 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
144 let session = Session::new(
145 history_secret.client_id.clone(),
146 mls_backend,
147 database.into(),
148 transport,
149 );
150
151 session
152 .restore_from_history_secret(history_secret)
153 .await
154 .map_err(RecursiveError::context(
155 "restoring ephemeral session from history secret",
156 ))?;
157
158 tx.set_mls_session(session)
159 .await
160 .map_err(RecursiveError::context("Setting mls session"))?;
161
162 tx.finish()
163 .await
164 .map_err(RecursiveError::context("finishing transaction"))?;
165
166 Ok(cc)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use rstest::rstest;
173 use rstest_reuse::apply;
174
175 use crate::test_utils::{TestContext, all_cred_cipher};
176
177 #[apply(all_cred_cipher)]
179 async fn can_create_ephemeral_client(case: TestContext) {
180 let [alice] = case.sessions().await;
181 let conversation = case.create_conversation([&alice]).await;
182 let conversation = conversation.enable_history_sharing_notify().await;
183
184 assert_eq!(
185 conversation.member_count().await,
186 2,
187 "the conversation should now magically have a second member"
188 );
189
190 let ephemeral_client = conversation.members().nth(1).unwrap();
191 assert!(
192 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
193 "alice can send messages to the history client"
194 );
195 assert!(
196 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
197 "the history client cannot send messages"
198 );
199 }
200}