core_crypto/mls/conversation/
welcome.rs

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