Skip to main content

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 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
35/// We always instantiate history clients with this prefix in their client id, so
36/// we can use prefix testing to determine with some accuracy whether or not something is a history client.
37pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
38
39/// A `HistorySecret` encodes sufficient client state that it can be used to instantiate an
40/// ephemeral client.
41#[derive(serde::Serialize, serde::Deserialize)]
42pub struct HistorySecret {
43    /// Client id of the associated history client
44    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
57/// Generate a new [`HistorySecret`].
58///
59/// This is useful when it's this client's turn to generate a new history client.
60///
61/// The generated secret is cryptographically unrelated to the current CoreCrypto client.
62///
63/// Note that this is a crate-private function; the public interface for this feature is
64/// [`Conversation::generate_history_secret`][crate::mls::conversation::Conversation::generate_history_secret].
65/// This implementation lives here instead of there for organizational reasons.
66pub(crate) async fn generate_history_secret(cipher_suite: CipherSuite) -> Result<HistorySecret> {
67    // generate a new client id
68    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::transaction("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::transaction("initializing ephemeral cc"))?;
83    let session = tx
84        .session()
85        .await
86        .map_err(RecursiveError::transaction("Getting mls session"))?;
87    let credential = Credential::basic(cipher_suite, session_id.clone()).map_err(RecursiveError::mls_credential(
88        "generating basic credential for ephemeral client",
89    ))?;
90    let credential_ref = tx
91        .add_credential(credential)
92        .await
93        .map_err(RecursiveError::transaction(
94            "adding basic credential to ephemeral client",
95        ))?;
96
97    // we can generate a key package from the ephemeral cc and ciphersutite
98    let key_package = tx
99        .generate_key_package(&credential_ref, None)
100        .await
101        .map_err(RecursiveError::transaction("generating keypackage"))?;
102    let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
103        .await
104        .map_err(OpenMlsError::wrap("encapsulating key package"))?;
105
106    // we don't need to finish the transaction here--the point of the ephemeral CC was that no mutations would be saved
107    // there
108    let _ = tx.abort().await;
109
110    Ok(HistorySecret {
111        client_id: session_id,
112        key_package,
113    })
114}
115
116pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
117    client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
118}
119
120impl CoreCrypto {
121    /// Instantiate a history client.
122    ///
123    /// This client exposes the full interface of `CoreCrypto`, but it should only be used to decrypt messages.
124    /// Other use is a logic error.
125    pub async fn history_client(history_secret: HistorySecret) -> Result<Arc<Self>> {
126        if !history_secret
127            .client_id
128            .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
129        {
130            return Err(Error::InvalidHistorySecret("client id has invalid format"));
131        }
132
133        // pass in-memory database
134        let database =
135            Database::open_in_memory().expect("Opening an in-memory database for a history client cannot fail");
136
137        let cc = CoreCrypto::new(database.clone());
138        let tx = cc
139            .new_transaction()
140            .await
141            .map_err(RecursiveError::transaction("creating new transaction"))?;
142
143        // store the client id (with some other stuff)
144        let mls_backend = CryptoProvider::new(database.clone());
145        let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
146        let session = Session::new(
147            history_secret.client_id.clone(),
148            mls_backend,
149            database.into(),
150            transport,
151        );
152
153        session
154            .restore_from_history_secret(history_secret)
155            .await
156            .map_err(RecursiveError::mls_client(
157                "restoring ephemeral session from history secret",
158            ))?;
159
160        tx.set_mls_session(session)
161            .await
162            .map_err(RecursiveError::transaction("Setting mls session"))?;
163
164        tx.finish()
165            .await
166            .map_err(RecursiveError::transaction("finishing transaction"))?;
167
168        Ok(cc)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use rstest::rstest;
175    use rstest_reuse::apply;
176
177    use crate::test_utils::{TestContext, all_cred_cipher};
178
179    /// Create a history secret, and restore it into a CoreCrypto instance
180    #[apply(all_cred_cipher)]
181    async fn can_create_ephemeral_client(case: TestContext) {
182        let [alice] = case.sessions().await;
183        let conversation = case.create_conversation([&alice]).await;
184        let conversation = conversation.enable_history_sharing_notify().await;
185
186        assert_eq!(
187            conversation.member_count().await,
188            2,
189            "the conversation should now magically have a second member"
190        );
191
192        let ephemeral_client = conversation.members().nth(1).unwrap();
193        assert!(
194            conversation.can_one_way_communicate(&alice, ephemeral_client).await,
195            "alice can send messages to the history client"
196        );
197        assert!(
198            !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
199            "the history client cannot send messages"
200        );
201    }
202}