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: TestContext) {
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 .transaction
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 .transaction
74 .conversation(&id)
75 .await
76 .unwrap()
77 .clear_pending_commit()
78 .await
79 .unwrap();
80
81 alice_central
82 .transaction
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 .transaction
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 .transaction
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 .transaction
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: TestContext) {
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 .transaction
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 .transaction
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 .transaction
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 .transaction
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 .transaction
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: TestContext) {
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 .transaction
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
202 .transaction
203 .new_update_proposal(&id)
204 .await
205 .unwrap()
206 .proposal;
207
208 bob_central
210 .transaction
211 .conversation(&id)
212 .await
213 .unwrap()
214 .decrypt_message(&proposal.to_bytes().unwrap())
215 .await
216 .unwrap();
217
218 let decryption = bob_central
220 .transaction
221 .conversation(&id)
222 .await
223 .unwrap()
224 .decrypt_message(&proposal.to_bytes().unwrap())
225 .await;
226 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
227
228 bob_central
230 .transaction
231 .conversation(&id)
232 .await
233 .unwrap()
234 .commit_pending_proposals()
235 .await
236 .unwrap();
237
238 let decryption = bob_central
240 .transaction
241 .conversation(&id)
242 .await
243 .unwrap()
244 .decrypt_message(&proposal.to_bytes().unwrap())
245 .await;
246 assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
247 })
248 })
249 .await
250 }
251
252 #[apply(all_cred_cipher)]
253 #[wasm_bindgen_test]
254 async fn decrypting_duplicate_external_proposal_should_fail(case: TestContext) {
255 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
256 Box::pin(async move {
257 let id = conversation_id();
258 alice_central
259 .transaction
260 .new_conversation(&id, case.credential_type, case.cfg.clone())
261 .await
262 .unwrap();
263
264 let epoch = alice_central.transaction.conversation(&id).await.unwrap().epoch().await;
265
266 let ext_proposal = bob_central
267 .transaction
268 .new_external_add_proposal(id.clone(), epoch.into(), case.ciphersuite(), case.credential_type)
269 .await
270 .unwrap();
271
272 alice_central
274 .transaction
275 .conversation(&id)
276 .await
277 .unwrap()
278 .decrypt_message(&ext_proposal.to_bytes().unwrap())
279 .await
280 .unwrap();
281
282 let decryption = alice_central
284 .transaction
285 .conversation(&id)
286 .await
287 .unwrap()
288 .decrypt_message(&ext_proposal.to_bytes().unwrap())
289 .await;
290 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
291
292 alice_central
294 .transaction
295 .conversation(&id)
296 .await
297 .unwrap()
298 .commit_pending_proposals()
299 .await
300 .unwrap();
301
302 let decryption = alice_central
304 .transaction
305 .conversation(&id)
306 .await
307 .unwrap()
308 .decrypt_message(&ext_proposal.to_bytes().unwrap())
309 .await;
310 assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
311 })
312 })
313 .await
314 }
315
316 #[apply(all_cred_cipher)]
318 #[wasm_bindgen_test]
319 async fn decrypting_duplicate_application_message_should_fail(case: TestContext) {
320 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
321 Box::pin(async move {
322 let id = conversation_id();
323 alice_central
324 .transaction
325 .new_conversation(&id, case.credential_type, case.cfg.clone())
326 .await
327 .unwrap();
328 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
329
330 let msg = b"Hello bob";
331 let encrypted = alice_central
332 .transaction
333 .conversation(&id)
334 .await
335 .unwrap()
336 .encrypt_message(msg)
337 .await
338 .unwrap();
339
340 bob_central
342 .transaction
343 .conversation(&id)
344 .await
345 .unwrap()
346 .decrypt_message(&encrypted)
347 .await
348 .unwrap();
349 let decryption = bob_central
351 .transaction
352 .conversation(&id)
353 .await
354 .unwrap()
355 .decrypt_message(&encrypted)
356 .await;
357 assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
358 })
359 })
360 .await
361 }
362}