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, TntMessageTxCounter},
14    traits::{EntityDatabaseMutation as _, EntityDeleteBorrowed 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(in crate::mls::conversation) 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    ///
50    /// If the counter hasn't been used yet for this conversation, it is loaded from the database or initialized
51    /// freshly.
52    pub(in crate::mls::conversation) async fn obtain_tnt_message_tx_counter(
53        &mut self,
54        database: &impl FetchFromDatabase,
55    ) -> Result<TntMessageCounter> {
56        let mut counter = self.tnt_message_tx_counter;
57
58        if counter.is_zero() {
59            counter = database
60                .get_borrowed::<TntMessageTxCounter>(KeystoreConversationIdRef::new(self.group_id().as_slice()))
61                .await
62                .map_err(KeystoreError::wrap("searching for tnt message counters for group"))?
63                .map(|counter| counter.count)
64                .unwrap_or_default()
65                .into();
66        }
67
68        counter.increment()?;
69        self.tnt_message_tx_counter = counter;
70        self.group.set_state(InnerState::Changed);
71
72        Ok(counter)
73    }
74
75    pub(in crate::mls::conversation) async fn reset_tnt_message_tx_counter(&mut self, tx: &Transaction) -> Result<()> {
76        self.tnt_message_tx_counter = Default::default();
77        let id = KeystoreConversationIdRef::new(self.group.group_id().as_slice());
78        TntMessageTxCounter::delete_borrowed(tx, id)
79            .map_err(KeystoreError::wrap("removing transient message tx counter"))?;
80        Ok(())
81    }
82
83    pub(crate) async fn persist(&mut self, tx: &Transaction) -> Result<()> {
84        // We must change the mls group persisted state before persisting, otherwise it will never reach the DB.
85        self.mls_group_mut().set_state(InnerState::Persisted);
86        let id = self.group.group_id();
87        let group = self.mls_group();
88
89        // While we are an active member the group itself tells us which credential we present. Once
90        // we have been evicted it no longer does: our leaf is gone from the ratchet tree, and our
91        // former slot may even have been recycled by a member added in the same commit, in which
92        // case `own_leaf_index` resolves to *their* leaf and we would link this conversation to
93        // their credential. So rather than derive, reuse what the row already records: the
94        // credential we held while we were a member is the one we used, and being evicted does not
95        // change that. Handling our own eviction wipes the conversation immediately afterwards, so
96        // this value is seldom durable — but `persist` still has to produce a valid one, because
97        // every mutation of a group reaches the keystore through here.
98        let (credential_id, credential_type) = if group.is_active() {
99            let current_credential = group_metadata::current_credential_pk(group, tx).await?;
100            (current_credential.public_key_hash, current_credential.credential_type)
101        } else {
102            let persisted = tx
103                .get_borrowed::<PersistedMlsGroup>(KeystoreConversationIdRef::new(id.as_slice()))
104                .await
105                .map_err(KeystoreError::wrap("finding the existing row of an evicted conversation"))?
106                // We can only have been evicted from a conversation we were a member of, and
107                // joining one always persists it, so the row is always already there: the only
108                // other caller of this function performs the first persist of a group we have just
109                // joined or created, which is necessarily still active.
110                .ok_or(Error::MlsGroupInvalidState(
111                    "an evicted conversation must already have been persisted",
112                ))?;
113            (persisted.credential_id, persisted.credential_type)
114        };
115
116        PersistedMlsGroup {
117            id: id.as_slice().into(),
118            state: core_crypto_keystore::ser(group).map_err(KeystoreError::wrap("serializing group state"))?,
119            epoch: group.epoch().as_u64(),
120            ciphersuite: group.ciphersuite() as u16,
121            credential_id,
122            credential_type,
123            own_leaf_index: group.own_leaf_index().u32(),
124            // `persist` is reached either by an established conversation persisting a normal
125            // change, or by `persist_conversation_from_mls_group` once an external commit has
126            // already been merged — either way, this group is no longer pending by the time this
127            // runs.
128            is_pending: false,
129        }
130        .save(tx)
131        .map_err(KeystoreError::wrap("persisting mls group"))?;
132
133        TntMessageTxCounter {
134            conversation_id: id.as_slice().into(),
135            count: self.tnt_message_tx_counter.into(),
136        }
137        .save(tx)
138        .map_err(KeystoreError::wrap("saving transient message tx counter"))?;
139
140        Ok(())
141    }
142}
143
144/// A Conversation exposes the read-only interface of an MLS conversation.
145#[derive(Debug, derive_more::Constructor)]
146pub struct Conversation {
147    pub(in crate::mls::conversation) id: ConversationId,
148    pub(in crate::mls::conversation) group: RwLock<MlsGroupState>,
149    pub(in crate::mls::conversation) configuration: ConversationConfiguration,
150    session: Session,
151}
152
153impl Conversation {
154    /// Returns the conversation's ID
155    pub fn id(&self) -> &ConversationIdRef {
156        self.id.as_ref()
157    }
158
159    /// Returns an immutable guard over the underlying MLS group
160    pub(crate) async fn group(&self) -> RwLockReadGuard<'_, MlsGroupState> {
161        self.group.read().await
162    }
163
164    /// Returns the conversation's configuration
165    pub fn configuration(&self) -> &ConversationConfiguration {
166        &self.configuration
167    }
168
169    /// Returns current epoch of the MLS group
170    pub async fn epoch(&self) -> u64 {
171        self.group().await.epoch().as_u64()
172    }
173
174    /// Returns this conversation's cipher suite
175    pub fn cipher_suite(&self) -> CipherSuite {
176        self.configuration.cipher_suite
177    }
178
179    /// Returns a reference to the credential used in this conversation
180    pub async fn credential_ref(&self) -> Result<CredentialRef> {
181        let credential = self
182            .find_current_credential()
183            .await
184            .map_err(|_| Error::IdentityInitializationError)?;
185        Ok(CredentialRef::from_credential(&credential))
186    }
187
188    /// Derives a new key from the one in the group, to be used elsewhere.
189    ///
190    /// # Arguments
191    /// * `key_length` - the length of the key to be derived. If the value is higher than the bounds of `u16` or the
192    ///   context hash * 255, an error will be returned
193    ///
194    /// # Errors
195    /// OpenMls secret generation error
196    pub async fn export_secret_key(&self, key_length: usize) -> Result<SecretKey> {
197        const EXPORTER_LABEL: &str = "exporter";
198        const EXPORTER_CONTEXT: &[u8] = &[];
199        self.group()
200            .await
201            .export_secret(
202                &self.session.crypto_provider,
203                EXPORTER_LABEL,
204                EXPORTER_CONTEXT,
205                key_length,
206            )
207            .map(Into::into)
208            .map_err(OpenMlsError::wrap("exporting secret key"))
209            .map_err(Into::into)
210    }
211
212    /// Returns the first external sender present in this group.
213    ///
214    /// This should be used to initialize a subconversation
215    pub async fn get_external_sender(&self) -> Result<ExternalSender> {
216        let group = self.group().await;
217        let ext_senders = group
218            .group_context_extensions()
219            .external_senders()
220            .ok_or(Error::MissingExternalSenderExtension)?;
221        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
222        Ok(ext_sender.clone().into())
223    }
224}
225
226#[cfg(test)]
227mod test_utils {
228    use openmls::prelude::SignaturePublicKey;
229
230    use super::*;
231
232    impl Conversation {
233        pub async fn signature_keys(&self) -> Vec<SignaturePublicKey> {
234            let group = self.group().await;
235            group
236                .members()
237                .map(|m| m.signature_key)
238                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
239                .collect()
240        }
241
242        pub async fn encryption_keys(&self) -> Vec<Vec<u8>> {
243            let group = self.group().await;
244            group.members().map(|m| m.encryption_key).collect()
245        }
246
247        pub async fn extensions(&self) -> openmls::prelude::Extensions {
248            let group = self.group().await;
249            group.export_group_context().extensions().to_owned()
250        }
251    }
252}