core_crypto/mls/conversation/
renew.rs

1use core_crypto_keystore::entities::MlsEncryptionKeyPair;
2use openmls::prelude::{LeafNode, LeafNodeIndex, Proposal, QueuedProposal, Sender, StagedCommit};
3use openmls_traits::OpenMlsCryptoProvider;
4
5use mls_crypto_provider::MlsCryptoProvider;
6
7use crate::prelude::{Client, CryptoError, CryptoResult, MlsConversation, MlsProposalBundle};
8
9/// Marker struct holding methods responsible for restoring (renewing) proposals (or pending commit)
10/// in case another commit has been accepted by the backend instead of ours
11pub(crate) struct Renew;
12
13impl Renew {
14    /// Renews proposals:
15    /// * in pending_proposals but not in valid commit
16    /// * in pending_commit but not in valid commit
17    ///
18    /// NB: we do not deal with partial commit (commit which do not contain all pending proposals)
19    /// because they cannot be created at the moment by core-crypto
20    ///
21    /// * `self_index` - own client [KeyPackageRef] in current MLS group
22    /// * `pending_proposals` - local pending proposals in group's proposal store
23    /// * `pending_commit` - local pending commit which is now invalid
24    /// * `valid_commit` - commit accepted by the backend which will now supersede our local pending commit
25    pub(crate) fn renew<'a>(
26        self_index: &LeafNodeIndex,
27        pending_proposals: impl Iterator<Item = QueuedProposal> + 'a,
28        pending_commit: Option<&'a StagedCommit>,
29        valid_commit: &'a StagedCommit,
30    ) -> (Vec<QueuedProposal>, bool) {
31        // indicates if we need to renew an update proposal.
32        // true only if we have an empty pending commit or the valid commit does not contain one of our update proposal
33        // otherwise, local orphan update proposal will be renewed regularly, without this flag
34        let mut needs_update = false;
35
36        let renewed_pending_proposals = if let Some(pending_commit) = pending_commit {
37            // present in pending commit but not in valid commit
38            let commit_proposals = pending_commit.queued_proposals().cloned().collect::<Vec<_>>();
39
40            // if our own pending commit is empty it means we were attempting to update
41            let empty_commit = commit_proposals.is_empty();
42
43            // does the valid commit contains one of our update proposal ?
44            let valid_commit_has_own_update_proposal = valid_commit.update_proposals().any(|p| match p.sender() {
45                Sender::Member(sender_index) => self_index == sender_index,
46                _ => false,
47            });
48
49            // do we need to renew the update or has it already been committed
50            needs_update = !valid_commit_has_own_update_proposal && empty_commit;
51
52            // local proposals present in local pending commit but not in valid commit
53            commit_proposals
54                .into_iter()
55                .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
56                .collect::<Vec<_>>()
57        } else {
58            // local pending proposals present locally but not in valid commit
59            pending_proposals
60                .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
61                .collect::<Vec<_>>()
62        };
63        (renewed_pending_proposals, needs_update)
64    }
65
66    /// A proposal has to be renewed if it is absent from supplied commit
67    fn is_proposal_renewable(proposal: QueuedProposal, commit: Option<&StagedCommit>) -> Option<QueuedProposal> {
68        if let Some(commit) = commit {
69            let in_commit = match proposal.proposal() {
70                Proposal::Add(ref add) => commit.add_proposals().any(|p| {
71                    let commits_identity = p.add_proposal().key_package().leaf_node().credential().identity();
72                    let proposal_identity = add.key_package().leaf_node().credential().identity();
73                    commits_identity == proposal_identity
74                }),
75                Proposal::Remove(ref remove) => commit
76                    .remove_proposals()
77                    .any(|p| p.remove_proposal().removed() == remove.removed()),
78                Proposal::Update(ref update) => commit
79                    .update_proposals()
80                    .any(|p| p.update_proposal().leaf_node() == update.leaf_node()),
81                _ => true,
82            };
83            if in_commit {
84                None
85            } else {
86                Some(proposal)
87            }
88        } else {
89            // if proposal is orphan (not present in commit)
90            Some(proposal)
91        }
92    }
93}
94
95impl MlsConversation {
96    /// Given the proposals to renew, actually restore them by using associated methods in [MlsGroup].
97    /// This will also add them to the local proposal store
98    pub(crate) async fn renew_proposals_for_current_epoch(
99        &mut self,
100        client: &Client,
101        backend: &MlsCryptoProvider,
102        proposals: impl Iterator<Item = QueuedProposal>,
103        needs_update: bool,
104    ) -> CryptoResult<Vec<MlsProposalBundle>> {
105        let mut bundle = vec![];
106        let is_external = |p: &QueuedProposal| matches!(p.sender(), Sender::External(_) | Sender::NewMemberProposal);
107        let proposals = proposals.filter(|p| !is_external(p));
108        for proposal in proposals {
109            let msg = match proposal.proposal {
110                Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package.into()).await?,
111                Proposal::Remove(remove) => self.propose_remove_member(client, backend, remove.removed()).await?,
112                Proposal::Update(update) => self.renew_update(client, backend, Some(update.leaf_node())).await?,
113                _ => return Err(CryptoError::ImplementationError),
114            };
115            bundle.push(msg);
116        }
117        if needs_update {
118            let proposal = self.renew_update(client, backend, None).await?;
119            bundle.push(proposal);
120        }
121        Ok(bundle)
122    }
123
124    /// Renews an update proposal by considering the explicit LeafNode supplied in the proposal
125    /// by applying it to the current own LeafNode.
126    /// At this point, we have already verified we are only operating on proposals created by self.
127    async fn renew_update(
128        &mut self,
129        client: &Client,
130        backend: &MlsCryptoProvider,
131        leaf_node: Option<&LeafNode>,
132    ) -> CryptoResult<MlsProposalBundle> {
133        if let Some(leaf_node) = leaf_node {
134            // Creating an update rekeys the LeafNode everytime. Hence we need to clear the previous
135            // encryption key from the keystore otherwise we would have a leak
136            backend
137                .key_store()
138                .remove::<MlsEncryptionKeyPair, _>(leaf_node.encryption_key().as_slice())
139                .await?;
140        }
141
142        let mut leaf_node = leaf_node
143            .or_else(|| self.group.own_leaf())
144            .cloned()
145            .ok_or(CryptoError::InternalMlsError)?;
146
147        let sc = self.signature_scheme();
148        let ct = self.own_credential_type()?;
149        let cb = client.find_most_recent_credential_bundle(sc, ct).await?;
150
151        leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
152
153        self.propose_explicit_self_update(client, backend, Some(leaf_node))
154            .await
155    }
156
157    pub(crate) fn self_pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
158        self.group
159            .pending_proposals()
160            .filter(|&p| matches!(p.sender(), Sender::Member(i) if i == &self.group.own_leaf_index()))
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use wasm_bindgen_test::*;
167
168    use crate::test_utils::*;
169
170    mod update {
171        use super::*;
172
173        #[apply(all_cred_cipher)]
174        #[wasm_bindgen_test]
175        pub async fn renewable_when_created_by_self(case: TestCase) {
176            run_test_with_client_ids(
177                case.clone(),
178                ["alice", "bob"],
179                move |[mut alice_central, bob_central]| {
180                    Box::pin(async move {
181                        let id = conversation_id();
182                        alice_central
183                            .context
184                            .new_conversation(&id, case.credential_type, case.cfg.clone())
185                            .await
186                            .unwrap();
187                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
188
189                        assert!(alice_central.pending_proposals(&id).await.is_empty());
190                        alice_central.context.new_update_proposal(&id).await.unwrap();
191                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
192
193                        // Bob hasn't Alice's proposal but creates a commit
194                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
195                        bob_central.context.commit_accepted(&id).await.unwrap();
196
197                        let proposals = alice_central
198                            .context
199                            .decrypt_message(&id, commit.to_bytes().unwrap())
200                            .await
201                            .unwrap()
202                            .proposals;
203                        // Alice should renew the proposal because its hers
204                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
205                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
206
207                        // It should also renew the proposal when in pending_commit
208                        alice_central.context.commit_pending_proposals(&id).await.unwrap();
209                        assert!(alice_central.pending_commit(&id).await.is_some());
210                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
211                        let proposals = alice_central
212                            .context
213                            .decrypt_message(&id, commit.to_bytes().unwrap())
214                            .await
215                            .unwrap()
216                            .proposals;
217                        // Alice should renew the proposal because its hers
218                        // It should also replace existing one
219                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
220                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
221                    })
222                },
223            )
224            .await
225        }
226
227        #[apply(all_cred_cipher)]
228        #[wasm_bindgen_test]
229        pub async fn renews_pending_commit_when_created_by_self(case: TestCase) {
230            run_test_with_client_ids(
231                case.clone(),
232                ["alice", "bob"],
233                move |[mut alice_central, bob_central]| {
234                    Box::pin(async move {
235                        let id = conversation_id();
236                        alice_central
237                            .context
238                            .new_conversation(&id, case.credential_type, case.cfg.clone())
239                            .await
240                            .unwrap();
241                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
242
243                        alice_central.context.update_keying_material(&id).await.unwrap();
244                        assert!(alice_central.pending_commit(&id).await.is_some());
245
246                        // but Bob creates a commit meanwhile
247                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
248
249                        let proposals = alice_central
250                            .context
251                            .decrypt_message(&id, commit.to_bytes().unwrap())
252                            .await
253                            .unwrap()
254                            .proposals;
255                        // Alice should renew the proposal because its her's
256                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
257                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
258                    })
259                },
260            )
261            .await
262        }
263
264        #[apply(all_cred_cipher)]
265        #[wasm_bindgen_test]
266        pub async fn not_renewable_when_in_valid_commit(case: TestCase) {
267            run_test_with_client_ids(
268                case.clone(),
269                ["alice", "bob"],
270                move |[mut alice_central, bob_central]| {
271                    Box::pin(async move {
272                        let id = conversation_id();
273                        alice_central
274                            .context
275                            .new_conversation(&id, case.credential_type, case.cfg.clone())
276                            .await
277                            .unwrap();
278                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
279
280                        assert!(alice_central.pending_proposals(&id).await.is_empty());
281                        let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
282                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
283
284                        // Bob has Alice's update proposal
285                        bob_central
286                            .context
287                            .decrypt_message(&id, proposal.to_bytes().unwrap())
288                            .await
289                            .unwrap();
290
291                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
292                        bob_central.context.commit_accepted(&id).await.unwrap();
293
294                        // Bob's commit has Alice's proposal
295                        let proposals = alice_central
296                            .context
297                            .decrypt_message(&id, commit.to_bytes().unwrap())
298                            .await
299                            .unwrap()
300                            .proposals;
301                        // Alice proposal should not be renew as it was in valid commit
302                        assert!(alice_central.pending_proposals(&id).await.is_empty());
303                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
304
305                        // Same if proposal is also in pending commit
306                        let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
307                        alice_central.context.commit_pending_proposals(&id).await.unwrap();
308                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
309                        assert!(alice_central.pending_commit(&id).await.is_some());
310                        bob_central
311                            .context
312                            .decrypt_message(&id, proposal.to_bytes().unwrap())
313                            .await
314                            .unwrap();
315                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
316                        let proposals = alice_central
317                            .context
318                            .decrypt_message(&id, commit.to_bytes().unwrap())
319                            .await
320                            .unwrap()
321                            .proposals;
322                        // Alice should not be renew as it was in valid commit
323                        assert!(alice_central.pending_proposals(&id).await.is_empty());
324                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
325                    })
326                },
327            )
328            .await
329        }
330
331        #[apply(all_cred_cipher)]
332        #[wasm_bindgen_test]
333        pub async fn not_renewable_by_ref(case: TestCase) {
334            run_test_with_client_ids(
335                case.clone(),
336                ["alice", "bob", "charlie"],
337                move |[mut alice_central, bob_central, charlie_central]| {
338                    Box::pin(async move {
339                        let id = conversation_id();
340                        alice_central
341                            .context
342                            .new_conversation(&id, case.credential_type, case.cfg.clone())
343                            .await
344                            .unwrap();
345                        alice_central
346                            .invite_all(&case, &id, [&bob_central, &charlie_central])
347                            .await
348                            .unwrap();
349
350                        let proposal = bob_central.context.new_update_proposal(&id).await.unwrap().proposal;
351                        assert!(alice_central.pending_proposals(&id).await.is_empty());
352                        alice_central
353                            .context
354                            .decrypt_message(&id, proposal.to_bytes().unwrap())
355                            .await
356                            .unwrap();
357                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
358
359                        // Charlie does not have other proposals, it creates a commit
360                        let commit = charlie_central
361                            .context
362                            .update_keying_material(&id)
363                            .await
364                            .unwrap()
365                            .commit;
366                        let proposals = alice_central
367                            .context
368                            .decrypt_message(&id, commit.to_bytes().unwrap())
369                            .await
370                            .unwrap()
371                            .proposals;
372                        // Alice should not renew Bob's update proposal
373                        assert!(alice_central.pending_proposals(&id).await.is_empty());
374                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
375                    })
376                },
377            )
378            .await
379        }
380    }
381
382    mod add {
383        use super::*;
384
385        #[apply(all_cred_cipher)]
386        #[wasm_bindgen_test]
387        pub async fn not_renewable_when_valid_commit_adds_same(case: TestCase) {
388            run_test_with_client_ids(
389                case.clone(),
390                ["alice", "bob", "charlie"],
391                move |[mut alice_central, bob_central, charlie_central]| {
392                    Box::pin(async move {
393                        let id = conversation_id();
394                        alice_central
395                            .context
396                            .new_conversation(&id, case.credential_type, case.cfg.clone())
397                            .await
398                            .unwrap();
399                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
400
401                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
402                        assert!(alice_central.pending_proposals(&id).await.is_empty());
403                        alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
404                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
405
406                        let charlie = charlie_central.rand_key_package(&case).await;
407                        let commit = bob_central
408                            .context
409                            .add_members_to_conversation(&id, vec![charlie])
410                            .await
411                            .unwrap()
412                            .commit;
413                        let proposals = alice_central
414                            .context
415                            .decrypt_message(&id, commit.to_bytes().unwrap())
416                            .await
417                            .unwrap()
418                            .proposals;
419                        // Alice proposal is not renewed since she also wanted to add Charlie
420                        assert!(alice_central.pending_proposals(&id).await.is_empty());
421                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
422                    })
423                },
424            )
425            .await
426        }
427
428        #[apply(all_cred_cipher)]
429        #[wasm_bindgen_test]
430        pub async fn not_renewable_in_pending_commit_when_valid_commit_adds_same(case: TestCase) {
431            run_test_with_client_ids(
432                case.clone(),
433                ["alice", "bob", "charlie"],
434                move |[mut alice_central, bob_central, charlie_central]| {
435                    Box::pin(async move {
436                        let id = conversation_id();
437                        alice_central
438                            .context
439                            .new_conversation(&id, case.credential_type, case.cfg.clone())
440                            .await
441                            .unwrap();
442                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
443
444                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
445                        assert!(alice_central.pending_proposals(&id).await.is_empty());
446                        alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
447                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
448
449                        // Here Alice also creates a commit
450                        alice_central.context.commit_pending_proposals(&id).await.unwrap();
451                        assert!(alice_central.pending_commit(&id).await.is_some());
452
453                        let charlie = charlie_central.rand_key_package(&case).await;
454                        let commit = bob_central
455                            .context
456                            .add_members_to_conversation(&id, vec![charlie])
457                            .await
458                            .unwrap()
459                            .commit;
460                        let proposals = alice_central
461                            .context
462                            .decrypt_message(&id, commit.to_bytes().unwrap())
463                            .await
464                            .unwrap()
465                            .proposals;
466                        // Alice proposal is not renewed since she also wanted to add Charlie
467                        assert!(alice_central.pending_proposals(&id).await.is_empty());
468                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
469                    })
470                },
471            )
472            .await
473        }
474
475        #[apply(all_cred_cipher)]
476        #[wasm_bindgen_test]
477        pub async fn not_renewable_by_ref(case: TestCase) {
478            run_test_with_client_ids(
479                case.clone(),
480                ["alice", "bob", "charlie", "debbie"],
481                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
482                    Box::pin(async move {
483                        let id = conversation_id();
484                        alice_central
485                            .context
486                            .new_conversation(&id, case.credential_type, case.cfg.clone())
487                            .await
488                            .unwrap();
489                        alice_central
490                            .invite_all(&case, &id, [&bob_central, &charlie_central])
491                            .await
492                            .unwrap();
493
494                        // Bob will propose adding Debbie
495                        let debbie_kp = debbie_central.get_one_key_package(&case).await;
496                        let proposal = bob_central
497                            .context
498                            .new_add_proposal(&id, debbie_kp)
499                            .await
500                            .unwrap()
501                            .proposal;
502                        alice_central
503                            .context
504                            .decrypt_message(&id, proposal.to_bytes().unwrap())
505                            .await
506                            .unwrap();
507                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
508
509                        // But Charlie will commit meanwhile
510                        let commit = charlie_central
511                            .context
512                            .update_keying_material(&id)
513                            .await
514                            .unwrap()
515                            .commit;
516                        let proposals = alice_central
517                            .context
518                            .decrypt_message(&id, commit.to_bytes().unwrap())
519                            .await
520                            .unwrap()
521                            .proposals;
522                        // which Alice should not renew since it's not hers
523                        assert!(alice_central.pending_proposals(&id).await.is_empty());
524                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
525                    })
526                },
527            )
528            .await
529        }
530
531        #[apply(all_cred_cipher)]
532        #[wasm_bindgen_test]
533        pub async fn renewable_when_valid_commit_doesnt_adds_same(case: TestCase) {
534            run_test_with_client_ids(
535                case.clone(),
536                ["alice", "bob", "charlie"],
537                move |[mut alice_central, bob_central, charlie_central]| {
538                    Box::pin(async move {
539                        let id = conversation_id();
540                        alice_central
541                            .context
542                            .new_conversation(&id, case.credential_type, case.cfg.clone())
543                            .await
544                            .unwrap();
545                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
546
547                        // Alice proposes adding Charlie
548                        let charlie_kp = charlie_central.get_one_key_package(&case).await;
549                        assert!(alice_central.pending_proposals(&id).await.is_empty());
550                        alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
551                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
552
553                        // But meanwhile Bob will create a commit without Alice's proposal
554                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
555                        bob_central.context.commit_accepted(&id).await.unwrap();
556                        let proposals = alice_central
557                            .context
558                            .decrypt_message(&id, commit.to_bytes().unwrap())
559                            .await
560                            .unwrap()
561                            .proposals;
562                        // So Alice proposal should be renewed
563                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
564                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
565
566                        // And same should happen when proposal is in pending commit
567                        alice_central.context.commit_pending_proposals(&id).await.unwrap();
568                        assert!(alice_central.pending_commit(&id).await.is_some());
569                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
570                        let proposals = alice_central
571                            .context
572                            .decrypt_message(&id, commit.to_bytes().unwrap())
573                            .await
574                            .unwrap()
575                            .proposals;
576                        // So Alice proposal should also be renewed
577                        // It should also replace existing one
578                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
579                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
580                    })
581                },
582            )
583            .await
584        }
585
586        #[apply(all_cred_cipher)]
587        #[wasm_bindgen_test]
588        pub async fn renews_pending_commit_when_valid_commit_doesnt_add_same(case: TestCase) {
589            run_test_with_client_ids(
590                case.clone(),
591                ["alice", "bob", "charlie"],
592                move |[mut alice_central, bob_central, charlie_central]| {
593                    Box::pin(async move {
594                        let id = conversation_id();
595                        alice_central
596                            .context
597                            .new_conversation(&id, case.credential_type, case.cfg.clone())
598                            .await
599                            .unwrap();
600                        alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
601
602                        // Alice commits adding Charlie
603                        let charlie = charlie_central.rand_key_package(&case).await;
604                        alice_central
605                            .context
606                            .add_members_to_conversation(&id, vec![charlie])
607                            .await
608                            .unwrap();
609                        assert!(alice_central.pending_commit(&id).await.is_some());
610
611                        // But meanwhile Bob will create a commit
612                        let commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
613                        let proposals = alice_central
614                            .context
615                            .decrypt_message(&id, commit.to_bytes().unwrap())
616                            .await
617                            .unwrap()
618                            .proposals;
619                        // So Alice proposal should be renewed
620                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
621                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
622                    })
623                },
624            )
625            .await
626        }
627    }
628
629    mod remove {
630        use super::*;
631
632        #[apply(all_cred_cipher)]
633        #[wasm_bindgen_test]
634        pub async fn not_renewable_when_valid_commit_removes_same(case: TestCase) {
635            run_test_with_client_ids(
636                case.clone(),
637                ["alice", "bob", "charlie"],
638                move |[mut alice_central, bob_central, charlie_central]| {
639                    Box::pin(async move {
640                        let id = conversation_id();
641                        alice_central
642                            .context
643                            .new_conversation(&id, case.credential_type, case.cfg.clone())
644                            .await
645                            .unwrap();
646                        alice_central
647                            .invite_all(&case, &id, [&bob_central, &charlie_central])
648                            .await
649                            .unwrap();
650
651                        assert!(alice_central.pending_proposals(&id).await.is_empty());
652                        alice_central
653                            .context
654                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
655                            .await
656                            .unwrap();
657                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
658
659                        let commit = bob_central
660                            .context
661                            .remove_members_from_conversation(&id, &[charlie_central.get_client_id().await])
662                            .await
663                            .unwrap()
664                            .commit;
665                        let proposals = alice_central
666                            .context
667                            .decrypt_message(&id, commit.to_bytes().unwrap())
668                            .await
669                            .unwrap()
670                            .proposals;
671                        // Remove proposal is not renewed since commit does same
672                        assert!(alice_central.pending_proposals(&id).await.is_empty());
673                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
674                    })
675                },
676            )
677            .await
678        }
679
680        #[apply(all_cred_cipher)]
681        #[wasm_bindgen_test]
682        pub async fn not_renewable_by_ref(case: TestCase) {
683            run_test_with_client_ids(
684                case.clone(),
685                ["alice", "bob", "charlie"],
686                move |[mut alice_central, bob_central, charlie_central]| {
687                    Box::pin(async move {
688                        let id = conversation_id();
689                        alice_central
690                            .context
691                            .new_conversation(&id, case.credential_type, case.cfg.clone())
692                            .await
693                            .unwrap();
694                        alice_central
695                            .invite_all(&case, &id, [&bob_central, &charlie_central])
696                            .await
697                            .unwrap();
698
699                        let proposal = bob_central
700                            .context
701                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
702                            .await
703                            .unwrap()
704                            .proposal;
705                        assert!(alice_central.pending_proposals(&id).await.is_empty());
706                        alice_central
707                            .context
708                            .decrypt_message(&id, proposal.to_bytes().unwrap())
709                            .await
710                            .unwrap();
711                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
712
713                        let commit = charlie_central
714                            .context
715                            .update_keying_material(&id)
716                            .await
717                            .unwrap()
718                            .commit;
719                        let proposals = alice_central
720                            .context
721                            .decrypt_message(&id, commit.to_bytes().unwrap())
722                            .await
723                            .unwrap()
724                            .proposals;
725                        // Remove proposal is not renewed since by ref
726                        assert!(alice_central.pending_proposals(&id).await.is_empty());
727                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
728                    })
729                },
730            )
731            .await
732        }
733
734        #[apply(all_cred_cipher)]
735        #[wasm_bindgen_test]
736        pub async fn renewable_when_valid_commit_doesnt_remove_same(case: TestCase) {
737            run_test_with_client_ids(
738                case.clone(),
739                ["alice", "bob", "charlie", "debbie"],
740                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
741                    Box::pin(async move {
742                        let id = conversation_id();
743                        alice_central
744                            .context
745                            .new_conversation(&id, case.credential_type, case.cfg.clone())
746                            .await
747                            .unwrap();
748                        alice_central
749                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
750                            .await
751                            .unwrap();
752
753                        // Alice wants to remove Charlie
754                        assert!(alice_central.pending_proposals(&id).await.is_empty());
755                        alice_central
756                            .context
757                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
758                            .await
759                            .unwrap();
760                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
761
762                        // Whereas Bob wants to remove Debbie
763                        let commit = bob_central
764                            .context
765                            .remove_members_from_conversation(&id, &[debbie_central.get_client_id().await])
766                            .await
767                            .unwrap()
768                            .commit;
769                        let proposals = alice_central
770                            .context
771                            .decrypt_message(&id, commit.to_bytes().unwrap())
772                            .await
773                            .unwrap()
774                            .proposals;
775                        // Remove is renewed since valid commit removes another
776                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
777                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
778                    })
779                },
780            )
781            .await
782        }
783
784        #[apply(all_cred_cipher)]
785        #[wasm_bindgen_test]
786        pub async fn renews_pending_commit_when_commit_doesnt_remove_same(case: TestCase) {
787            run_test_with_client_ids(
788                case.clone(),
789                ["alice", "bob", "charlie", "debbie"],
790                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
791                    Box::pin(async move {
792                        let id = conversation_id();
793                        alice_central
794                            .context
795                            .new_conversation(&id, case.credential_type, case.cfg.clone())
796                            .await
797                            .unwrap();
798                        alice_central
799                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
800                            .await
801                            .unwrap();
802
803                        // Alice wants to remove Charlie
804                        alice_central
805                            .context
806                            .remove_members_from_conversation(&id, &[charlie_central.get_client_id().await])
807                            .await
808                            .unwrap();
809                        assert!(alice_central.pending_commit(&id).await.is_some());
810
811                        // Whereas Bob wants to remove Debbie
812                        let commit = bob_central
813                            .context
814                            .remove_members_from_conversation(&id, &[debbie_central.get_client_id().await])
815                            .await
816                            .unwrap()
817                            .commit;
818                        let proposals = alice_central
819                            .context
820                            .decrypt_message(&id, commit.to_bytes().unwrap())
821                            .await
822                            .unwrap()
823                            .proposals;
824                        // Remove is renewed since valid commit removes another
825                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
826                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
827                    })
828                },
829            )
830            .await
831        }
832
833        #[apply(all_cred_cipher)]
834        #[wasm_bindgen_test]
835        pub async fn renews_pending_commit_from_proposal_when_commit_doesnt_remove_same(case: TestCase) {
836            run_test_with_client_ids(
837                case.clone(),
838                ["alice", "bob", "charlie", "debbie"],
839                move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
840                    Box::pin(async move {
841                        let id = conversation_id();
842                        alice_central
843                            .context
844                            .new_conversation(&id, case.credential_type, case.cfg.clone())
845                            .await
846                            .unwrap();
847                        alice_central
848                            .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
849                            .await
850                            .unwrap();
851
852                        // Alice wants to remove Charlie
853                        alice_central
854                            .context
855                            .new_remove_proposal(&id, charlie_central.get_client_id().await)
856                            .await
857                            .unwrap();
858                        alice_central.context.commit_pending_proposals(&id).await.unwrap();
859                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
860                        assert!(alice_central.pending_commit(&id).await.is_some());
861
862                        // Whereas Bob wants to remove Debbie
863                        let commit = bob_central
864                            .context
865                            .remove_members_from_conversation(&id, &[debbie_central.get_client_id().await])
866                            .await
867                            .unwrap()
868                            .commit;
869                        let proposals = alice_central
870                            .context
871                            .decrypt_message(&id, commit.to_bytes().unwrap())
872                            .await
873                            .unwrap()
874                            .proposals;
875                        // Remove is renewed since valid commit removes another
876                        assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
877                        assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
878                    })
879                },
880            )
881            .await
882        }
883    }
884}