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