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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Application messages are actual text messages user exchange. In MLS they can only be encrypted.
//!
//! This table summarizes when a MLS group can create an application message:
//!
//! | can encrypt ?     | 0 pend. Commit | 1 pend. Commit |
//! |-------------------|----------------|----------------|
//! | 0 pend. Proposal  | ✅              | ❌              |
//! | 1+ pend. Proposal | ❌              | ❌              |

use mls_crypto_provider::MlsCryptoProvider;
use openmls::prelude::MlsMessageOutBody;

use super::MlsConversation;
use crate::context::CentralContext;
use crate::prelude::Client;
use crate::{mls::ConversationId, CryptoError, CryptoResult, MlsError};

/// Abstraction over a MLS group capable of encrypting a MLS message
impl MlsConversation {
    /// see [CentralContext::encrypt_message]
    /// It is durable because encrypting increments the message generation
    #[cfg_attr(test, crate::durable)]
    pub async fn encrypt_message(
        &mut self,
        client: &Client,
        message: impl AsRef<[u8]>,
        backend: &MlsCryptoProvider,
    ) -> CryptoResult<Vec<u8>> {
        let signer = &self
            .find_current_credential_bundle(client)
            .await
            .map_err(|_| CryptoError::IdentityInitializationError)?
            .signature_key;
        let encrypted = self
            .group
            .create_message(backend, signer, message.as_ref())
            .map_err(MlsError::from)?;

        // make sure all application messages are encrypted
        debug_assert!(matches!(encrypted.body, MlsMessageOutBody::PrivateMessage(_)));

        let encrypted = encrypted.to_bytes().map_err(MlsError::from)?;

        self.persist_group_when_changed(&backend.keystore(), false).await?;
        Ok(encrypted)
    }
}

impl CentralContext {
    /// Encrypts a raw payload then serializes it to the TLS wire format
    ///
    /// # Arguments
    /// * `conversation` - the group/conversation id
    /// * `message` - the message as a byte array
    ///
    /// # Return type
    /// This method will return an encrypted TLS serialized message.
    ///
    /// # Errors
    /// If the conversation can't be found, an error will be returned. Other errors are originating
    /// from OpenMls and the KeyStore
    #[cfg_attr(test, crate::idempotent)]
    pub async fn encrypt_message(
        &self,
        conversation: &ConversationId,
        message: impl AsRef<[u8]>,
    ) -> CryptoResult<Vec<u8>> {
        let client = self.mls_client().await?;
        self.get_conversation(conversation)
            .await?
            .write()
            .await
            .encrypt_message(&client, message, &self.mls_provider().await?)
            .await
    }
}

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

    use crate::test_utils::*;

    wasm_bindgen_test_configure!(run_in_browser);

    #[apply(all_cred_cipher)]
    #[wasm_bindgen_test]
    async fn can_encrypt_app_message(case: TestCase) {
        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
            Box::pin(async move {
                let id = conversation_id();
                alice_central
                    .context
                    .new_conversation(&id, case.credential_type, case.cfg.clone())
                    .await
                    .unwrap();
                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();

                let msg = b"Hello bob";
                let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
                assert_ne!(&msg[..], &encrypted[..]);
                let decrypted = bob_central
                    .context
                    .decrypt_message(&id, encrypted)
                    .await
                    .unwrap()
                    .app_msg
                    .unwrap();
                assert_eq!(&decrypted[..], &msg[..]);
            })
        })
        .await
    }

    // Ensures encrypting an application message is durable
    #[apply(all_cred_cipher)]
    #[wasm_bindgen_test]
    async fn can_encrypt_consecutive_messages(case: TestCase) {
        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
            Box::pin(async move {
                let id = conversation_id();
                alice_central
                    .context
                    .new_conversation(&id, case.credential_type, case.cfg.clone())
                    .await
                    .unwrap();
                alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();

                let msg = b"Hello bob";
                let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
                assert_ne!(&msg[..], &encrypted[..]);
                let decrypted = bob_central
                    .context
                    .decrypt_message(&id, encrypted)
                    .await
                    .unwrap()
                    .app_msg
                    .unwrap();
                assert_eq!(&decrypted[..], &msg[..]);

                let msg = b"Hello bob again";
                let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
                assert_ne!(&msg[..], &encrypted[..]);
                let decrypted = bob_central
                    .context
                    .decrypt_message(&id, encrypted)
                    .await
                    .unwrap()
                    .app_msg
                    .unwrap();
                assert_eq!(&decrypted[..], &msg[..]);
            })
        })
        .await
    }
}