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::collections::{HashMap, hash_map::Entry};
10
11use async_lock::{RwLock, RwLockReadGuard};
12use core_crypto_keystore::{
13    Transaction,
14    entities::{PersistedMlsGroup, TargetedMessageTxCounter, TargetedMessageTxCounterPk},
15    traits::FetchFromDatabase,
16};
17use openmls::{
18    group::{InnerState, MlsGroup},
19    prelude::LeafNodeIndex,
20};
21
22use super::{ConversationIdRef, Error, Result, SecretKey};
23use crate::{
24    CipherSuite, ConversationConfiguration, ConversationId, CredentialRef, ExternalSender, KeystoreError, OpenMlsError,
25    Session, mls::TntMessageCounter,
26};
27
28#[derive(derive_more::Constructor, derive_more::Deref, derive_more::DerefMut, derive_more::Debug)]
29pub(crate) struct MlsGroupState {
30    #[deref]
31    #[deref_mut]
32    group: MlsGroup,
33    /// The count of targeted messages sent (therefore `tx`) this epoch, keyed by recipients.
34    /// Supposed to be used when encrypting targeted messages only, and to be reset whenever the mls epoch is
35    /// incremented. Do not access this field directly, use [MlsGroupState::obtain_targeted_message_tx_counter] and
36    /// [MlsGroupState::reset_targeted_message_tx_counters] only.
37    ///
38    /// The purpose of these counters is replay protection on the recipient side: we provide the count when sending a
39    /// message to a receiver, and they check if the counter is greater than any they've seen before.
40    #[debug(skip)]
41    targeted_message_tx_counters: HashMap<LeafNodeIndex, TntMessageCounter>,
42}
43
44impl MlsGroupState {
45    pub(in crate::mls::conversation) fn mls_group(&self) -> &MlsGroup {
46        &self.group
47    }
48
49    pub(in crate::mls::conversation) fn mls_group_mut(&mut self) -> &mut MlsGroup {
50        &mut self.group
51    }
52
53    /// Get the targeted message sender (tx) counter bound to this conversation and the given recipient after
54    /// incrementing it.
55    ///
56    /// If the counter hasn't been used yet for this conversation, it is loaded from the database or initialized
57    /// freshly.
58    pub(in crate::mls::conversation) async fn obtain_targeted_message_tx_counter(
59        &mut self,
60        recipient: LeafNodeIndex,
61        database: &impl FetchFromDatabase,
62    ) -> Result<TntMessageCounter> {
63        let key = TargetedMessageTxCounterPk::new(self.group_id().to_vec(), recipient.u32());
64
65        // `or_insert_with` can't be used because db loading is async.
66        let mut counter = match self.targeted_message_tx_counters.entry(recipient) {
67            Entry::Occupied(entry) => entry,
68            Entry::Vacant(entry) => {
69                let count = database
70                    .get::<TargetedMessageTxCounter>(&key)
71                    .await
72                    .map_err(KeystoreError::wrap("searching for tnt message counters for group"))?
73                    .map(|counter| counter.count)
74                    .unwrap_or_default();
75
76                entry.insert_entry(count.into())
77            }
78        };
79
80        counter.get_mut().increment()?;
81        self.group.set_state(openmls::group::InnerState::Changed);
82
83        Ok(*counter.get())
84    }
85
86    pub(in crate::mls::conversation) async fn reset_targeted_message_tx_counters(&mut self, tx: &Transaction) {
87        self.targeted_message_tx_counters = Default::default();
88        let id = self.group.group_id().to_vec();
89        tx.bulk_remove::<TargetedMessageTxCounter, _>(id.into()).await
90    }
91
92    pub(crate) async fn persist(&mut self, tx: &Transaction) -> Result<()> {
93        // We must change the mls group persisted state before persisting, otherwise it will never reach the DB.
94        self.mls_group_mut().set_state(InnerState::Persisted);
95        let id = self.group.group_id();
96
97        tx.save(PersistedMlsGroup {
98            id: id.to_vec(),
99            state: core_crypto_keystore::ser(self.mls_group())
100                .map_err(KeystoreError::wrap("serializing group state"))?,
101        })
102        .await
103        .map_err(KeystoreError::wrap("persisting mls group"))?;
104
105        for (receiver, counter) in self.targeted_message_tx_counters.iter() {
106            tx.save(TargetedMessageTxCounter {
107                conversation_id: id.to_vec(),
108                receiver: receiver.u32(),
109                count: (*counter).into(),
110            })
111            .await
112            .map_err(KeystoreError::wrap("saving tnt message counter"))?;
113        }
114
115        Ok(())
116    }
117}
118
119/// A Conversation exposes the read-only interface of an MLS conversation.
120#[derive(Debug, derive_more::Constructor)]
121pub struct Conversation {
122    pub(in crate::mls::conversation) id: ConversationId,
123    pub(in crate::mls::conversation) group: RwLock<MlsGroupState>,
124    pub(in crate::mls::conversation) configuration: ConversationConfiguration,
125    session: Session,
126}
127
128impl Conversation {
129    /// Returns the conversation's ID
130    pub fn id(&self) -> &ConversationIdRef {
131        ConversationIdRef::new(&self.id)
132    }
133
134    /// Returns an immutable guard over the underlying MLS group
135    pub(crate) async fn group(&self) -> RwLockReadGuard<'_, MlsGroupState> {
136        self.group.read().await
137    }
138
139    /// Returns the conversation's configuration
140    pub fn configuration(&self) -> &ConversationConfiguration {
141        &self.configuration
142    }
143
144    /// Returns current epoch of the MLS group
145    pub async fn epoch(&self) -> u64 {
146        self.group().await.epoch().as_u64()
147    }
148
149    /// Returns this conversation's cipher suite
150    pub fn cipher_suite(&self) -> CipherSuite {
151        self.configuration.cipher_suite
152    }
153
154    /// Returns a reference to the credential used in this conversation
155    pub async fn credential_ref(&self) -> Result<CredentialRef> {
156        let credential = self
157            .find_current_credential()
158            .await
159            .map_err(|_| Error::IdentityInitializationError)?;
160        Ok(CredentialRef::from_credential(&credential))
161    }
162
163    /// Derives a new key from the one in the group, to be used elsewhere.
164    ///
165    /// # Arguments
166    /// * `key_length` - the length of the key to be derived. If the value is higher than the bounds of `u16` or the
167    ///   context hash * 255, an error will be returned
168    ///
169    /// # Errors
170    /// OpenMls secret generation error
171    pub async fn export_secret_key(&self, key_length: usize) -> Result<SecretKey> {
172        const EXPORTER_LABEL: &str = "exporter";
173        const EXPORTER_CONTEXT: &[u8] = &[];
174        self.group()
175            .await
176            .export_secret(
177                &self.session.crypto_provider,
178                EXPORTER_LABEL,
179                EXPORTER_CONTEXT,
180                key_length,
181            )
182            .map(Into::into)
183            .map_err(OpenMlsError::wrap("exporting secret key"))
184            .map_err(Into::into)
185    }
186
187    /// Returns the first external sender present in this group.
188    ///
189    /// This should be used to initialize a subconversation
190    pub async fn get_external_sender(&self) -> Result<ExternalSender> {
191        let group = self.group().await;
192        let ext_senders = group
193            .group_context_extensions()
194            .external_senders()
195            .ok_or(Error::MissingExternalSenderExtension)?;
196        let ext_sender = ext_senders.first().ok_or(Error::MissingExternalSenderExtension)?;
197        Ok(ext_sender.clone().into())
198    }
199}
200
201#[cfg(test)]
202mod test_utils {
203    use openmls::prelude::SignaturePublicKey;
204
205    use super::*;
206
207    impl Conversation {
208        pub async fn signature_keys(&self) -> Vec<SignaturePublicKey> {
209            let group = self.group().await;
210            group
211                .members()
212                .map(|m| m.signature_key)
213                .map(|mpk| SignaturePublicKey::from(mpk.as_slice()))
214                .collect()
215        }
216
217        pub async fn encryption_keys(&self) -> Vec<Vec<u8>> {
218            let group = self.group().await;
219            group.members().map(|m| m.encryption_key).collect()
220        }
221
222        pub async fn extensions(&self) -> openmls::prelude::Extensions {
223            let group = self.group().await;
224            group.export_group_context().extensions().to_owned()
225        }
226    }
227}