core_crypto/mls/conversation/
proposal.rs1use core_crypto_keystore::Database;
9use openmls::{binary_tree::LeafNodeIndex, framing::MlsMessageOut, key_packages::KeyPackageIn, prelude::LeafNode};
10
11use super::{Error, Result};
12use crate::{MlsConversation, MlsError, MlsProposalRef, Session, mls_provider::MlsCryptoProvider};
13
14impl MlsConversation {
16 #[cfg_attr(test, crate::durable)]
18 pub async fn propose_add_member(
19 &mut self,
20 client: &Session<Database>,
21 backend: &MlsCryptoProvider,
22 database: &Database,
23 key_package: KeyPackageIn,
24 ) -> Result<MlsProposalBundle> {
25 let signer = &self
26 .find_current_credential(client)
27 .await
28 .map_err(|_| Error::IdentityInitializationError)?
29 .signature_key_pair;
30
31 let (proposal, proposal_ref) = self
32 .group
33 .propose_add_member(backend, signer, key_package)
34 .await
35 .map_err(MlsError::wrap("propose add member"))?;
36 let proposal = MlsProposalBundle {
37 proposal,
38 proposal_ref: proposal_ref.into(),
39 };
40 self.persist_group_when_changed(database, false).await?;
41 Ok(proposal)
42 }
43
44 #[cfg_attr(test, crate::durable)]
46 pub async fn propose_remove_member(
47 &mut self,
48 client: &Session<Database>,
49 provider: &MlsCryptoProvider,
50 database: &Database,
51 member: LeafNodeIndex,
52 ) -> Result<MlsProposalBundle> {
53 let signer = &self
54 .find_current_credential(client)
55 .await
56 .map_err(|_| Error::IdentityInitializationError)?
57 .signature_key_pair;
58 let proposal = self
59 .group
60 .propose_remove_member(provider, signer, member)
61 .map_err(MlsError::wrap("propose remove member"))
62 .map(MlsProposalBundle::from)?;
63 self.persist_group_when_changed(database, false).await?;
64 Ok(proposal)
65 }
66
67 #[cfg_attr(test, crate::durable)]
69 pub async fn propose_self_update(
70 &mut self,
71 client: &Session<Database>,
72 provider: &MlsCryptoProvider,
73 database: &Database,
74 ) -> Result<MlsProposalBundle> {
75 self.propose_explicit_self_update(client, provider, database, None)
76 .await
77 }
78
79 #[cfg_attr(test, crate::durable)]
81 pub async fn propose_explicit_self_update(
82 &mut self,
83 client: &Session<Database>,
84 backend: &MlsCryptoProvider,
85 database: &Database,
86 leaf_node: Option<LeafNode>,
87 ) -> Result<MlsProposalBundle> {
88 let msg_signer = &self
89 .find_current_credential(client)
90 .await
91 .map_err(|_| Error::IdentityInitializationError)?
92 .signature_key_pair;
93
94 let proposal = if let Some(own_leaf) = leaf_node {
95 let credential = self.find_credential_for_leaf_node(client, &own_leaf).await?;
96 self.group
97 .propose_explicit_self_update(backend, msg_signer, own_leaf, credential.signature_key())
98 .await
99 } else {
100 self.group.propose_self_update(backend, msg_signer).await
101 }
102 .map(MlsProposalBundle::from)
103 .map_err(MlsError::wrap("proposing explicit self update"))?;
104
105 self.persist_group_when_changed(database, false).await?;
106 Ok(proposal)
107 }
108}
109
110#[derive(Debug)]
112pub struct MlsProposalBundle {
113 pub proposal: MlsMessageOut,
115 pub proposal_ref: MlsProposalRef,
117}
118
119impl From<(MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)> for MlsProposalBundle {
120 fn from((proposal, proposal_ref): (MlsMessageOut, openmls::prelude::hash_ref::ProposalRef)) -> Self {
121 Self {
122 proposal,
123 proposal_ref: proposal_ref.into(),
124 }
125 }
126}
127
128impl MlsProposalBundle {
129 #[allow(clippy::type_complexity)]
133 pub fn to_bytes(self) -> Result<(Vec<u8>, Vec<u8>)> {
134 use openmls::prelude::TlsSerializeTrait as _;
135 let proposal = self
136 .proposal
137 .tls_serialize_detached()
138 .map_err(Error::tls_serialize("proposal"))?;
139 let proposal_ref = self.proposal_ref.to_bytes();
140
141 Ok((proposal, proposal_ref))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use itertools::Itertools;
148
149 use super::*;
150 use crate::{mls::conversation::ConversationWithMls as _, test_utils::*};
151
152 mod propose_add_members {
153 use super::*;
154
155 #[apply(all_cred_cipher)]
156 async fn can_propose_adding_members_to_conversation(case: TestContext) {
157 let [alice, bob, charlie] = case.sessions().await;
158 Box::pin(async move {
159 let conversation = case.create_conversation([&alice, &bob]).await;
160 assert!(!conversation.has_pending_proposals().await);
161
162 let proposal_guard = conversation.invite_proposal(&charlie).await;
163 assert_eq!(proposal_guard.conversation().pending_proposal_count().await, 1);
164 let commit_guard = proposal_guard
165 .notify_members()
166 .await
167 .acting_as(&bob)
168 .await
169 .commit_pending_proposals()
170 .await;
171 assert_eq!(commit_guard.conversation().members_counted_by(&bob).await, 3);
172 assert_eq!(commit_guard.conversation().members_counted_by(&alice).await, 2);
173
174 let conversation = commit_guard.notify_members().await;
177 assert_eq!(conversation.member_count().await, 3);
178 assert!(conversation.is_functional_and_contains([&alice, &bob, &charlie]).await)
179 })
180 .await
181 }
182 }
183
184 mod propose_remove_members {
185 use super::*;
186
187 #[apply(all_cred_cipher)]
188 async fn can_propose_removing_members_from_conversation(case: TestContext) {
189 let [alice, bob, charlie] = case.sessions().await;
190 Box::pin(async move {
191 let conversation = case.create_conversation([&alice, &bob, &charlie]).await;
192
193 assert!(!conversation.has_pending_proposals().await);
194 let proposal_guard = conversation.remove_proposal(&charlie).await;
195 assert_eq!(proposal_guard.conversation().pending_proposal_count().await, 1);
196 let conversation = proposal_guard
197 .notify_members()
198 .await
199 .acting_as(&bob)
200 .await
201 .commit_pending_proposals_notify()
202 .await;
203 assert_eq!(conversation.member_count().await, 2);
204 assert!(conversation.is_functional_and_contains([&alice, &bob]).await)
205 })
206 .await
207 }
208 }
209
210 mod propose_self_update {
211 use super::*;
212
213 #[apply(all_cred_cipher)]
214 async fn can_propose_updating(case: TestContext) {
215 let [alice, bob] = case.sessions().await;
216 Box::pin(async move {
217 let conversation = case.create_conversation([&alice, &bob]).await;
218
219 let bob_keys = conversation
220 .guard_of(&bob)
221 .await
222 .conversation()
223 .await
224 .signature_keys()
225 .collect::<Vec<_>>();
226 let alice_keys = conversation
227 .guard()
228 .await
229 .conversation()
230 .await
231 .signature_keys()
232 .collect::<Vec<_>>();
233 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
234 let alice_key = conversation.encryption_public_key().await;
235
236 let commit_guard = conversation
237 .update_proposal_notify()
238 .await
239 .acting_as(&bob)
240 .await
241 .commit_pending_proposals()
242 .await;
243
244 let conversation = commit_guard.conversation();
245
246 assert!(
247 !conversation
248 .guard_of(&bob)
249 .await
250 .conversation()
251 .await
252 .encryption_keys()
253 .contains(&alice_key)
254 );
255
256 assert!(
257 conversation
258 .guard_of(&alice)
259 .await
260 .conversation()
261 .await
262 .encryption_keys()
263 .contains(&alice_key)
264 );
265 let conversation = commit_guard.notify_members().await;
268 assert!(
269 !conversation
270 .guard_of(&alice)
271 .await
272 .conversation()
273 .await
274 .encryption_keys()
275 .contains(&alice_key)
276 );
277
278 assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
280 })
281 .await;
282 }
283 }
284
285 mod delivery_semantics {
286 use super::*;
287
288 #[apply(all_cred_cipher)]
289 async fn should_prevent_out_of_order_proposals(case: TestContext) {
290 let [alice, bob] = case.sessions().await;
291 Box::pin(async move {
292 let conversation = case.create_conversation([&alice, &bob]).await;
293 let id = conversation.id().clone();
294
295 let proposal_guard = conversation.update_proposal().await;
296 let proposal = proposal_guard.message();
297 proposal_guard
298 .notify_members()
299 .await
300 .acting_as(&bob)
301 .await
302 .commit_pending_proposals_notify()
303 .await;
304 let past_proposal = bob
308 .transaction
309 .conversation(&id)
310 .await
311 .unwrap()
312 .decrypt_message(&proposal.to_bytes().unwrap())
313 .await;
314 assert!(matches!(past_proposal.unwrap_err(), Error::StaleProposal));
315 })
316 .await;
317 }
318 }
319}