Skip to main content

core_crypto/mls/conversation/immutable/
mod.rs

1mod clients;
2mod commit_delay;
3mod credential;
4mod duplicate;
5mod e2ei;
6mod history_sharing;
7mod persistence;
8
9use async_lock::{RwLock, RwLockReadGuard};
10use core_crypto_keystore::{
11    Transaction,
12    ancillary::ConversationIdRef as KeystoreConversationIdRef,
13    entities::PersistedMlsGroup,
14    traits::{EntityDatabaseMutation as _, FetchFromDatabase},
15};
16use openmls::group::{InnerState, MlsGroup};
17
18use super::{ConversationIdRef, Error, Result, SecretKey, group_metadata};
19use crate::{
20    CipherSuite, ConversationConfiguration, ConversationId, CredentialRef, ExternalSender, KeystoreError, OpenMlsError,
21    Session, mls::TntMessageCounter,
22};
23
24#[derive(derive_more::Constructor, derive_more::Deref, derive_more::DerefMut, derive_more::Debug)]
25pub(crate) struct MlsGroupState {
26    #[deref]
27    #[deref_mut]
28    group: MlsGroup,
29    /// The count of transient messages plus targeted messages sent (hereafter `tx`) this epoch.
30    /// Supposed to be used when encrypting tnt messages only, and to be reset whenever the mls epoch is
31    /// incremented. Do not access this field directly, use [MlsGroupState::obtain_tnt_message_tx_counter] and
32    /// [MlsGroupState::reset_tnt_message_tx_counter] only.
33    ///
34    /// The purpose of these counters is replay protection on the recipient side: we provide the count when sending a
35    /// message to a receiver, and they check if the counter is greater than any they've seen before.
36    tnt_message_tx_counter: TntMessageCounter,
37}
38
39impl MlsGroupState {
40    pub(crate) fn mls_group(&self) -> &MlsGroup {
41        &self.group
42    }
43
44    pub(in crate::mls::conversation) fn mls_group_mut(&mut self) -> &mut MlsGroup {
45        &mut self.group
46    }
47
48    /// Get the transient message sender (tx) counter bound to this conversation after incrementing it.
49    pub(in crate::mls::conversation) fn obtain_tnt_message_tx_counter(&mut self) -> Result<TntMessageCounter> {
50        self.tnt_message_tx_counter.increment()?;
51        self.group.set_state(InnerState::Changed);
52
53        Ok(self.tnt_message_tx_counter)
54    }
55
56    pub(in crate::mls::conversation) fn reset_tnt_message_tx_counter(&mut self) {
57        self.tnt_message_tx_counter = Default::default();
58        self.group.set_state(InnerState::Changed);
59    }
60
61    pub(crate) async fn persist(&mut self, tx: &Transaction) -> Result<()> {
62        // We must change the mls group persisted state before persisting, otherwise it will never reach the DB.
63        self.mls_group_mut().set_state(InnerState::Persisted);
64        let id = self.group.group_id();
65        let group = self.mls_group();
66
67        // While we are an active member the group itself tells us which credential we present. Once
68        // we have been evicted it no longer does: our leaf is gone from the ratchet tree, and our
69        // former slot may even have been recycled by a member added in the same commit, in which
70        // case `own_leaf_index` resolves to *their* leaf and we would link this conversation to
71        // their credential. So rather than derive, reuse what the row already records: the
72        // credential we held while we were a member is the one we used, and being evicted does not
73        // change that. Handling our own eviction wipes the conversation immediately afterwards, so
74        // this value is seldom durable — but `persist` still has to produce a valid one, because
75        // every mutation of a group reaches the keystore through here.
76        let (credential_id, credential_type) = if group.is_active() {
77            let current_credential = group_metadata::current_credential_pk(group, tx).await?;
78            (current_credential.public_key_hash, current_credential.credential_type)
79        } else {
80            let persisted = tx
81                .get_borrowed::<PersistedMlsGroup>(KeystoreConversationIdRef::new(id.as_slice()))
82                .await
83                .map_err(KeystoreError::wrap(
84                    "finding the existing row of an evicted conversation",
85                ))?
86                // We can only have been evicted from a conversation we were a member of, and
87                // joining one always persists it, so the row is always already there: the only
88                // other caller of this function performs the first persist of a group we have just
89                // joined or created, which is necessarily still active.
90                .ok_or(Error::MlsGroupInvalidState(
91                    "an evicted conversation must already have been persisted",
92                ))?;
93            (persisted.credential_id, persisted.credential_type)
94        };
95
96        PersistedMlsGroup {
97            id: id.as_slice().into(),
98            state: core_crypto_keystore::ser(group).map_err(KeystoreError::wrap("serializing group state"))?,
99            epoch: group.epoch().as_u64(),
100            ciphersuite: group.ciphersuite() as u16,
101            credential_id,
102            credential_type,
103            own_leaf_index: group.own_leaf_index().u32(),
104            // `persist` is reached either by an established conversation persisting a normal
105            // change, or by `persist_conversation_from_mls_group` once an external commit has
106            // already been merged — either way, this group is no longer pending by the time this
107            // runs.
108            is_pending: false,
109            tnt_tx_counter: self.tnt_message_tx_counter.into(),
110        }
111        .save(tx)
112        .map_err(KeystoreError::wrap("persisting mls group"))?;
113
114        Ok(())
115    }
116}
117
118/// A Conversation exposes the read-only interface of an MLS conversation.
119#[derive(Debug, derive_more::Constructor)]
120pub struct Conversation {
121    pub(in crate::mls::conversation) id: ConversationId,
122    pub(in crate::mls::conversation) group: RwLock<MlsGroupState>,
123    pub(in crate::mls::conversation) configuration: ConversationConfiguration,
124    pub(in crate::mls::conversation) session: Session,
125}
126
127impl Conversation {
128    /// Returns the conversation's ID
129    pub fn id(&self) -> &ConversationIdRef {
130        self.id.as_ref()
131    }
132
133    /// Returns an immutable guard over the underlying MLS group
134    pub(crate) async fn group(&self) -> RwLockReadGuard<'_, MlsGroupState> {
135        self.group.read().await
136    }
137
138    /// Returns the conversation's configuration
139    pub fn configuration(&self) -> &ConversationConfiguration {
140        &self.configuration
141    }
142
143    /// Returns current epoch of the MLS group
144    pub async fn epoch(&self) -> u64 {
145        self.group().await.epoch().as_u64()
146    }
147
148    /// Returns this conversation's cipher suite
149    pub fn cipher_suite(&self) -> CipherSuite {
150        self.configuration.cipher_suite
151    }
152
153    /// Returns a reference to the credential used in this conversation
154    pub async fn credential_ref(&self) -> Result<CredentialRef> {
155        let credential = self
156            .find_current_credential()
157            .await
158            .map_err(|_| Error::IdentityInitializationError)?;
159        Ok(CredentialRef::from_credential(&credential))
160    }
161
162    /// Derives a new key from the one in the group, to be used elsewhere.
163    ///
164    /// # Arguments
165    /// * `key_length` - the length of the key to be derived. If the value is higher than the bounds of `u16` or the
166    ///   context hash * 255, an error will be returned
167    ///
168    /// # Errors
169    /// OpenMls secret generation error
170    pub async fn export_secret_key(&self, key_length: usize) -> Result<SecretKey> {
171        const EXPORTER_LABEL: &str = "exporter";
172        const EXPORTER_CONTEXT: &[u8] = &[];
173        self.group()
174            .await
175            .export_secret(
176                &self.session.crypto_provider,
177                EXPORTER_LABEL,
178                EXPORTER_CONTEXT,
179                key_length,
180            )
181            .map(Into::into)
182            .map_err(OpenMlsError::wrap("exporting secret key"))
183            .map_err(Into::into)
184    }
185
186    /// Returns the first external sender present in this group.
187    ///
188    /// This should be used to initialize a subconversation
189    pub async fn get_external_sender(&self) -> Result<ExternalSender> {
190        let group = self.group().await;
191        let ext_senders = group
192            .group_context_extensions()
193            .external_senders()
194            .ok_or(Error::MissingExternalSenderExtension)?;
195        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
196        Ok(ext_sender.clone().into())
197    }
198}
199
200#[cfg(test)]
201mod test_utils {
202    use openmls::prelude::SignaturePublicKey;
203
204    use super::*;
205
206    impl Conversation {
207        pub async fn signature_keys(&self) -> Vec<SignaturePublicKey> {
208            let group = self.group().await;
209            group
210                .members()
211                .map(|m| m.signature_key)
212                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
213                .collect()
214        }
215
216        pub async fn encryption_keys(&self) -> Vec<Vec<u8>> {
217            let group = self.group().await;
218            group.members().map(|m| m.encryption_key).collect()
219        }
220
221        pub async fn extensions(&self) -> openmls::prelude::Extensions {
222            let group = self.group().await;
223            group.export_group_context().extensions().to_owned()
224        }
225    }
226}