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}