1use std::{borrow::Borrow, sync::Arc};
25
26use core_crypto_keystore::{ConnectionType, 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,
33 mls_provider::{CryptoProvider, DatabaseKey},
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
58pub(crate) async fn generate_history_secret(cipher_suite: CipherSuite) -> Result<HistorySecret> {
68 let session_id = ClientId::new_ephemeral();
70
71 let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
72 .await
73 .unwrap();
74
75 let cc = CoreCrypto::new(database.clone());
76 let tx = cc
77 .new_transaction()
78 .await
79 .map_err(RecursiveError::transaction("creating new transaction"))?;
80
81 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
82 tx.mls_init(session_id.clone(), transport)
83 .await
84 .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
85 let session = tx
86 .session()
87 .await
88 .map_err(RecursiveError::transaction("Getting mls session"))?;
89 let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::mls_credential(
90 "generating basic credential for ephemeral client",
91 ))?;
92 let credential_ref = tx
93 .add_credential(credential)
94 .await
95 .map_err(RecursiveError::transaction(
96 "adding basic credential to ephemeral client",
97 ))?;
98
99 let key_package = tx
101 .generate_key_package(&credential_ref, None)
102 .await
103 .map_err(RecursiveError::transaction("generating keypackage"))?;
104 let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
105 .await
106 .map_err(OpenMlsError::wrap("encapsulating key package"))?;
107
108 let _ = tx.abort().await;
111
112 Ok(HistorySecret {
113 client_id: session_id,
114 key_package,
115 })
116}
117
118pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
119 client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
120}
121
122impl CoreCrypto {
123 pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<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 database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
137 .await
138 .unwrap();
139
140 let cc = CoreCrypto::new(database.clone());
141 let tx = cc
142 .new_transaction()
143 .await
144 .map_err(RecursiveError::transaction("creating new transaction"))?;
145
146 let mls_backend = CryptoProvider::new(database.clone());
148 let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
149 let session = Session::new(
150 history_secret.client_id.clone(),
151 mls_backend,
152 database.into(),
153 transport,
154 );
155
156 session
157 .restore_from_history_secret(history_secret)
158 .await
159 .map_err(RecursiveError::mls_client(
160 "restoring ephemeral session from history secret",
161 ))?;
162
163 tx.set_mls_session(session)
164 .await
165 .map_err(RecursiveError::transaction("Setting mls session"))?;
166
167 tx.finish()
168 .await
169 .map_err(RecursiveError::transaction("finishing transaction"))?;
170
171 Ok(cc)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use rstest::rstest;
178 use rstest_reuse::apply;
179
180 use crate::test_utils::{TestContext, all_cred_cipher};
181
182 #[apply(all_cred_cipher)]
184 async fn can_create_ephemeral_client(case: TestContext) {
185 let [alice] = case.sessions().await;
186 let conversation = case.create_conversation([&alice]).await;
187 let conversation = conversation.enable_history_sharing_notify().await;
188
189 assert_eq!(
190 conversation.member_count().await,
191 2,
192 "the conversation should now magically have a second member"
193 );
194
195 let ephemeral_client = conversation.members().nth(1).unwrap();
196 assert!(
197 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
198 "alice can send messages to the history client"
199 );
200 assert!(
201 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
202 "the history client cannot send messages"
203 );
204 }
205}