core_crypto/mls/conversation/
duplicate.rs

1//! Due the current delivery semantics on backend side (at least once) we have to deal with this
2//! in CoreCrypto so as not to return a decryption error to the client. Remove this when this is used
3//! with a DS guaranteeing exactly once delivery semantics since the following degrades the performances
4
5use 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                // we use the confirmation tag to detect duplicate since it is issued from the GroupContext
17                // which is supposed to be unique per epoch
18                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                    // a commit MUST have a ConfirmationTag
26                    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()); // TODO: eventually remove this clone 😮‍💨. Tracking issue: WPB-9622
33                        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::test_utils::*;
50    use wasm_bindgen_test::*;
51
52    wasm_bindgen_test_configure!(run_in_browser);
53
54    #[apply(all_cred_cipher)]
55    #[wasm_bindgen_test]
56    async fn decrypting_duplicate_member_commit_should_fail(case: TestContext) {
57        // cannot work in pure ciphertext since we'd have to decrypt the message first
58        if case.is_pure_ciphertext() {
59            return;
60        }
61
62        let [alice, bob] = case.sessions().await;
63        Box::pin(async move {
64            let conversation = case.create_conversation([&alice, &bob]).await;
65
66            // an commit to verify that we can still detect wrong epoch correctly
67            let commit_guard = conversation.update_unmerged().await;
68            let unknown_commit = commit_guard.message();
69            let conversation = commit_guard.finish();
70            conversation.guard().await.clear_pending_commit().await.unwrap();
71
72            let commit_guard = conversation.update().await;
73            let commit = commit_guard.message();
74
75            // decrypt once ... ok
76            let conversation = commit_guard.notify_members().await;
77            // decrypt twice ... not ok
78            let decrypt_duplicate = conversation
79                .guard_of(&bob)
80                .await
81                .decrypt_message(&commit.to_bytes().unwrap())
82                .await;
83            assert!(matches!(decrypt_duplicate.unwrap_err(), Error::DuplicateMessage));
84
85            // Decrypting unknown commit.
86            // It fails with this error since it's not the commit who has created this epoch
87            let decrypt_lost_commit = conversation
88                .guard_of(&bob)
89                .await
90                .decrypt_message(&unknown_commit.to_bytes().unwrap())
91                .await;
92            assert!(matches!(decrypt_lost_commit.unwrap_err(), Error::StaleCommit));
93        })
94        .await
95    }
96
97    #[apply(all_cred_cipher)]
98    #[wasm_bindgen_test]
99    async fn decrypting_duplicate_external_commit_should_fail(case: TestContext) {
100        let [alice, bob] = case.sessions().await;
101        Box::pin(async move {
102            let conversation = case.create_conversation([&alice]).await;
103
104            // an external commit to verify that we can still detect wrong epoch correctly
105            let (commit_guard, mut pending_conversation) = conversation.external_join_unmerged(&bob).await;
106            let unknown_ext_commit = commit_guard.message();
107            pending_conversation.clear().await.unwrap();
108            let conversation = commit_guard.finish();
109
110            let commit_guard = conversation.external_join(&bob).await;
111            let ext_commit = commit_guard.message();
112
113            // decrypt once ... ok
114            let conversation = commit_guard.notify_members().await;
115            // decrypt twice ... not ok
116            let decryption = conversation
117                .guard()
118                .await
119                .decrypt_message(&ext_commit.to_bytes().unwrap())
120                .await;
121            assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
122
123            // Decrypting unknown external commit.
124            // It fails with this error since it's not the external commit who has created this epoch
125            let decryption = conversation
126                .guard()
127                .await
128                .decrypt_message(&unknown_ext_commit.to_bytes().unwrap())
129                .await;
130            assert!(matches!(decryption.unwrap_err(), Error::StaleCommit));
131        })
132        .await
133    }
134
135    #[apply(all_cred_cipher)]
136    #[wasm_bindgen_test]
137    async fn decrypting_duplicate_proposal_should_fail(case: TestContext) {
138        let [alice, bob] = case.sessions().await;
139        Box::pin(async move {
140            let conversation = case.create_conversation([&alice, &bob]).await;
141
142            let proposal_guard = conversation.update_proposal().await;
143            let proposal = proposal_guard.message();
144
145            // decrypt once ... ok
146            let conversation = proposal_guard.notify_members().await;
147
148            // decrypt twice ... not ok
149            let decryption = conversation
150                .guard_of(&bob)
151                .await
152                .decrypt_message(&proposal.to_bytes().unwrap())
153                .await;
154            assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
155
156            // advance Bob's epoch to trigger failure
157            let conversation = conversation
158                .acting_as(&bob)
159                .await
160                .commit_pending_proposals_notify()
161                .await;
162
163            // Epoch has advanced so we cannot detect duplicates anymore
164            let decryption = conversation
165                .guard_of(&bob)
166                .await
167                .decrypt_message(&proposal.to_bytes().unwrap())
168                .await;
169            assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
170        })
171        .await
172    }
173
174    #[apply(all_cred_cipher)]
175    #[wasm_bindgen_test]
176    async fn decrypting_duplicate_external_proposal_should_fail(case: TestContext) {
177        let [alice, bob] = case.sessions().await;
178        Box::pin(async move {
179            let conversation = case.create_conversation([&alice]).await;
180
181            let proposal_guard = conversation.external_join_proposal(&bob).await;
182            let proposal = proposal_guard.message();
183
184            // decrypt once ... ok
185            let conversation = proposal_guard.notify_members().await;
186
187            // decrypt twice ... not ok
188            let decryption = conversation
189                .guard()
190                .await
191                .decrypt_message(&proposal.to_bytes().unwrap())
192                .await;
193            assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
194
195            // advance alice's epoch
196            let conversation = conversation.commit_pending_proposals_notify().await;
197
198            // Epoch has advanced so we cannot detect duplicates anymore
199            let decryption = conversation
200                .guard()
201                .await
202                .decrypt_message(&proposal.to_bytes().unwrap())
203                .await;
204            assert!(matches!(decryption.unwrap_err(), Error::StaleProposal));
205        })
206        .await
207    }
208
209    // Ensures decrypting an application message is durable (we increment the messages generation & persist the group)
210    #[apply(all_cred_cipher)]
211    #[wasm_bindgen_test]
212    async fn decrypting_duplicate_application_message_should_fail(case: TestContext) {
213        let [alice, bob] = case.sessions().await;
214        Box::pin(async move {
215            let conversation = case.create_conversation([&alice, &bob]).await;
216
217            let msg = b"Hello bob";
218            let encrypted = conversation.guard().await.encrypt_message(msg).await.unwrap();
219
220            // decrypt once .. ok
221            conversation
222                .guard_of(&bob)
223                .await
224                .decrypt_message(&encrypted)
225                .await
226                .unwrap();
227            // decrypt twice .. not ok
228            let decryption = conversation.guard_of(&bob).await.decrypt_message(&encrypted).await;
229            assert!(matches!(decryption.unwrap_err(), Error::DuplicateMessage));
230        })
231        .await
232    }
233}