core_crypto/mls/conversation/
duplicate.rs1use super::{Error, Result};
6use crate::{MlsError, prelude::MlsConversation};
7use mls_crypto_provider::MlsCryptoProvider;
8use openmls::prelude::{ContentType, FramedContentBodyIn, Proposal, PublicMessageIn, Sender};
9
10impl MlsConversation {
11 pub(crate) fn is_duplicate_message(&self, backend: &MlsCryptoProvider, msg: &PublicMessageIn) -> Result<bool> {
12 let (sender, content_type) = (msg.sender(), msg.body().content_type());
13
14 match (content_type, sender) {
15 (ContentType::Commit, Sender::Member(_) | Sender::NewMemberCommit) => {
16 if let Some(msg_ct) = msg.confirmation_tag() {
19 let group_ct = self
20 .group
21 .compute_confirmation_tag(backend)
22 .map_err(MlsError::wrap("computing confirmation tag"))?;
23 Ok(msg_ct == &group_ct)
24 } else {
25 Err(Error::MlsGroupInvalidState("a commit must have a ConfirmationTag"))
27 }
28 }
29 (ContentType::Proposal, Sender::Member(_) | Sender::NewMemberProposal) => {
30 match msg.body() {
31 FramedContentBodyIn::Proposal(proposal) => {
32 let proposal = Proposal::from(proposal.clone()); let already_exists = self.group.pending_proposals().any(|pp| pp.proposal() == &proposal);
34 Ok(already_exists)
35 }
36 _ => Err(Error::MlsGroupInvalidState(
37 "message body was not a proposal despite ContentType::Proposal",
38 )),
39 }
40 }
41 (_, _) => Ok(false),
42 }
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::super::error::Error;
49 use crate::mls::conversation::Conversation as _;
50 use crate::test_utils::*;
51 use wasm_bindgen_test::*;
52
53 wasm_bindgen_test_configure!(run_in_browser);
54
55 #[apply(all_cred_cipher)]
56 #[wasm_bindgen_test]
57 async fn decrypting_duplicate_member_commit_should_fail(case: TestCase) {
58 if !case.is_pure_ciphertext() {
60 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
61 Box::pin(async move {
62 let id = conversation_id();
63 alice_central
64 .context
65 .new_conversation(&id, case.credential_type, case.cfg.clone())
66 .await
67 .unwrap();
68 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
69
70 let unknown_commit = alice_central.create_unmerged_commit(&id).await.commit;
72 alice_central
73 .context
74 .conversation(&id)
75 .await
76 .unwrap()
77 .clear_pending_commit()
78 .await
79 .unwrap();
80
81 alice_central
82 .context
83 .conversation(&id)
84 .await
85 .unwrap()
86 .update_key_material()
87 .await
88 .unwrap();
89 let commit = alice_central.mls_transport.latest_commit().await;
90
91 bob_central
93 .context
94 .conversation(&id)
95 .await
96 .unwrap()
97 .decrypt_message(&commit.to_bytes().unwrap())
98 .await
99 .unwrap();
100 let decrypt_duplicate = bob_central
102 .context
103 .conversation(&id)
104 .await
105 .unwrap()
106 .decrypt_message(&commit.to_bytes().unwrap())
107 .await;
108 assert!(matches!(decrypt_duplicate.unwrap_err(), Error::DuplicateMessage));
109
110 let decrypt_lost_commit = bob_central
113 .context
114 .conversation(&id)
115 .await
116 .unwrap()
117 .decrypt_message(&unknown_commit.to_bytes().unwrap())
118 .await;
119 assert!(matches!(decrypt_lost_commit.unwrap_err(), Error::StaleCommit));
120 })
121 })
122 .await
123 }
124 }
125
126 #[apply(all_cred_cipher)]
127 #[wasm_bindgen_test]
128 async fn decrypting_duplicate_external_commit_should_fail(case: TestCase) {
129 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
130 Box::pin(async move {
131 let id = conversation_id();
132 alice_central
133 .context
134 .new_conversation(&id, case.credential_type, case.cfg.clone())
135 .await
136 .unwrap();
137
138 let gi = alice_central.get_group_info(&id).await;
139
140 let (unknown_ext_commit, mut pending_conversation) = bob_central
142 .create_unmerged_external_commit(gi.clone(), case.custom_cfg(), case.credential_type)
143 .await;
144 let unknown_ext_commit = unknown_ext_commit.commit;
145 pending_conversation.clear().await.unwrap();
146
147 bob_central
148 .context
149 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
150 .await
151 .unwrap();
152 let ext_commit = bob_central.mls_transport.latest_commit().await;
153
154 alice_central
156 .context
157 .conversation(&id)
158 .await
159 .unwrap()
160 .decrypt_message(&ext_commit.to_bytes().unwrap())
161 .await
162 .unwrap();
163 let decryption = alice_central
165 .context
166 .conversation(&id)
167 .await
168 .unwrap()
169 .decrypt_message(&ext_commit.to_bytes().unwrap())
170 .await;
171 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
172
173 let decryption = alice_central
176 .context
177 .conversation(&id)
178 .await
179 .unwrap()
180 .decrypt_message(&unknown_ext_commit.to_bytes().unwrap())
181 .await;
182 assert!(matches!(decryption.unwrap_err(), Error::StaleCommit));
183 })
184 })
185 .await
186 }
187
188 #[apply(all_cred_cipher)]
189 #[wasm_bindgen_test]
190 async fn decrypting_duplicate_proposal_should_fail(case: TestCase) {
191 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
192 Box::pin(async move {
193 let id = conversation_id();
194 alice_central
195 .context
196 .new_conversation(&id, case.credential_type, case.cfg.clone())
197 .await
198 .unwrap();
199 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
200
201 let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
202
203 bob_central
205 .context
206 .conversation(&id)
207 .await
208 .unwrap()
209 .decrypt_message(&proposal.to_bytes().unwrap())
210 .await
211 .unwrap();
212
213 let decryption = bob_central
215 .context
216 .conversation(&id)
217 .await
218 .unwrap()
219 .decrypt_message(&proposal.to_bytes().unwrap())
220 .await;
221 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
222
223 bob_central
225 .context
226 .conversation(&id)
227 .await
228 .unwrap()
229 .commit_pending_proposals()
230 .await
231 .unwrap();
232
233 let decryption = bob_central
235 .context
236 .conversation(&id)
237 .await
238 .unwrap()
239 .decrypt_message(&proposal.to_bytes().unwrap())
240 .await;
241 assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
242 })
243 })
244 .await
245 }
246
247 #[apply(all_cred_cipher)]
248 #[wasm_bindgen_test]
249 async fn decrypting_duplicate_external_proposal_should_fail(case: TestCase) {
250 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
251 Box::pin(async move {
252 let id = conversation_id();
253 alice_central
254 .context
255 .new_conversation(&id, case.credential_type, case.cfg.clone())
256 .await
257 .unwrap();
258
259 let epoch = alice_central.context.conversation(&id).await.unwrap().epoch().await;
260
261 let ext_proposal = bob_central
262 .context
263 .new_external_add_proposal(id.clone(), epoch.into(), case.ciphersuite(), case.credential_type)
264 .await
265 .unwrap();
266
267 alice_central
269 .context
270 .conversation(&id)
271 .await
272 .unwrap()
273 .decrypt_message(&ext_proposal.to_bytes().unwrap())
274 .await
275 .unwrap();
276
277 let decryption = alice_central
279 .context
280 .conversation(&id)
281 .await
282 .unwrap()
283 .decrypt_message(&ext_proposal.to_bytes().unwrap())
284 .await;
285 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
286
287 alice_central
289 .context
290 .conversation(&id)
291 .await
292 .unwrap()
293 .commit_pending_proposals()
294 .await
295 .unwrap();
296
297 let decryption = alice_central
299 .context
300 .conversation(&id)
301 .await
302 .unwrap()
303 .decrypt_message(&ext_proposal.to_bytes().unwrap())
304 .await;
305 assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
306 })
307 })
308 .await
309 }
310
311 #[apply(all_cred_cipher)]
313 #[wasm_bindgen_test]
314 async fn decrypting_duplicate_application_message_should_fail(case: TestCase) {
315 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
316 Box::pin(async move {
317 let id = conversation_id();
318 alice_central
319 .context
320 .new_conversation(&id, case.credential_type, case.cfg.clone())
321 .await
322 .unwrap();
323 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
324
325 let msg = b"Hello bob";
326 let encrypted = alice_central
327 .context
328 .conversation(&id)
329 .await
330 .unwrap()
331 .encrypt_message(msg)
332 .await
333 .unwrap();
334
335 bob_central
337 .context
338 .conversation(&id)
339 .await
340 .unwrap()
341 .decrypt_message(&encrypted)
342 .await
343 .unwrap();
344 let decryption = bob_central
346 .context
347 .conversation(&id)
348 .await
349 .unwrap()
350 .decrypt_message(&encrypted)
351 .await;
352 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
353 })
354 })
355 .await
356 }
357}