core_crypto/mls/conversation/
export.rs

1// Wire
2// Copyright (C) 2022 Wire Swiss GmbH
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see http://www.gnu.org/licenses/.
16
17//! Primitives to export data from a group, such as derived keys and client ids.
18
19use crate::context::CentralContext;
20use crate::mls::{
21    client::id::ClientId, ConversationId, CryptoError, CryptoResult, MlsCentral, MlsConversation, MlsError,
22};
23
24impl MlsConversation {
25    const EXPORTER_LABEL: &'static str = "exporter";
26    const EXPORTER_CONTEXT: &'static [u8] = &[];
27
28    /// See [MlsCentral::export_secret_key]
29    pub fn export_secret_key(
30        &self,
31        backend: &impl openmls_traits::OpenMlsCryptoProvider,
32        key_length: usize,
33    ) -> CryptoResult<Vec<u8>> {
34        self.group
35            .export_secret(backend, Self::EXPORTER_LABEL, Self::EXPORTER_CONTEXT, key_length)
36            .map_err(MlsError::from)
37            .map_err(CryptoError::from)
38    }
39
40    /// See [MlsCentral::get_client_ids]
41    pub fn get_client_ids(&self) -> Vec<ClientId> {
42        self.group
43            .members()
44            .map(|kp| ClientId::from(kp.credential.identity()))
45            .collect()
46    }
47}
48
49impl CentralContext {
50    /// See [MlsCentral::export_secret_key]
51    #[cfg_attr(test, crate::idempotent)]
52    pub async fn export_secret_key(
53        &self,
54        conversation_id: &ConversationId,
55        key_length: usize,
56    ) -> CryptoResult<Vec<u8>> {
57        self.get_conversation(conversation_id)
58            .await?
59            .read()
60            .await
61            .export_secret_key(&self.mls_provider().await?, key_length)
62    }
63
64    /// See [MlsCentral::get_client_ids]
65    #[cfg_attr(test, crate::idempotent)]
66    pub async fn get_client_ids(&self, conversation_id: &ConversationId) -> CryptoResult<Vec<ClientId>> {
67        Ok(self
68            .get_conversation(conversation_id)
69            .await?
70            .read()
71            .await
72            .get_client_ids())
73    }
74}
75
76impl MlsCentral {
77    /// Derives a new key from the one in the group, allowing it to be use elsewehere.
78    ///
79    /// # Arguments
80    /// * `conversation_id` - the group/conversation id
81    /// * `key_length` - the length of the key to be derived. If the value is higher than the
82    ///   bounds of `u16` or the context hash * 255, an error will be returned
83    ///
84    /// # Errors
85    /// OpenMls secret generation error or conversation not found
86    #[cfg_attr(test, crate::idempotent)]
87    pub async fn export_secret_key(
88        &self,
89        conversation_id: &ConversationId,
90        key_length: usize,
91    ) -> CryptoResult<Vec<u8>> {
92        self.get_conversation(conversation_id)
93            .await?
94            .ok_or_else(|| CryptoError::ConversationNotFound(conversation_id.clone()))?
95            .export_secret_key(&self.mls_backend, key_length)
96    }
97
98    /// Exports the clients from a conversation
99    ///
100    /// # Arguments
101    /// * `conversation_id` - the group/conversation id
102    ///
103    /// # Errors
104    /// if the conversation can't be found
105    #[cfg_attr(test, crate::idempotent)]
106    pub async fn get_client_ids(&self, conversation_id: &ConversationId) -> CryptoResult<Vec<ClientId>> {
107        Ok(self
108            .get_conversation(conversation_id)
109            .await?
110            .ok_or_else(|| CryptoError::ConversationNotFound(conversation_id.clone()))?
111            .get_client_ids())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117
118    use crate::{
119        prelude::{CryptoError, MlsError},
120        test_utils::*,
121    };
122    use openmls::prelude::ExportSecretError;
123
124    use wasm_bindgen_test::*;
125
126    wasm_bindgen_test_configure!(run_in_browser);
127
128    mod export_secret {
129        use super::*;
130
131        #[apply(all_cred_cipher)]
132        #[wasm_bindgen_test]
133        pub async fn can_export_secret_key(case: TestCase) {
134            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
135                Box::pin(async move {
136                    let id = conversation_id();
137                    alice_central
138                        .context
139                        .new_conversation(&id, case.credential_type, case.cfg.clone())
140                        .await
141                        .unwrap();
142
143                    let key_length = 128;
144                    let result = alice_central.context.export_secret_key(&id, key_length).await;
145                    assert!(result.is_ok());
146                    assert_eq!(result.unwrap().len(), key_length);
147                })
148            })
149            .await
150        }
151
152        #[apply(all_cred_cipher)]
153        #[wasm_bindgen_test]
154        pub async fn cannot_export_secret_key_invalid_length(case: TestCase) {
155            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
156                Box::pin(async move {
157                    let id = conversation_id();
158                    alice_central
159                        .context
160                        .new_conversation(&id, case.credential_type, case.cfg.clone())
161                        .await
162                        .unwrap();
163
164                    let result = alice_central.context.export_secret_key(&id, usize::MAX).await;
165                    assert!(matches!(
166                        result.unwrap_err(),
167                        CryptoError::MlsError(MlsError::MlsExportSecretError(ExportSecretError::KeyLengthTooLong))
168                    ));
169                })
170            })
171            .await
172        }
173
174        #[apply(all_cred_cipher)]
175        #[wasm_bindgen_test]
176        pub async fn cannot_export_secret_key_not_found(case: TestCase) {
177            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
178                Box::pin(async move {
179                    let id = conversation_id();
180                    alice_central
181                        .context
182                        .new_conversation(&id, case.credential_type, case.cfg.clone())
183                        .await
184                        .unwrap();
185
186                    let unknown_id = b"not_found".to_vec();
187                    let error = alice_central.context.get_client_ids(&unknown_id).await.unwrap_err();
188                    assert!(matches!(error, CryptoError::ConversationNotFound(c) if c == unknown_id));
189                })
190            })
191            .await
192        }
193    }
194
195    mod get_client_ids {
196        use super::*;
197
198        #[apply(all_cred_cipher)]
199        #[wasm_bindgen_test]
200        pub async fn can_get_client_ids(case: TestCase) {
201            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
202                Box::pin(async move {
203                    let id = conversation_id();
204                    alice_central
205                        .context
206                        .new_conversation(&id, case.credential_type, case.cfg.clone())
207                        .await
208                        .unwrap();
209
210                    assert_eq!(alice_central.context.get_client_ids(&id).await.unwrap().len(), 1);
211
212                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
213                    assert_eq!(alice_central.context.get_client_ids(&id).await.unwrap().len(), 2);
214                })
215            })
216            .await
217        }
218
219        #[apply(all_cred_cipher)]
220        #[wasm_bindgen_test]
221        pub async fn cannot_get_client_ids_not_found(case: TestCase) {
222            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
223                Box::pin(async move {
224                    let id = conversation_id();
225                    alice_central
226                        .context
227                        .new_conversation(&id, case.credential_type, case.cfg.clone())
228                        .await
229                        .unwrap();
230
231                    let unknown_id = b"not_found".to_vec();
232                    let error = alice_central.context.get_client_ids(&unknown_id).await.unwrap_err();
233                    assert!(matches!(error, CryptoError::ConversationNotFound(c) if c == unknown_id));
234                })
235            })
236            .await
237        }
238    }
239}