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(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    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("finding the existing row of an evicted conversation"))?
84                // We can only have been evicted from a conversation we were a member of, and
85                // joining one always persists it, so the row is always already there: the only
86                // other caller of this function performs the first persist of a group we have just
87                // joined or created, which is necessarily still active.
88                .ok_or(Error::MlsGroupInvalidState(
89                    "an evicted conversation must already have been persisted",
90                ))?;
91            (persisted.credential_id, persisted.credential_type)
92        };
93
94        PersistedMlsGroup {
95            id: id.as_slice().into(),
96            state: core_crypto_keystore::ser(group).map_err(KeystoreError::wrap("serializing group state"))?,
97            epoch: group.epoch().as_u64(),
98            ciphersuite: group.ciphersuite() as u16,
99            credential_id,
100            credential_type,
101            own_leaf_index: group.own_leaf_index().u32(),
102            // `persist` is reached either by an established conversation persisting a normal
103            // change, or by `persist_conversation_from_mls_group` once an external commit has
104            // already been merged — either way, this group is no longer pending by the time this
105            // runs.
106            is_pending: false,
107            tnt_tx_counter: self.tnt_message_tx_counter.into(),
108        }
109        .save(tx)
110        .map_err(KeystoreError::wrap("persisting mls group"))?;
111
112        Ok(())
113    }
114}
115
116/// A Conversation exposes the read-only interface of an MLS conversation.
117#[derive(Debug, derive_more::Constructor)]
118pub struct Conversation {
119    pub(in crate::mls::conversation) id: ConversationId,
120    pub(in crate::mls::conversation) group: RwLock<MlsGroupState>,
121    pub(in crate::mls::conversation) configuration: ConversationConfiguration,
122    pub(in crate::mls::conversation) session: Session,
123}
124
125impl Conversation {
126    /// Returns the conversation's ID
127    pub fn id(&self) -> &ConversationIdRef {
128        self.id.as_ref()
129    }
130
131    /// Returns an immutable guard over the underlying MLS group
132    pub(crate) async fn group(&self) -> RwLockReadGuard<'_, MlsGroupState> {
133        self.group.read().await
134    }
135
136    /// Returns the conversation's configuration
137    pub fn configuration(&self) -> &ConversationConfiguration {
138        &self.configuration
139    }
140
141    /// Returns current epoch of the MLS group
142    pub async fn epoch(&self) -> u64 {
143        self.group().await.epoch().as_u64()
144    }
145
146    /// Returns this conversation's cipher suite
147    pub fn cipher_suite(&self) -> CipherSuite {
148        self.configuration.cipher_suite
149    }
150
151    /// Returns a reference to the credential used in this conversation
152    pub async fn credential_ref(&self) -> Result<CredentialRef> {
153        let credential = self
154            .find_current_credential()
155            .await
156            .map_err(|_| Error::IdentityInitializationError)?;
157        Ok(CredentialRef::from_credential(&credential))
158    }
159
160    /// Derives a new key from the one in the group, to be used elsewhere.
161    ///
162    /// # Arguments
163    /// * `key_length` - the length of the key to be derived. If the value is higher than the bounds of `u16` or the
164    ///   context hash * 255, an error will be returned
165    ///
166    /// # Errors
167    /// OpenMls secret generation error
168    pub async fn export_secret_key(&self, key_length: usize) -> Result<SecretKey> {
169        const EXPORTER_LABEL: &str = "exporter";
170        const EXPORTER_CONTEXT: &[u8] = &[];
171        self.group()
172            .await
173            .export_secret(
174                &self.session.crypto_provider,
175                EXPORTER_LABEL,
176                EXPORTER_CONTEXT,
177                key_length,
178            )
179            .map(Into::into)
180            .map_err(OpenMlsError::wrap("exporting secret key"))
181            .map_err(Into::into)
182    }
183
184    /// Returns the first external sender present in this group.
185    ///
186    /// This should be used to initialize a subconversation
187    pub async fn get_external_sender(&self) -> Result<ExternalSender> {
188        let group = self.group().await;
189        let ext_senders = group
190            .group_context_extensions()
191            .external_senders()
192            .ok_or(Error::MissingExternalSenderExtension)?;
193        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
194        Ok(ext_sender.clone().into())
195    }
196}
197
198#[cfg(test)]
199mod test_utils {
200    use openmls::prelude::SignaturePublicKey;
201
202    use super::*;
203
204    impl Conversation {
205        pub async fn signature_keys(&self) -> Vec<SignaturePublicKey> {
206            let group = self.group().await;
207            group
208                .members()
209                .map(|m| m.signature_key)
210                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
211                .collect()
212        }
213
214        pub async fn encryption_keys(&self) -> Vec<Vec<u8>> {
215            let group = self.group().await;
216            group.members().map(|m| m.encryption_key).collect()
217        }
218
219        pub async fn extensions(&self) -> openmls::prelude::Extensions {
220            let group = self.group().await;
221            group.export_group_context().extensions().to_owned()
222        }
223    }
224}