core_crypto/mls/conversation/
wipe.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use super::Result;
use crate::{
    KeystoreError, MlsError, RecursiveError,
    context::CentralContext,
    prelude::{ConversationId, MlsConversation},
};
use core_crypto_keystore::CryptoKeystoreMls;
use mls_crypto_provider::MlsCryptoProvider;
use openmls_traits::OpenMlsCryptoProvider;

impl CentralContext {
    /// Destroys a group locally
    ///
    /// # Errors
    /// KeyStore errors, such as IO
    #[cfg_attr(test, crate::dispotent)]
    pub async fn wipe_conversation(&self, id: &ConversationId) -> Result<()> {
        let provider = self
            .mls_provider()
            .await
            .map_err(RecursiveError::root("getting mls provider"))?;
        self.get_conversation(id)
            .await?
            .write()
            .await
            .wipe_associated_entities(&provider)
            .await?;
        provider
            .key_store()
            .mls_group_delete(id)
            .await
            .map_err(KeystoreError::wrap("deleting mls group"))?;
        let _ = self
            .mls_groups()
            .await
            .map_err(RecursiveError::root("getting mls groups"))?
            .remove(id);
        Ok(())
    }
}

impl MlsConversation {
    async fn wipe_associated_entities(&mut self, backend: &MlsCryptoProvider) -> Result<()> {
        // the own client may or may not have generated an epoch keypair in the previous epoch
        // Since it is a terminal operation, ignoring the error is fine here.
        let _ = self.group.delete_previous_epoch_keypairs(backend).await;

        let pending_proposals = self.group.pending_proposals().cloned().collect::<Vec<_>>();
        for proposal in pending_proposals {
            // Update proposals rekey the own leaf node. Hence the associated encryption keypair has to be cleared
            self.group
                .remove_pending_proposal(backend.key_store(), proposal.proposal_reference())
                .await
                .map_err(MlsError::wrap("removing pending proposal"))?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use wasm_bindgen_test::*;

    use super::super::error::Error;
    use crate::test_utils::*;

    wasm_bindgen_test_configure!(run_in_browser);

    #[apply(all_cred_cipher)]
    #[wasm_bindgen_test]
    async fn can_wipe_group(case: TestCase) {
        run_test_with_central(case.clone(), move |[central]| {
            Box::pin(async move {
                let id = conversation_id();
                central
                    .context
                    .new_conversation(&id, case.credential_type, case.cfg.clone())
                    .await
                    .unwrap();
                assert!(central.get_conversation_unchecked(&id).await.group.is_active());

                central.context.wipe_conversation(&id).await.unwrap();
                assert!(!central.context.conversation_exists(&id).await.unwrap());
            })
        })
        .await;
    }

    #[apply(all_cred_cipher)]
    #[wasm_bindgen_test]
    async fn cannot_wipe_group_non_existent(case: TestCase) {
        use crate::LeafError;

        run_test_with_central(case.clone(), move |[central]| {
            Box::pin(async move {
                let id = conversation_id();
                let err = central.context.wipe_conversation(&id).await.unwrap_err();
                assert!(matches!(err, Error::Leaf(LeafError::ConversationNotFound(conv_id)) if conv_id == id));
            })
        })
        .await;
    }

    // should delete anything related to this conversation
    #[apply(all_cred_cipher)]
    #[wasm_bindgen_test]
    async fn should_cascade_deletion(case: TestCase) {
        run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
            Box::pin(async move {
                let id = conversation_id();
                cc.context
                    .new_conversation(&id, case.credential_type, case.cfg.clone())
                    .await
                    .unwrap();
                let initial_count = cc.context.count_entities().await;

                cc.context.new_update_proposal(&id).await.unwrap();
                let post_proposal_count = cc.context.count_entities().await;
                assert_eq!(
                    post_proposal_count.encryption_keypair,
                    initial_count.encryption_keypair + 1
                );

                cc.context.wipe_conversation(&id).await.unwrap();

                let final_count = cc.context.count_entities().await;
                assert_eq!(final_count.group, 0);
                assert_eq!(final_count.encryption_keypair, final_count.key_package);
                assert_eq!(final_count.epoch_encryption_keypair, 0);
            })
        })
        .await
    }
}