core_crypto/mls/conversation/
duplicate.rs1use crate::prelude::MlsConversation;
6use crate::{CryptoError, MlsError};
7use mls_crypto_provider::MlsCryptoProvider;
8use openmls::prelude::{ContentType, FramedContentBodyIn, Proposal, PublicMessageIn, Sender};
9
10impl MlsConversation {
11 pub(crate) fn is_duplicate_message(
12 &self,
13 backend: &MlsCryptoProvider,
14 msg: &PublicMessageIn,
15 ) -> Result<bool, CryptoError> {
16 let (sender, content_type) = (msg.sender(), msg.body().content_type());
17
18 match (content_type, sender) {
19 (ContentType::Commit, Sender::Member(_) | Sender::NewMemberCommit) => {
20 if let Some(msg_ct) = msg.confirmation_tag() {
23 let group_ct = self.group.compute_confirmation_tag(backend).map_err(MlsError::from)?;
24 Ok(msg_ct == &group_ct)
25 } else {
26 Err(CryptoError::InternalMlsError)
28 }
29 }
30 (ContentType::Proposal, Sender::Member(_) | Sender::NewMemberProposal) => {
31 match msg.body() {
32 FramedContentBodyIn::Proposal(proposal) => {
33 let proposal = Proposal::from(proposal.clone()); let already_exists = self.group.pending_proposals().any(|pp| pp.proposal() == &proposal);
35 Ok(already_exists)
36 }
37 _ => Err(CryptoError::InternalMlsError),
38 }
39 }
40 (_, _) => Ok(false),
41 }
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use crate::{test_utils::*, CryptoError};
48 use wasm_bindgen_test::*;
49
50 wasm_bindgen_test_configure!(run_in_browser);
51
52 #[apply(all_cred_cipher)]
53 #[wasm_bindgen_test]
54 async fn decrypting_duplicate_member_commit_should_fail(case: TestCase) {
55 if !case.is_pure_ciphertext() {
57 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
58 Box::pin(async move {
59 let id = conversation_id();
60 alice_central
61 .context
62 .new_conversation(&id, case.credential_type, case.cfg.clone())
63 .await
64 .unwrap();
65 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
66
67 let unknown_commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
69 alice_central.context.clear_pending_commit(&id).await.unwrap();
70
71 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
72 alice_central.context.commit_accepted(&id).await.unwrap();
73
74 bob_central
76 .context
77 .decrypt_message(&id, &commit.to_bytes().unwrap())
78 .await
79 .unwrap();
80 let decrypt_duplicate = bob_central
82 .context
83 .decrypt_message(&id, &commit.to_bytes().unwrap())
84 .await;
85 assert!(matches!(decrypt_duplicate.unwrap_err(), CryptoError::DuplicateMessage));
86
87 let decrypt_lost_commit = bob_central
90 .context
91 .decrypt_message(&id, &unknown_commit.to_bytes().unwrap())
92 .await;
93 assert!(matches!(decrypt_lost_commit.unwrap_err(), CryptoError::StaleCommit));
94 })
95 })
96 .await
97 }
98 }
99
100 #[apply(all_cred_cipher)]
101 #[wasm_bindgen_test]
102 async fn decrypting_duplicate_external_commit_should_fail(case: TestCase) {
103 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
104 Box::pin(async move {
105 let id = conversation_id();
106 alice_central
107 .context
108 .new_conversation(&id, case.credential_type, case.cfg.clone())
109 .await
110 .unwrap();
111
112 let gi = alice_central.get_group_info(&id).await;
113
114 let unknown_ext_commit = bob_central
116 .context
117 .join_by_external_commit(gi.clone(), case.custom_cfg(), case.credential_type)
118 .await
119 .unwrap()
120 .commit;
121 bob_central
122 .context
123 .clear_pending_group_from_external_commit(&id)
124 .await
125 .unwrap();
126
127 let ext_commit = bob_central
128 .context
129 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
130 .await
131 .unwrap()
132 .commit;
133 bob_central
134 .context
135 .merge_pending_group_from_external_commit(&id)
136 .await
137 .unwrap();
138
139 alice_central
141 .context
142 .decrypt_message(&id, &ext_commit.to_bytes().unwrap())
143 .await
144 .unwrap();
145 let decryption = alice_central
147 .context
148 .decrypt_message(&id, &ext_commit.to_bytes().unwrap())
149 .await;
150 assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
151
152 let decryption = alice_central
155 .context
156 .decrypt_message(&id, &unknown_ext_commit.to_bytes().unwrap())
157 .await;
158 assert!(matches!(decryption.unwrap_err(), CryptoError::StaleCommit));
159 })
160 })
161 .await
162 }
163
164 #[apply(all_cred_cipher)]
165 #[wasm_bindgen_test]
166 async fn decrypting_duplicate_proposal_should_fail(case: TestCase) {
167 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
168 Box::pin(async move {
169 let id = conversation_id();
170 alice_central
171 .context
172 .new_conversation(&id, case.credential_type, case.cfg.clone())
173 .await
174 .unwrap();
175 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
176
177 let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
178
179 bob_central
181 .context
182 .decrypt_message(&id, &proposal.to_bytes().unwrap())
183 .await
184 .unwrap();
185
186 let decryption = bob_central
188 .context
189 .decrypt_message(&id, &proposal.to_bytes().unwrap())
190 .await;
191 assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
192
193 bob_central.context.commit_pending_proposals(&id).await.unwrap();
195 bob_central.context.commit_accepted(&id).await.unwrap();
196
197 let decryption = bob_central
199 .context
200 .decrypt_message(&id, &proposal.to_bytes().unwrap())
201 .await;
202 assert!(matches!(decryption.unwrap_err(), CryptoError::StaleProposal));
203 })
204 })
205 .await
206 }
207
208 #[apply(all_cred_cipher)]
209 #[wasm_bindgen_test]
210 async fn decrypting_duplicate_external_proposal_should_fail(case: TestCase) {
211 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
212 Box::pin(async move {
213 let id = conversation_id();
214 alice_central
215 .context
216 .new_conversation(&id, case.credential_type, case.cfg.clone())
217 .await
218 .unwrap();
219
220 let epoch = alice_central.context.conversation_epoch(&id).await.unwrap();
221
222 let ext_proposal = bob_central
223 .context
224 .new_external_add_proposal(id.clone(), epoch.into(), case.ciphersuite(), case.credential_type)
225 .await
226 .unwrap();
227
228 alice_central
230 .context
231 .decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
232 .await
233 .unwrap();
234
235 let decryption = alice_central
237 .context
238 .decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
239 .await;
240 assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
241
242 alice_central.context.commit_pending_proposals(&id).await.unwrap();
244 alice_central.context.commit_accepted(&id).await.unwrap();
245
246 let decryption = alice_central
248 .context
249 .decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
250 .await;
251 assert!(matches!(decryption.unwrap_err(), CryptoError::StaleProposal));
252 })
253 })
254 .await
255 }
256
257 #[apply(all_cred_cipher)]
259 #[wasm_bindgen_test]
260 async fn decrypting_duplicate_application_message_should_fail(case: TestCase) {
261 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
262 Box::pin(async move {
263 let id = conversation_id();
264 alice_central
265 .context
266 .new_conversation(&id, case.credential_type, case.cfg.clone())
267 .await
268 .unwrap();
269 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
270
271 let msg = b"Hello bob";
272 let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
273
274 bob_central.context.decrypt_message(&id, &encrypted).await.unwrap();
276 let decryption = bob_central.context.decrypt_message(&id, &encrypted).await;
278 assert!(matches!(decryption.unwrap_err(), CryptoError::DuplicateMessage));
279 })
280 })
281 .await
282 }
283}