core_crypto/mls/conversation/
mod.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//! MLS groups (aka conversation) are the actual entities cementing all the participants in a
18//! conversation.
19//!
20//! This table summarizes what operations are permitted on a group depending its state:
21//! *(PP=pending proposal, PC=pending commit)*
22//!
23//! | can I ?   | 0 PP / 0 PC | 1+ PP / 0 PC | 0 PP / 1 PC | 1+ PP / 1 PC |
24//! |-----------|-------------|--------------|-------------|--------------|
25//! | encrypt   | ✅           | ❌            | ❌           | ❌            |
26//! | handshake | ✅           | ✅            | ❌           | ❌            |
27//! | merge     | ❌           | ❌            | ✅           | ✅            |
28//! | decrypt   | ✅           | ✅            | ✅           | ✅            |
29
30use std::collections::HashMap;
31
32use openmls::prelude::{CredentialWithKey, SignaturePublicKey};
33use openmls::{group::MlsGroup, prelude::Credential};
34use openmls_traits::types::SignatureScheme;
35
36use core_crypto_keystore::{Connection, CryptoKeystoreMls};
37use mls_crypto_provider::{CryptoKeystore, MlsCryptoProvider};
38
39use config::MlsConversationConfiguration;
40
41use crate::{
42    group_store::{GroupStore, GroupStoreValue},
43    mls::{client::Client, MlsCentral},
44    prelude::{CryptoError, CryptoResult, MlsCiphersuite, MlsCredentialType, MlsError},
45};
46
47use crate::context::CentralContext;
48
49mod buffer_messages;
50pub(crate) mod commit;
51mod commit_delay;
52pub(crate) mod config;
53#[cfg(test)]
54mod db_count;
55pub mod decrypt;
56mod duplicate;
57#[cfg(test)]
58mod durability;
59pub(crate) mod encrypt;
60pub(crate) mod export;
61pub(crate) mod external_sender;
62pub(crate) mod group_info;
63mod leaf_node_validation;
64pub(crate) mod merge;
65mod orphan_welcome;
66mod own_commit;
67pub(crate) mod proposal;
68mod renew;
69pub(crate) mod welcome;
70mod wipe;
71/// A unique identifier for a group/conversation. The identifier must be unique within a client.
72pub type ConversationId = Vec<u8>;
73
74/// This is a wrapper on top of the OpenMls's [MlsGroup], that provides Core Crypto specific functionality
75///
76/// This type will store the state of a group. With the [MlsGroup] it holds, it provides all
77/// operations that can be done in a group, such as creating proposals and commits.
78/// More information [here](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-general-setting)
79#[derive(Debug)]
80#[allow(dead_code)]
81pub struct MlsConversation {
82    pub(crate) id: ConversationId,
83    pub(crate) parent_id: Option<ConversationId>,
84    pub(crate) group: MlsGroup,
85    configuration: MlsConversationConfiguration,
86}
87
88impl MlsConversation {
89    /// Creates a new group/conversation
90    ///
91    /// # Arguments
92    /// * `id` - group/conversation identifier
93    /// * `author_client` - the client responsible for creating the group
94    /// * `creator_credential_type` - kind of credential the creator wants to join the group with
95    /// * `config` - group configuration
96    /// * `backend` - MLS Provider that will be used to persist the group
97    ///
98    /// # Errors
99    /// Errors can happen from OpenMls or from the KeyStore
100    pub async fn create(
101        id: ConversationId,
102        author_client: &Client,
103        creator_credential_type: MlsCredentialType,
104        configuration: MlsConversationConfiguration,
105        backend: &MlsCryptoProvider,
106    ) -> CryptoResult<Self> {
107        let (cs, ct) = (configuration.ciphersuite, creator_credential_type);
108        let cb = author_client
109            .get_most_recent_or_create_credential_bundle(backend, cs.signature_algorithm(), ct)
110            .await?;
111
112        let group = MlsGroup::new_with_group_id(
113            backend,
114            &cb.signature_key,
115            &configuration.as_openmls_default_configuration()?,
116            openmls::prelude::GroupId::from_slice(id.as_slice()),
117            cb.to_mls_credential_with_key(),
118        )
119        .await
120        .map_err(MlsError::from)?;
121
122        let mut conversation = Self {
123            id,
124            group,
125            parent_id: None,
126            configuration,
127        };
128
129        conversation
130            .persist_group_when_changed(&backend.keystore(), true)
131            .await?;
132
133        Ok(conversation)
134    }
135
136    /// Internal API: create a group from an existing conversation. For example by external commit
137    pub(crate) async fn from_mls_group(
138        group: MlsGroup,
139        configuration: MlsConversationConfiguration,
140        backend: &MlsCryptoProvider,
141    ) -> CryptoResult<Self> {
142        let id = ConversationId::from(group.group_id().as_slice());
143
144        let mut conversation = Self {
145            id,
146            group,
147            configuration,
148            parent_id: None,
149        };
150
151        conversation
152            .persist_group_when_changed(&backend.keystore(), true)
153            .await?;
154
155        Ok(conversation)
156    }
157
158    /// Internal API: restore the conversation from a persistence-saved serialized Group State.
159    pub(crate) fn from_serialized_state(buf: Vec<u8>, parent_id: Option<ConversationId>) -> CryptoResult<Self> {
160        let group: MlsGroup = core_crypto_keystore::deser(&buf)?;
161        let id = ConversationId::from(group.group_id().as_slice());
162        let configuration = MlsConversationConfiguration {
163            ciphersuite: group.ciphersuite().into(),
164            ..Default::default()
165        };
166
167        Ok(Self {
168            id,
169            group,
170            parent_id,
171            configuration,
172        })
173    }
174
175    /// Group/conversation id
176    pub fn id(&self) -> &ConversationId {
177        &self.id
178    }
179
180    /// Returns all members credentials from the group/conversation
181    pub fn members(&self) -> HashMap<Vec<u8>, Credential> {
182        self.group.members().fold(HashMap::new(), |mut acc, kp| {
183            let credential = kp.credential;
184            let id = credential.identity().to_vec();
185            acc.entry(id).or_insert(credential);
186            acc
187        })
188    }
189
190    /// Returns all members credentials with their signature public key from the group/conversation
191    pub fn members_with_key(&self) -> HashMap<Vec<u8>, CredentialWithKey> {
192        self.group.members().fold(HashMap::new(), |mut acc, kp| {
193            let credential = kp.credential;
194            let id = credential.identity().to_vec();
195            let signature_key = SignaturePublicKey::from(kp.signature_key);
196            let credential = CredentialWithKey {
197                credential,
198                signature_key,
199            };
200            acc.entry(id).or_insert(credential);
201            acc
202        })
203    }
204
205    pub(crate) async fn persist_group_when_changed(
206        &mut self,
207        keystore: &CryptoKeystore,
208        force: bool,
209    ) -> CryptoResult<()> {
210        if force || self.group.state_changed() == openmls::group::InnerState::Changed {
211            keystore
212                .mls_group_persist(
213                    &self.id,
214                    &core_crypto_keystore::ser(&self.group)?,
215                    self.parent_id.as_deref(),
216                )
217                .await?;
218
219            self.group.set_state(openmls::group::InnerState::Persisted);
220        }
221
222        Ok(())
223    }
224
225    /// Marks this conversation as child of another.
226    /// Prequisite: Being a member of this group and for it to be stored in the keystore
227    pub async fn mark_as_child_of(&mut self, parent_id: &ConversationId, keystore: &Connection) -> CryptoResult<()> {
228        if keystore.mls_group_exists(parent_id).await {
229            self.parent_id = Some(parent_id.clone());
230            self.persist_group_when_changed(keystore, true).await?;
231            Ok(())
232        } else {
233            Err(CryptoError::ParentGroupNotFound)
234        }
235    }
236
237    pub(crate) fn own_credential_type(&self) -> CryptoResult<MlsCredentialType> {
238        Ok(self
239            .group
240            .own_leaf_node()
241            .ok_or(CryptoError::InternalMlsError)?
242            .credential()
243            .credential_type()
244            .into())
245    }
246
247    pub(crate) fn ciphersuite(&self) -> MlsCiphersuite {
248        self.configuration.ciphersuite
249    }
250
251    pub(crate) fn signature_scheme(&self) -> SignatureScheme {
252        self.ciphersuite().signature_algorithm()
253    }
254}
255
256impl MlsCentral {
257    pub(crate) async fn get_conversation(&self, id: &ConversationId) -> CryptoResult<Option<MlsConversation>> {
258        GroupStore::fetch_from_keystore(id, &self.mls_backend.keystore(), None).await
259    }
260}
261
262impl CentralContext {
263    pub(crate) async fn get_conversation(&self, id: &ConversationId) -> CryptoResult<GroupStoreValue<MlsConversation>> {
264        let keystore = self.mls_provider().await?.keystore();
265        self.mls_groups()
266            .await?
267            .get_fetch(id, &keystore, None)
268            .await?
269            .ok_or_else(|| CryptoError::ConversationNotFound(id.clone()))
270    }
271
272    pub(crate) async fn get_parent_conversation(
273        &self,
274        conversation: &GroupStoreValue<MlsConversation>,
275    ) -> CryptoResult<Option<GroupStoreValue<MlsConversation>>> {
276        let conversation_lock = conversation.read().await;
277        if let Some(parent_id) = conversation_lock.parent_id.as_ref() {
278            Ok(Some(
279                self.get_conversation(parent_id)
280                    .await
281                    .map_err(|_| CryptoError::ParentGroupNotFound)?,
282            ))
283        } else {
284            Ok(None)
285        }
286    }
287
288    pub(crate) async fn get_all_conversations(&self) -> CryptoResult<Vec<GroupStoreValue<MlsConversation>>> {
289        let keystore = self.mls_provider().await?.keystore();
290        self.mls_groups().await?.get_fetch_all(&keystore).await
291    }
292
293    /// Mark a conversation as child of another one
294    /// This will affect the behavior of callbacks in particular
295    #[cfg_attr(test, crate::idempotent)]
296    pub async fn mark_conversation_as_child_of(
297        &self,
298        child_id: &ConversationId,
299        parent_id: &ConversationId,
300    ) -> CryptoResult<()> {
301        let conversation = self.get_conversation(child_id).await?;
302        conversation
303            .write()
304            .await
305            .mark_as_child_of(parent_id, &self.keystore().await?)
306            .await?;
307
308        Ok(())
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use crate::e2e_identity::rotate::tests::all::failsafe_ctx;
315
316    use wasm_bindgen_test::*;
317
318    use crate::{
319        prelude::{
320            ClientIdentifier, MlsCentralConfiguration, MlsConversationCreationMessage, INITIAL_KEYING_MATERIAL_COUNT,
321        },
322        test_utils::*,
323        CoreCrypto,
324    };
325
326    use super::*;
327
328    wasm_bindgen_test_configure!(run_in_browser);
329
330    #[apply(all_cred_cipher)]
331    #[wasm_bindgen_test]
332    pub async fn create_self_conversation_should_succeed(case: TestCase) {
333        run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
334            Box::pin(async move {
335                let id = conversation_id();
336                alice_central
337                    .context
338                    .new_conversation(&id, case.credential_type, case.cfg.clone())
339                    .await
340                    .unwrap();
341                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
342                assert_eq!(
343                    alice_central
344                        .get_conversation_unchecked(&id)
345                        .await
346                        .group
347                        .group_id()
348                        .as_slice(),
349                    id
350                );
351                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
352                let alice_can_send_message = alice_central.context.encrypt_message(&id, b"me").await;
353                assert!(alice_can_send_message.is_ok());
354            })
355        })
356        .await;
357    }
358
359    #[apply(all_cred_cipher)]
360    #[wasm_bindgen_test]
361    pub async fn create_1_1_conversation_should_succeed(case: TestCase) {
362        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
363            Box::pin(async move {
364                let id = conversation_id();
365
366                alice_central
367                    .context
368                    .new_conversation(&id, case.credential_type, case.cfg.clone())
369                    .await
370                    .unwrap();
371
372                let bob = bob_central.rand_key_package(&case).await;
373                let MlsConversationCreationMessage { welcome, .. } = alice_central
374                    .context
375                    .add_members_to_conversation(&id, vec![bob])
376                    .await
377                    .unwrap();
378                // before merging, commit is not applied
379                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
380                alice_central.context.commit_accepted(&id).await.unwrap();
381
382                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
383                assert_eq!(
384                    alice_central
385                        .get_conversation_unchecked(&id)
386                        .await
387                        .group
388                        .group_id()
389                        .as_slice(),
390                    id
391                );
392                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
393
394                bob_central
395                    .context
396                    .process_welcome_message(welcome.into(), case.custom_cfg())
397                    .await
398                    .unwrap();
399
400                assert_eq!(
401                    bob_central.get_conversation_unchecked(&id).await.id(),
402                    alice_central.get_conversation_unchecked(&id).await.id()
403                );
404                assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
405            })
406        })
407        .await;
408    }
409
410    #[apply(all_cred_cipher)]
411    #[wasm_bindgen_test]
412    pub async fn create_many_people_conversation(case: TestCase) {
413        run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
414            Box::pin(async move {
415                let x509_test_chain_arc = failsafe_ctx(&mut [&mut alice_central], case.signature_scheme()).await;
416                let x509_test_chain = x509_test_chain_arc.as_ref().as_ref().unwrap();
417
418                let id = conversation_id();
419                alice_central
420                    .context
421                    .new_conversation(&id, case.credential_type, case.cfg.clone())
422                    .await
423                    .unwrap();
424
425                let mut bob_and_friends: Vec<ClientContext> = Vec::with_capacity(GROUP_SAMPLE_SIZE);
426                for _ in 0..GROUP_SAMPLE_SIZE {
427                    let uuid = uuid::Uuid::new_v4();
428                    let name = uuid.hyphenated().to_string();
429                    let path = tmp_db_file();
430                    let config = MlsCentralConfiguration::try_new(
431                        path.0,
432                        name.clone(),
433                        None,
434                        vec![case.ciphersuite()],
435                        None,
436                        Some(INITIAL_KEYING_MATERIAL_COUNT),
437                    )
438                    .unwrap();
439                    let central = MlsCentral::try_new(config).await.unwrap();
440                    let cc = CoreCrypto::from(central);
441                    let friend_context = cc.new_transaction().await.unwrap();
442                    let central = cc.mls;
443
444                    x509_test_chain.register_with_central(&friend_context).await;
445
446                    let client_id: crate::prelude::ClientId = name.as_str().into();
447                    let identity = match case.credential_type {
448                        MlsCredentialType::Basic => ClientIdentifier::Basic(client_id),
449                        MlsCredentialType::X509 => {
450                            let x509_test_chain = alice_central
451                                .x509_test_chain
452                                .as_ref()
453                                .as_ref()
454                                .expect("No x509 test chain");
455                            let cert = crate::prelude::CertificateBundle::rand(
456                                &client_id,
457                                x509_test_chain.find_local_intermediate_ca(),
458                            );
459                            ClientIdentifier::X509(HashMap::from([(case.cfg.ciphersuite.signature_algorithm(), cert)]))
460                        }
461                    };
462                    friend_context
463                        .mls_init(
464                            identity,
465                            vec![case.cfg.ciphersuite],
466                            Some(INITIAL_KEYING_MATERIAL_COUNT),
467                        )
468                        .await
469                        .unwrap();
470
471                    let context = ClientContext {
472                        context: friend_context,
473                        central,
474                        x509_test_chain: x509_test_chain_arc.clone(),
475                    };
476                    bob_and_friends.push(context);
477                }
478
479                let number_of_friends = bob_and_friends.len();
480
481                let mut bob_and_friends_kps = vec![];
482                for c in &bob_and_friends {
483                    bob_and_friends_kps.push(c.rand_key_package(&case).await);
484                }
485
486                let MlsConversationCreationMessage { welcome, .. } = alice_central
487                    .context
488                    .add_members_to_conversation(&id, bob_and_friends_kps)
489                    .await
490                    .unwrap();
491                // before merging, commit is not applied
492                assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
493                alice_central.context.commit_accepted(&id).await.unwrap();
494
495                assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
496                assert_eq!(
497                    alice_central
498                        .get_conversation_unchecked(&id)
499                        .await
500                        .group
501                        .group_id()
502                        .as_slice(),
503                    id
504                );
505                assert_eq!(
506                    alice_central.get_conversation_unchecked(&id).await.members().len(),
507                    1 + number_of_friends
508                );
509
510                let mut bob_and_friends_groups = Vec::with_capacity(bob_and_friends.len());
511                // TODO: Do things in parallel, this is waaaaay too slow (takes around 5 minutes). Tracking issue: WPB-9624
512                for c in bob_and_friends {
513                    c.context
514                        .process_welcome_message(welcome.clone().into(), case.custom_cfg())
515                        .await
516                        .unwrap();
517                    assert!(c.try_talk_to(&id, &alice_central).await.is_ok());
518                    bob_and_friends_groups.push(c);
519                }
520
521                assert_eq!(bob_and_friends_groups.len(), GROUP_SAMPLE_SIZE);
522            })
523        })
524        .await;
525    }
526}