core_crypto/
ephemeral.rs

1//! Utilities for ephemeral CoreCrypto instances.
2//!
3//! Ephemeral instances are intended to support history sharing. History sharing works like this:
4//! every history-enabled conversation has a passive "history client" as a member. This client
5//! is a member of the MLS group (and can therefore decrypt messages), but it is not actively running
6//! on any device or decrypting any messages.
7//!
8//! Approximately once daily, and whenever a member is removed from the group, a new history-sharing era
9//! begins. The client submitting the commit which instantiates the new history-sharing era is responsible
10//! for ensuring that the old history client is removed from the group, and new one is added. Additionally,
11//! one of the first application messages in the new history-sharing era contains the serialized history
12//! secret.
13//!
14//! When a new client joins the history-enabled conversation, they receive a list of history secrets
15//! and their associated history-sharing eras (identified by the epoch number at which they start).
16//! For each history-sharing era, they can instantiate an ephemeral client from the history secret,
17//! and use that client to decrypt all messages in this era.
18//!
19//! Though ephemeral clients are full instances of `CoreCrypto` and contain the same API, they cannot
20//! be used to generate messages for sending, as they don't posess a credential with a signature key:
21//! Any attempt to encrypt a message will fail because the client cannot retrieve the signature key from
22//! its keystore.
23
24use 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
33/// We always instantiate history clients with this prefix in their client id, so
34/// we can use prefix testing to determine with some accuracy whether or not something is a history client.
35pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
36
37/// A `HistorySecret` encodes sufficient client state that it can be used to instantiate an
38/// ephemeral client.
39#[derive(serde::Serialize, serde::Deserialize)]
40pub struct HistorySecret {
41    /// Client id of the associated history client
42    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
55/// Create a new [`CoreCrypto`] with an **uninitialized** mls session.
56///
57/// You must initialize the session yourself before using this!
58async 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)) // don't generate any keypackages; we will never add this client to another group
64        .build()
65        .validate()
66        .map_err(RecursiveError::mls("validating ephemeral session configuration"))?;
67
68    // Construct the MLS session, but don't initialize it. The implementation when `client_id` is `None` just
69    // does construction, which is what we need.
70    let session = Session::try_new(config)
71        .await
72        .map_err(RecursiveError::mls("creating ephemeral session"))?;
73
74    Ok(session.into())
75}
76
77/// Generate a new [`HistorySecret`].
78///
79/// This is useful when it's this client's turn to generate a new history client.
80///
81/// The generated secret is cryptographically unrelated to the current CoreCrypto client.
82///
83/// Note that this is a crate-private function; the public interface for this feature is
84/// [`Conversation::generate_history_secret`][core_crypto::mls::conversation::Conversation::generate_history_secret].
85/// This implementation lives here instead of there for organizational reasons.
86pub(crate) async fn generate_history_secret(ciphersuite: MlsCiphersuite) -> Result<HistorySecret> {
87    // generate a new completely arbitrary client id
88    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    // we can generate a key package from the ephemeral cc and ciphersutite
103    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    // we don't need to finish the transaction here--the point of the ephemeral CC was that no mutations would be saved there
114
115    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    /// Instantiate a history client.
124    ///
125    /// This client exposes the full interface of `CoreCrypto`, but it should only be used to decrypt messages.
126    /// Other use is a logic error.
127    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    /// Create a history secret, and restore it into a CoreCrypto instance
164    #[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}