Skip to main content

openmls/group/mls_group/
application.rs

1use openmls_traits::signatures::Signer;
2
3use crate::storage::OpenMlsProvider;
4
5#[cfg(feature = "virtual-clients-draft")]
6use crate::tree::secret_tree::SecretType;
7
8#[cfg(feature = "virtual-clients-draft")]
9use super::errors::ConfirmMessageError;
10use super::{errors::CreateMessageError, *};
11
12/// The result of [`MlsGroup::create_unconfirmed_message`]: the encrypted
13/// message together with the bookkeeping a virtual client needs to coordinate
14/// the send with the DS.
15#[cfg(feature = "virtual-clients-draft")]
16#[derive(Debug, Clone)]
17pub struct UnconfirmedMessage {
18    /// The encrypted application message to fan out.
19    pub message: MlsMessageOut,
20    /// The epoch the message was encrypted in. Pass it together with
21    /// `generation` to [`MlsGroup::confirm_application_message`] once the DS
22    /// has accepted the message, to delete the retained encryption secret.
23    pub epoch: GroupEpoch,
24    /// The ratchet generation used for encryption. Pass it together with
25    /// `epoch` to [`MlsGroup::confirm_application_message`] once the DS has
26    /// accepted the message, to delete the retained encryption secret.
27    pub generation: u32,
28    /// The [`GenerationId`] to attach to the fanned-out message, present when
29    /// the group is bound to an emulation epoch and `None` otherwise. A
30    /// strongly-consistent DS compares it across siblings to detect generation
31    /// collisions.
32    ///
33    /// [`GenerationId`]: crate::components::vc_derivation_info::GenerationId
34    pub generation_id: Option<crate::components::vc_derivation_info::GenerationId>,
35}
36
37impl MlsGroup {
38    // === Application messages ===
39
40    /// Creates an application message. Returns
41    /// `CreateMessageError::MlsGroupStateError::UseAfterEviction` if the member
42    /// is no longer part of the group. Returns
43    /// `CreateMessageError::MlsGroupStateError::PendingProposal` if pending
44    /// proposals exist. In that case `.process_pending_proposals()` must be
45    /// called first and incoming messages from the DS must be processed
46    /// afterwards.
47    #[cfg(not(feature = "virtual-clients-draft"))]
48    pub fn create_message<Provider: OpenMlsProvider>(
49        &mut self,
50        provider: &Provider,
51        signer: &impl Signer,
52        message: &[u8],
53    ) -> Result<MlsMessageOut, CreateMessageError> {
54        let (_, output) =
55            self.create_message_internal::<_, CreateMessageError>(provider, signer, message)?;
56        Ok(output)
57    }
58
59    /// Creates an application message. Returns
60    /// `CreateMessageError::MlsGroupStateError::UseAfterEviction` if the member
61    /// is no longer part of the group. Returns
62    /// `CreateMessageError::MlsGroupStateError::PendingProposal` if pending
63    /// proposals exist. In that case `.process_pending_proposals()` must be
64    /// called first and incoming messages from the DS must be processed
65    /// afterwards.
66    #[cfg(all(feature = "virtual-clients-draft", any(feature = "test-utils", test)))]
67    pub fn create_message<Provider: OpenMlsProvider>(
68        &mut self,
69        provider: &Provider,
70        signer: &impl Signer,
71        message: &[u8],
72    ) -> Result<MlsMessageOut, CreateMessageError<Provider::StorageError>> {
73        let (generation, _generation_id, output) =
74            self.create_message_internal(provider, signer, message)?;
75        self.confirm_application_message(provider.storage(), self.epoch(), generation)?;
76        Ok(output)
77    }
78
79    #[cfg(not(feature = "virtual-clients-draft"))]
80    fn create_message_internal<Provider: OpenMlsProvider, E>(
81        &mut self,
82        provider: &Provider,
83        signer: &impl Signer,
84        message: &[u8],
85    ) -> Result<(u32, MlsMessageOut), E>
86    where
87        E: From<LibraryError> + From<MlsGroupStateError>,
88    {
89        if !self.is_active() {
90            return Err(MlsGroupStateError::UseAfterEviction.into());
91        }
92        if !self.proposal_store().is_empty() {
93            return Err(MlsGroupStateError::PendingProposal.into());
94        }
95
96        let aad = self.outgoing_authenticated_data()?;
97        let authenticated_content = AuthenticatedContent::new_application(
98            self.own_leaf_index(),
99            &aad,
100            message,
101            self.context(),
102            signer,
103        )?;
104        let EncryptionOutput {
105            generation,
106            private_message,
107        } = self
108            .encrypt(authenticated_content, provider)
109            // We know the application message is wellformed and we have the key material of the current epoch
110            .map_err(|_| LibraryError::custom("Malformed plaintext"))?;
111
112        let output = MlsMessageOut::from_private_message(private_message, self.version());
113        self.reset_aad();
114        Ok((generation, output))
115    }
116
117    #[cfg(feature = "virtual-clients-draft")]
118    fn create_message_internal<Provider: OpenMlsProvider>(
119        &mut self,
120        provider: &Provider,
121        signer: &impl Signer,
122        message: &[u8],
123    ) -> Result<
124        (
125            u32,
126            Option<crate::components::vc_derivation_info::GenerationId>,
127            MlsMessageOut,
128        ),
129        CreateMessageError<Provider::StorageError>,
130    > {
131        if !self.is_active() {
132            return Err(MlsGroupStateError::UseAfterEviction.into());
133        }
134        if !self.proposal_store().is_empty() {
135            return Err(MlsGroupStateError::PendingProposal.into());
136        }
137
138        let aad = self.outgoing_authenticated_data()?;
139        let authenticated_content = AuthenticatedContent::new_application(
140            self.own_leaf_index(),
141            &aad,
142            message,
143            self.context(),
144            signer,
145        )?;
146        let EncryptionOutput {
147            generation,
148            private_message,
149            generation_id,
150        } = self.encrypt(authenticated_content, provider)?;
151
152        let output = MlsMessageOut::from_private_message(private_message, self.version());
153        self.reset_aad();
154        Ok((generation, generation_id, output))
155    }
156
157    /// Creates an application message. Encryption secrets are only deleted
158    /// after the message has been confirmed via
159    /// `confirm_application_message()`.
160    ///
161    /// Returns the ratchet `generation` used for encryption, an optional
162    /// [`GenerationId`], and the encrypted message. The `generation` is passed
163    /// back to `confirm_application_message` to delete the retained encryption
164    /// secret once the DS has accepted the message. The [`GenerationId`] is
165    /// present when the group is bound to an emulation epoch and `None`
166    /// otherwise. When present, the application attaches it to the fanned-out
167    /// message so a strongly-consistent DS can detect generation collisions
168    /// between siblings.
169    ///
170    /// Returns `CreateMessageError::MlsGroupStateError::UseAfterEviction` if
171    /// the member is no longer part of the group. Returns
172    /// `CreateMessageError::MlsGroupStateError::PendingProposal` if pending
173    /// proposals exist. In that case `.process_pending_proposals()` must be
174    /// called first and incoming messages from the DS must be processed
175    /// afterwards.
176    ///
177    /// [`GenerationId`]: crate::components::vc_derivation_info::GenerationId
178    #[cfg(feature = "virtual-clients-draft")]
179    pub fn create_unconfirmed_message<Provider: OpenMlsProvider>(
180        &mut self,
181        provider: &Provider,
182        signer: &impl Signer,
183        message: &[u8],
184    ) -> Result<UnconfirmedMessage, CreateMessageError<Provider::StorageError>> {
185        let (generation, generation_id, message) =
186            self.create_message_internal(provider, signer, message)?;
187        Ok(UnconfirmedMessage {
188            message,
189            epoch: self.epoch(),
190            generation,
191            generation_id,
192        })
193    }
194
195    /// Deletes the retained own secret of the given `secret_type` created at
196    /// (`epoch`, `generation`). A confirm call deletes exactly the secret its
197    /// corresponding create call retained, or nothing.
198    #[cfg(feature = "virtual-clients-draft")]
199    fn confirm_own_secret<Storage: StorageProvider>(
200        &mut self,
201        storage: &Storage,
202        epoch: GroupEpoch,
203        generation: u32,
204        secret_type: SecretType,
205    ) -> Result<(), ConfirmMessageError<Storage::Error>> {
206        // Dispatch on the creation epoch directly rather than through
207        // `message_secrets_for_epoch_mut`, which maps future epochs to the
208        // current tree and would delete a different message's secret.
209        let message_secrets = if epoch > self.context().epoch() {
210            return Err(ConfirmMessageError::FutureEpoch);
211        } else if epoch == self.context().epoch() {
212            self.message_secrets_store.message_secrets_mut()
213        } else {
214            // The retained secret is already gone once its epoch has aged out
215            // of the store, so there is nothing left to delete.
216            let Some(message_secrets) = self.message_secrets_store.secrets_for_epoch_mut(epoch)
217            else {
218                return Ok(());
219            };
220            message_secrets
221        };
222        message_secrets
223            .secret_tree_mut()
224            .delete_own_secret_for_generation(secret_type, generation)?;
225        storage
226            .write_message_secrets(self.group_id(), &self.message_secrets_store)
227            .map_err(ConfirmMessageError::StorageError)?;
228        Ok(())
229    }
230
231    /// Deletes the retained encryption secret of the application message created
232    /// at (`epoch`, `generation`). A confirm call deletes exactly the secret its
233    /// corresponding [`MlsGroup::create_unconfirmed_message`] call retained, or
234    /// nothing.
235    ///
236    /// This is a no-op success when the epoch has aged out of the message
237    /// secrets store, or when the generation's secret is already gone (already
238    /// confirmed, or consumed by processing the message's own echo).
239    ///
240    /// Returns [`ConfirmMessageError::FutureEpoch`] when `epoch` is newer than
241    /// the group's current epoch.
242    ///
243    /// Only confirm once the DS has accepted exactly this message. After a lost
244    /// race against a sibling (the DS rejected the send because of a generation
245    /// collision), the secret must not be confirmed, since it is what decrypts
246    /// the sibling's winning message at the same generation.
247    #[cfg(feature = "virtual-clients-draft")]
248    pub fn confirm_application_message<Storage: StorageProvider>(
249        &mut self,
250        storage: &Storage,
251        epoch: GroupEpoch,
252        generation: u32,
253    ) -> Result<(), ConfirmMessageError<Storage::Error>> {
254        self.confirm_own_secret(storage, epoch, generation, SecretType::ApplicationSecret)
255    }
256
257    /// Deletes the retained encryption secret of the handshake message (proposal
258    /// or commit) created at (`epoch`, `generation`). A confirm call deletes
259    /// exactly the secret its corresponding create call retained, or nothing.
260    ///
261    /// Proposals and commits draw generations from the same per-epoch handshake
262    /// ratchet, so this single endpoint covers both.
263    ///
264    /// This is a no-op success when the epoch has aged out of the message
265    /// secrets store, or when the generation's secret is already gone (already
266    /// confirmed, or consumed by processing the message's own echo).
267    ///
268    /// Returns [`ConfirmMessageError::FutureEpoch`] when `epoch` is newer than
269    /// the group's current epoch.
270    ///
271    /// Only confirm once the DS has accepted exactly this message. After a lost
272    /// race against a sibling (the DS rejected the send because of a generation
273    /// collision), the secret must not be confirmed, since it is what decrypts
274    /// the sibling's winning message at the same generation.
275    #[cfg(feature = "virtual-clients-draft")]
276    pub fn confirm_handshake_message<Storage: StorageProvider>(
277        &mut self,
278        storage: &Storage,
279        epoch: GroupEpoch,
280        generation: u32,
281    ) -> Result<(), ConfirmMessageError<Storage::Error>> {
282        self.confirm_own_secret(storage, epoch, generation, SecretType::HandshakeSecret)
283    }
284}