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 mls_crypto_provider::{DatabaseKey, MlsCryptoProvider};
28use obfuscate::{Obfuscate, Obfuscated};
29use openmls::prelude::KeyPackageSecretEncapsulation;
30
31use crate::{
32    Ciphersuite, ClientId, ClientIdRef, ClientIdentifier, CoreCrypto, CoreCryptoTransportNotImplementedProvider,
33    Credential, Error, MlsError, RecursiveError, Result, Session, mls::session::identities::Identities,
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 client_id = uuid::Uuid::new_v4();
70    let client_id = format!("{HISTORY_CLIENT_ID_PREFIX}-{client_id}");
71    let client_id = ClientId::from(client_id.into_bytes());
72    let identifier = ClientIdentifier::Basic(client_id.clone());
73
74    let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
75        .await
76        .unwrap();
77
78    let cc = CoreCrypto::new(database.clone());
79    let tx = cc
80        .new_transaction()
81        .await
82        .map_err(RecursiveError::transaction("creating new transaction"))?;
83
84    let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
85    tx.mls_init(identifier, &[ciphersuite], transport)
86        .await
87        .map_err(RecursiveError::transaction("initializing ephemeral cc"))?;
88    let session = tx
89        .session()
90        .await
91        .map_err(RecursiveError::transaction("Getting mls session"))?;
92    let credential = Credential::basic(ciphersuite, client_id.clone(), &session.crypto_provider).map_err(
93        RecursiveError::mls_credential("generating basic credential for ephemeral client"),
94    )?;
95    let credential_ref = session
96        .add_credential(credential)
97        .await
98        .map_err(RecursiveError::mls_client(
99            "adding basic credential to ephemeral client",
100        ))?;
101
102    // we can generate a key package from the ephemeral cc and ciphersutite
103    let key_package = tx
104        .generate_keypackage(&credential_ref, None)
105        .await
106        .map_err(RecursiveError::transaction("generating keypackage"))?;
107    let key_package = KeyPackageSecretEncapsulation::load(&session.crypto_provider, key_package)
108        .await
109        .map_err(MlsError::wrap("encapsulating key package"))?;
110
111    // we don't need to finish the transaction here--the point of the ephemeral CC was that no mutations would be saved
112    // there
113    let _ = tx.abort().await;
114
115    Ok(HistorySecret { client_id, key_package })
116}
117
118pub(crate) fn is_history_client(client_id: impl Borrow<ClientIdRef>) -> bool {
119    client_id.borrow().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        // pass in-memory database
136        let database = Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
137            .await
138            .unwrap();
139
140        let cc = CoreCrypto::new(database.clone());
141        let tx = cc
142            .new_transaction()
143            .await
144            .map_err(RecursiveError::transaction("creating new transaction"))?;
145
146        // store the client id (with some other stuff)
147        let mls_backend = MlsCryptoProvider::new(database);
148        let transport = Arc::new(CoreCryptoTransportNotImplementedProvider::default());
149        let session = Session::new(
150            history_secret.client_id.clone(),
151            Identities::new(0),
152            mls_backend,
153            transport,
154        );
155
156        session
157            .restore_from_history_secret(history_secret)
158            .await
159            .map_err(RecursiveError::mls_client(
160                "restoring ephemeral session from history secret",
161            ))?;
162
163        tx.set_mls_session(session)
164            .await
165            .map_err(RecursiveError::transaction("Setting mls session"))?;
166
167        tx.finish()
168            .await
169            .map_err(RecursiveError::transaction("finishing transaction"))?;
170
171        Ok(cc)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use rstest::rstest;
178    use rstest_reuse::apply;
179
180    use crate::test_utils::{TestContext, all_cred_cipher};
181
182    /// Create a history secret, and restore it into a CoreCrypto instance
183    #[apply(all_cred_cipher)]
184    async fn can_create_ephemeral_client(case: TestContext) {
185        let [alice] = case.sessions().await;
186        let conversation = case.create_conversation([&alice]).await;
187        let conversation = conversation.enable_history_sharing_notify().await;
188
189        assert_eq!(
190            conversation.member_count().await,
191            2,
192            "the conversation should now magically have a second member"
193        );
194
195        let ephemeral_client = conversation.members().nth(1).unwrap();
196        assert!(
197            conversation.can_one_way_communicate(&alice, ephemeral_client).await,
198            "alice can send messages to the history client"
199        );
200        assert!(
201            !conversation.can_one_way_communicate(ephemeral_client, &alice).await,
202            "the history client cannot send messages"
203        );
204    }
205}