core_crypto/mls/conversation/mutable/
wipe.rs1use core_crypto_keystore::{entities::PersistedMlsGroup, traits::EntityDeleteBorrowed};
2
3use super::Result;
4use crate::{KeystoreError, OpenMlsError, RecursiveError, mls::conversation::ConversationMut};
5
6impl ConversationMut {
7 pub async fn wipe(&mut self) -> Result<()> {
12 let mut conversation_cache = self
14 .tx_context
15 .mls_groups()
16 .await
17 .map_err(RecursiveError::context("getting mls conversation cache"))?;
18
19 self.mutate_group(async |transaction, group, _| {
20 let proposals = group
23 .pending_proposals()
24 .map(|proposal| proposal.proposal_reference().to_owned())
25 .collect::<Vec<_>>();
26 for proposal in proposals {
27 group
29 .remove_pending_proposal(transaction, &proposal)
30 .await
31 .map_err(OpenMlsError::wrap("removing pending proposal"))?;
32 }
33
34 Ok(())
35 })
36 .await?;
37
38 let id = self.id();
39 let context = self
40 .tx_context
41 .inner()
42 .map_err(RecursiveError::context("getting inner context"))?;
43 let tx = context.transaction();
44 PersistedMlsGroup::delete_borrowed(tx, id.keystore()).map_err(KeystoreError::wrap("deleting mls group"))?;
45 let _ = conversation_cache.remove(id);
46
47 drop(conversation_cache);
50
51 self.tx_context
54 .clear_orphaned_conversation_buffers(id)
55 .await
56 .map_err(RecursiveError::context(
57 "clearing buffered messages and commits of a wiped conversation",
58 ))?;
59
60 Ok(())
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use crate::{mls::conversation::Error, test_utils::*};
67
68 #[apply(all_cred_cipher)]
80 async fn wipe_abandons_buffered_messages(case: TestContext) {
81 Box::pin(async move {
82 let [mut alice, bob] = case.sessions().await;
83 let conversation = case.create_conversation([&alice, &bob]).await;
84
85 let conversation = conversation
87 .acting_as(&bob)
88 .await
89 .update()
90 .await
91 .process_member_changes()
92 .await
93 .finish();
94 let app_msg = conversation
95 .guard_of(&bob)
96 .await
97 .encrypt_message(b"Hello Alice !")
98 .await
99 .unwrap();
100
101 let decrypt = conversation.guard_of(&alice).await.decrypt_message(app_msg).await;
104 assert!(matches!(decrypt.unwrap_err(), Error::BufferedFutureMessage { .. }));
105 assert_eq!(
106 alice.transaction.count_entities().await.pending_messages,
107 1,
108 "the message Alice could not decrypt must have been buffered"
109 );
110
111 conversation.guard_of(&alice).await.wipe().await.unwrap();
113
114 drop(conversation);
115 alice.commit_transaction().await;
116
117 let counts = alice.transaction.count_entities().await;
118 assert_eq!(counts.group, 0, "the wipe must have removed the conversation");
119 assert_eq!(
120 counts.pending_messages, 0,
121 "the buffered message belongs to a conversation which no longer exists, so wiping that \
122 conversation must have taken the message with it"
123 );
124 })
125 .await
126 }
127
128 #[apply(all_cred_cipher)]
135 async fn wipe_abandons_buffered_commits(case: TestContext) {
136 Box::pin(async move {
137 let [mut alice, bob, charlie] = case.sessions().await;
138 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
139
140 let conversation = conversation
142 .acting_as(&bob)
143 .await
144 .remove_proposal(&charlie)
145 .await
146 .finish();
147
148 let commit_guard = conversation.acting_as(&bob).await.commit_pending_proposals().await;
151 let (commit_guard, result) = commit_guard.notify_member_fallible(&alice).await;
152 assert!(matches!(result.unwrap_err(), Error::BufferedCommit));
153 let conversation = commit_guard.finish();
154
155 assert_eq!(
156 alice.transaction.count_entities().await.buffered_commits,
157 1,
158 "the commit Alice could not apply must have been buffered"
159 );
160
161 conversation.guard_of(&alice).await.wipe().await.unwrap();
163
164 drop(conversation);
165 alice.commit_transaction().await;
166
167 let counts = alice.transaction.count_entities().await;
168 assert_eq!(counts.group, 0, "the wipe must have removed the conversation");
169 assert_eq!(
170 counts.buffered_commits, 0,
171 "the buffered commit belongs to a conversation which no longer exists, so wiping that \
172 conversation must have taken the commit with it"
173 );
174 })
175 .await
176 }
177}