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::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    // we can generate a key package from the ephemeral cc and ciphersutite
96    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    // we don't need to finish the transaction here--the point of the ephemeral CC was that no mutations would be saved
105    // there
106    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    /// Instantiate a history client.
120    ///
121    /// This client exposes the full interface of `CoreCrypto`, but it should only be used to decrypt messages.
122    /// Other use is a logic error.
123    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        // pass in-memory database
132        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        // store the client id (with some other stuff)
142        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    /// Create a history secret, and restore it into a CoreCrypto instance
178    #[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}