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}