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::{ConnectionType, Database};
27use obfuscate::{Obfuscate, Obfuscated};
28use openmls::prelude::KeyPackageSecretEncapsulation;
29
30use crate::{
31    Ciphersuite, ClientId, ClientIdRef, CoreCrypto, CoreCryptoTransportNotImplementedProvider, Credential, Error,
32    MlsError, RecursiveError, Result, Session,
33    mls_provider::{DatabaseKey, MlsCryptoProvider},
34};
35
36/// We always instantiate history clients with this prefix in their client id, so
37/// we can use prefix testing to determine with some accuracy whether or not something is a history client.
38pub const HISTORY_CLIENT_ID_PREFIX: &str = "history-client";
39
40/// A `HistorySecret` encodes sufficient client state that it can be used to instantiate an
41/// ephemeral client.
42#[derive(serde::Serialize, serde::Deserialize)]
43pub struct HistorySecret {
44    /// Client id of the associated history client
45    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
58/// Generate a new [`HistorySecret`].
59///
60/// This is useful when it's this client's turn to generate a new history client.
61///
62/// The generated secret is cryptographically unrelated to the current CoreCrypto client.
63///
64/// Note that this is a crate-private function; the public interface for this feature is
65/// [`Conversation::generate_history_secret`][crate::mls::conversation::Conversation::generate_history_secret].
66/// This implementation lives here instead of there for organizational reasons.
67pub(crate) async fn generate_history_secret(ciphersuite: Ciphersuite) -> Result<HistorySecret> {
68    // generate a new completely arbitrary client id
69    let session_id = uuid::Uuid::new_v4();
70    let session_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{session_id}");
71    let session_id = ClientId::from(session_id.into_bytes());
72
73    let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
74        .await
75        .unwrap();
76
77    let cc = CoreCrypto::new(database.clone());
78    let tx = cc
79        .new_transaction()
80        .await
81        .map_err(RecursiveError::transaction("creating new transaction"))?;
82
83    let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
84    tx.mls_init(session_id.clone(), transport)
85        .await
86        .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
87    let session = tx
88        .session()
89        .await
90        .map_err(RecursiveError::transaction("Getting mls session"))?;
91    let credential = Credential::basic(ciphersuite, session_id.clone()).map_err(RecursiveError::mls_credential(
92        "generating basic credential for ephemeral client",
93    ))?;
94    let credential_ref = tx
95        .add_credential(credential)
96        .await
97        .map_err(RecursiveError::transaction(
98            "adding basic credential to ephemeral client",
99        ))?;
100
101    // we can generate a key package from the ephemeral cc and ciphersutite
102    let key_package = tx
103        .generate_keypackage(&credential_ref, None)
104        .await
105        .map_err(RecursiveError::transaction("generating keypackage"))?;
106    let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
107        .await
108        .map_err(MlsError::wrap("encapsulating key package"))?;
109
110    // we don't need to finish the transaction here--the point of the ephemeral CC was that no mutations would be saved
111    // there
112    let _ = tx.abort().await;
113
114    Ok(HistorySecret {
115        client_id: session_id,
116        key_package,
117    })
118}
119
120pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
121    client_id.borrow().starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
122}
123
124impl CoreCrypto {
125    /// Instantiate a history client.
126    ///
127    /// This client exposes the full interface of `CoreCrypto`, but it should only be used to decrypt messages.
128    /// Other use is a logic error.
129    pub async fn history_client(history_secret: HistorySecret) -> Result<Self> {
130        if !history_secret
131            .client_id
132            .starts_with(HISTORY_CLIENT_ID_PREFIX.as_bytes())
133        {
134            return Err(Error::InvalidHistorySecret("client id has invalid format"));
135        }
136
137        // pass in-memory database
138        let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
139            .await
140            .unwrap();
141
142        let cc = CoreCrypto::new(database.clone());
143        let tx = cc
144            .new_transaction()
145            .await
146            .map_err(RecursiveError::transaction("creating new transaction"))?;
147
148        // store the client id (with some other stuff)
149        let mls_backend = MlsCryptoProvider::new(database.clone());
150        let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
151        let session = Session::new(history_secret.client_id.clone(), mls_backend, database, transport);
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}