core_crypto/mls/conversation/
welcome.rs

1use std::borrow::BorrowMut;
2
3use crate::context::CentralContext;
4use crate::{
5    e2e_identity::init_certificates::NewCrlDistributionPoint,
6    group_store::GroupStore,
7    mls::credential::crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
8    prelude::{
9        ConversationId, CryptoError, CryptoResult, MlsConversation, MlsConversationConfiguration,
10        MlsCustomConfiguration, MlsError,
11    },
12};
13use core_crypto_keystore::{connection::FetchFromDatabase, entities::PersistedMlsPendingGroup};
14use mls_crypto_provider::MlsCryptoProvider;
15use openmls::prelude::{MlsGroup, MlsMessageIn, MlsMessageInBody, Welcome};
16use openmls_traits::OpenMlsCryptoProvider;
17use tls_codec::Deserialize;
18
19/// Contains everything client needs to know after decrypting an (encrypted) Welcome message
20#[derive(Debug)]
21pub struct WelcomeBundle {
22    /// MLS Group Id
23    pub id: ConversationId,
24    /// New CRL distribution points that appeared by the introduction of a new credential
25    pub crl_new_distribution_points: NewCrlDistributionPoint,
26}
27
28impl CentralContext {
29    /// Create a conversation from a TLS serialized MLS Welcome message. The `MlsConversationConfiguration` used in this function will be the default implementation.
30    ///
31    /// # Arguments
32    /// * `welcome` - a TLS serialized welcome message
33    /// * `configuration` - configuration of the MLS conversation fetched from the Delivery Service
34    ///
35    /// # Return type
36    /// This function will return the conversation/group id
37    ///
38    /// # Errors
39    /// see [CentralContext::process_welcome_message]
40    #[cfg_attr(test, crate::dispotent)]
41    pub async fn process_raw_welcome_message(
42        &self,
43        welcome: Vec<u8>,
44        custom_cfg: MlsCustomConfiguration,
45    ) -> CryptoResult<WelcomeBundle> {
46        let mut cursor = std::io::Cursor::new(welcome);
47        let welcome = MlsMessageIn::tls_deserialize(&mut cursor).map_err(MlsError::from)?;
48        self.process_welcome_message(welcome, custom_cfg).await
49    }
50
51    /// Create a conversation from a received MLS Welcome message
52    ///
53    /// # Arguments
54    /// * `welcome` - a `Welcome` message received as a result of a commit adding new members to a group
55    /// * `configuration` - configuration of the group/conversation
56    ///
57    /// # Return type
58    /// This function will return the conversation/group id
59    ///
60    /// # Errors
61    /// Errors can be originating from the KeyStore of from OpenMls:
62    /// * if no [openmls::key_packages::KeyPackage] can be read from the KeyStore
63    /// * if the message can't be decrypted
64    #[cfg_attr(test, crate::dispotent)]
65    pub async fn process_welcome_message(
66        &self,
67        welcome: MlsMessageIn,
68        custom_cfg: MlsCustomConfiguration,
69    ) -> CryptoResult<WelcomeBundle> {
70        let welcome = match welcome.extract() {
71            MlsMessageInBody::Welcome(welcome) => welcome,
72            _ => return Err(CryptoError::ConsumerError),
73        };
74        let cs = welcome.ciphersuite().into();
75        let configuration = MlsConversationConfiguration {
76            ciphersuite: cs,
77            custom: custom_cfg,
78            ..Default::default()
79        };
80        let mls_provider = self.mls_provider().await?;
81        let mut mls_groups = self.mls_groups().await?;
82        let conversation =
83            MlsConversation::from_welcome_message(welcome, configuration, &mls_provider, mls_groups.borrow_mut())
84                .await?;
85
86        // We wait for the group to be created then we iterate through all members
87        let crl_new_distribution_points =
88            get_new_crl_distribution_points(&mls_provider, extract_crl_uris_from_group(&conversation.group)?).await?;
89
90        let id = conversation.id.clone();
91        mls_groups.insert(id.clone(), conversation);
92
93        Ok(WelcomeBundle {
94            id,
95            crl_new_distribution_points,
96        })
97    }
98}
99
100impl MlsConversation {
101    // ? Do we need to provide the ratchet_tree to the MlsGroup? Does everything crumble down if we can't actually get it?
102    /// Create the MLS conversation from an MLS Welcome message
103    ///
104    /// # Arguments
105    /// * `welcome` - welcome message to create the group from
106    /// * `config` - group configuration
107    /// * `backend` - the KeyStore to persist the group
108    ///
109    /// # Errors
110    /// Errors can happen from OpenMls or from the KeyStore
111    async fn from_welcome_message(
112        welcome: Welcome,
113        configuration: MlsConversationConfiguration,
114        backend: &MlsCryptoProvider,
115        mls_groups: &mut GroupStore<MlsConversation>,
116    ) -> CryptoResult<Self> {
117        let mls_group_config = configuration.as_openmls_default_configuration()?;
118
119        let group = MlsGroup::new_from_welcome(backend, &mls_group_config, welcome, None).await;
120
121        let group = match group {
122            Err(openmls::prelude::WelcomeError::NoMatchingKeyPackage)
123            | Err(openmls::prelude::WelcomeError::NoMatchingEncryptionKey) => return Err(CryptoError::OrphanWelcome),
124            _ => group.map_err(MlsError::from)?,
125        };
126
127        let id = ConversationId::from(group.group_id().as_slice());
128        let existing_conversation = mls_groups.get_fetch(&id[..], &backend.keystore(), None).await;
129        let conversation_exists = existing_conversation.ok().flatten().is_some();
130
131        let pending_group = backend.key_store().find::<PersistedMlsPendingGroup>(&id[..]).await;
132        let pending_group_exists = pending_group.ok().flatten().is_some();
133
134        if conversation_exists || pending_group_exists {
135            return Err(CryptoError::ConversationAlreadyExists(id));
136        }
137
138        Self::from_mls_group(group, configuration, backend).await
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use wasm_bindgen_test::*;
145
146    use crate::{prelude::MlsConversationCreationMessage, test_utils::*};
147
148    use super::*;
149
150    wasm_bindgen_test_configure!(run_in_browser);
151
152    #[apply(all_cred_cipher)]
153    #[wasm_bindgen_test]
154    async fn joining_from_welcome_should_prune_local_key_material(case: TestCase) {
155        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
156            Box::pin(async move {
157                let id = conversation_id();
158                // has to be before the original key_package count because it creates one
159                let bob = bob_central.rand_key_package(&case).await;
160                // Keep track of the whatever amount was initially generated
161                let prev_count = bob_central.context.count_entities().await;
162
163                // Create a conversation from alice, where she invites bob
164                alice_central
165                    .context
166                    .new_conversation(&id, case.credential_type, case.cfg.clone())
167                    .await
168                    .unwrap();
169
170                let MlsConversationCreationMessage { welcome, .. } = alice_central
171                    .context
172                    .add_members_to_conversation(&id, vec![bob])
173                    .await
174                    .unwrap();
175
176                // Bob accepts the welcome message, and as such, it should prune the used keypackage from the store
177                bob_central
178                    .context
179                    .process_welcome_message(welcome.into(), case.custom_cfg())
180                    .await
181                    .unwrap();
182
183                // Ensure we're left with 1 less keypackage bundle in the store, because it was consumed with the OpenMLS Welcome message
184                let next_count = bob_central.context.count_entities().await;
185                assert_eq!(next_count.key_package, prev_count.key_package - 1);
186                assert_eq!(next_count.hpke_private_key, prev_count.hpke_private_key - 1);
187                assert_eq!(next_count.encryption_keypair, prev_count.encryption_keypair - 1);
188            })
189        })
190        .await;
191    }
192
193    #[apply(all_cred_cipher)]
194    #[wasm_bindgen_test]
195    async fn process_welcome_should_fail_when_already_exists(case: TestCase) {
196        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
197            Box::pin(async move {
198                let id = conversation_id();
199                alice_central
200                    .context
201                    .new_conversation(&id, case.credential_type, case.cfg.clone())
202                    .await
203                    .unwrap();
204                let bob = bob_central.rand_key_package(&case).await;
205                let welcome = alice_central
206                    .context
207                    .add_members_to_conversation(&id, vec![bob])
208                    .await
209                    .unwrap()
210                    .welcome;
211
212                // Meanwhile Bob creates a conversation with the exact same id as the one he's trying to join
213                bob_central
214                    .context
215                    .new_conversation(&id, case.credential_type, case.cfg.clone())
216                    .await
217                    .unwrap();
218                let join_welcome = bob_central
219                    .context
220                    .process_welcome_message(welcome.into(), case.custom_cfg())
221                    .await;
222                assert!(matches!(join_welcome.unwrap_err(), CryptoError::ConversationAlreadyExists(i) if i == id));
223            })
224        })
225        .await;
226    }
227}