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};
31
32pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
35
36#[derive(serde::Serialize, serde::Deserialize)]
39pub struct HistorySecret {
40 pub client_id: ClientId,
42 pub(crate) key_package: KeyPackageSecretEncapsulation,
43}
44
45async fn in_memory_cc_with_ciphersuite(ciphersuite: impl Into<MlsCiphersuite>) -> Result<CoreCrypto> {
49 let config = SessionConfig::builder()
50 .in_memory()
51 .ciphersuites([ciphersuite.into()])
52 .database_key(DatabaseKey::generate())
53 .nb_key_packages(Some(0)) .build()
55 .validate()
56 .map_err(RecursiveError::mls("validating ephemeral session configuration"))?;
57
58 let session = Session::try_new(config)
61 .await
62 .map_err(RecursiveError::mls("creating ephemeral session"))?;
63
64 Ok(session.into())
65}
66
67pub(crate) async fn generate_history_secret(ciphersuite: MlsCiphersuite) -> Result<HistorySecret> {
77 let client_id = uuid::Uuid::new_v4();
79 let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
80 let client_id = ClientId::from(client_id.into_bytes());
81 let identifier = ClientIdentifier::Basic(client_id.clone());
82
83 let cc = in_memory_cc_with_ciphersuite(ciphersuite).await?;
84 let tx = cc
85 .new_transaction()
86 .await
87 .map_err(RecursiveError::transaction("creating new transaction"))?;
88 cc.init(identifier, &[ciphersuite], &cc.crypto_provider, 0)
89 .await
90 .map_err(RecursiveError::mls_client("initializing ephemeral cc"))?;
91
92 let [key_package] = tx
94 .get_or_create_client_keypackages(ciphersuite, MlsCredentialType::Basic, 1)
95 .await
96 .map_err(RecursiveError::transaction("generating keypackages"))?
97 .try_into()
98 .expect("generating 1 keypackage returns 1 keypackage");
99 let key_package = KeyPackageSecretEncapsulation::load(&cc.crypto_provider, key_package)
100 .await
101 .map_err(MlsError::wrap("encapsulating key package"))?;
102
103 Ok(HistorySecret { client_id, key_package })
106}
107
108pub(crate) fn is_history_client(client_id: &ClientId) -> bool {
109 client_id.starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
110}
111
112impl CoreCrypto {
113 pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
118 if !history_secret
119 .client_id
120 .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
121 {
122 return Err(Error::InvalidHistorySecret("client id has invalid format"));
123 }
124
125 let session = in_memory_cc_with_ciphersuite(history_secret.key_package.ciphersuite()).await?;
126 let tx = session
127 .new_transaction()
128 .await
129 .map_err(RecursiveError::transaction("creating new transaction"))?;
130
131 session
132 .restore_from_history_secret(history_secret)
133 .await
134 .map_err(RecursiveError::mls_client(
135 "restoring ephemeral session from history secret",
136 ))?;
137
138 tx.finish()
139 .await
140 .map_err(RecursiveError::transaction("finishing transaction"))?;
141
142 Ok(session)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use rstest::rstest;
149 use rstest_reuse::apply;
150
151 use crate::test_utils::{TestContext, all_cred_cipher};
152
153 #[apply(all_cred_cipher)]
155 async fn can_create_ephemeral_client(case: TestContext) {
156 let [alice] = case.sessions().await;
157 let conversation = case
158 .create_conversation([&alice])
159 .await
160 .enable_history_sharing_notify()
161 .await;
162
163 assert_eq!(
164 conversation.member_count().await,
165 2,
166 "the convesation should now magically have a second member"
167 );
168
169 let ephemeral_client = conversation.members().nth(1).unwrap();
170 assert!(
171 conversation.can_one_way_communicate(&alice, ephemeral_client).await,
172 "alice can send messages to the history client"
173 );
174 assert!(
175 !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
176 "the history client cannot send messages"
177 );
178 }
179}