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