core_crypto/transaction_context/conversation/
welcome.rs

1//! This module contains transactional conversation operations that produce a [WelcomeBundle].
2
3use std::borrow::BorrowMut as _;
4
5use super::{Error, Result, TransactionContext};
6use crate::{
7    RecursiveError,
8    mls::credential::crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
9    prelude::{MlsConversation, MlsConversationConfiguration, MlsCustomConfiguration, WelcomeBundle},
10};
11use openmls::prelude::{MlsMessageIn, MlsMessageInBody};
12use tls_codec::Deserialize as _;
13
14impl TransactionContext {
15    /// Create a conversation from a TLS serialized MLS Welcome message. The `MlsConversationConfiguration` used in this function will be the default implementation.
16    ///
17    /// # Arguments
18    /// * `welcome` - a TLS serialized welcome message
19    /// * `configuration` - configuration of the MLS conversation fetched from the Delivery Service
20    ///
21    /// # Return type
22    /// This function will return the conversation/group id
23    ///
24    /// # Errors
25    /// see [TransactionContext::process_welcome_message]
26    #[cfg_attr(test, crate::dispotent)]
27    pub async fn process_raw_welcome_message(
28        &self,
29        welcome: Vec<u8>,
30        custom_cfg: MlsCustomConfiguration,
31    ) -> Result<WelcomeBundle> {
32        let mut cursor = std::io::Cursor::new(welcome);
33        let welcome =
34            MlsMessageIn::tls_deserialize(&mut cursor).map_err(Error::tls_deserialize("mls message in (welcome)"))?;
35        self.process_welcome_message(welcome, custom_cfg).await
36    }
37
38    /// Create a conversation from a received MLS Welcome message
39    ///
40    /// # Arguments
41    /// * `welcome` - a `Welcome` message received as a result of a commit adding new members to a group
42    /// * `configuration` - configuration of the group/conversation
43    ///
44    /// # Return type
45    /// This function will return the conversation/group id
46    ///
47    /// # Errors
48    /// Errors can be originating from the KeyStore of from OpenMls:
49    /// * if no [openmls::key_packages::KeyPackage] can be read from the KeyStore
50    /// * if the message can't be decrypted
51    #[cfg_attr(test, crate::dispotent)]
52    pub async fn process_welcome_message(
53        &self,
54        welcome: MlsMessageIn,
55        custom_cfg: MlsCustomConfiguration,
56    ) -> Result<WelcomeBundle> {
57        let MlsMessageInBody::Welcome(welcome) = welcome.extract() else {
58            return Err(Error::CallerError(
59                "the message provided to process_welcome_message was not a welcome message",
60            ));
61        };
62        let cs = welcome.ciphersuite().into();
63        let configuration = MlsConversationConfiguration {
64            ciphersuite: cs,
65            custom: custom_cfg,
66            ..Default::default()
67        };
68        let mls_provider = self
69            .mls_provider()
70            .await
71            .map_err(RecursiveError::transaction("getting mls provider"))?;
72        let mut mls_groups = self
73            .mls_groups()
74            .await
75            .map_err(RecursiveError::transaction("getting mls groups"))?;
76        let conversation =
77            MlsConversation::from_welcome_message(welcome, configuration, &mls_provider, mls_groups.borrow_mut())
78                .await
79                .map_err(RecursiveError::mls_conversation("creating conversation from welcome"))?;
80
81        // We wait for the group to be created then we iterate through all members
82        let crl_new_distribution_points = get_new_crl_distribution_points(
83            &mls_provider,
84            extract_crl_uris_from_group(&conversation.group)
85                .map_err(RecursiveError::mls_credential("extracting crl uris from group"))?,
86        )
87        .await
88        .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
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
100#[cfg(test)]
101mod tests {
102    use wasm_bindgen_test::*;
103
104    use crate::test_utils::*;
105
106    use super::*;
107
108    wasm_bindgen_test_configure!(run_in_browser);
109
110    #[apply(all_cred_cipher)]
111    #[wasm_bindgen_test]
112    async fn joining_from_welcome_should_prune_local_key_material(case: TestCase) {
113        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
114            Box::pin(async move {
115                let id = conversation_id();
116                // has to be before the original key_package count because it creates one
117                let bob = bob_central.rand_key_package(&case).await;
118                // Keep track of the whatever amount was initially generated
119                let prev_count = bob_central.context.count_entities().await;
120
121                // Create a conversation from alice, where she invites bob
122                alice_central
123                    .context
124                    .new_conversation(&id, case.credential_type, case.cfg.clone())
125                    .await
126                    .unwrap();
127
128                alice_central
129                    .context
130                    .conversation(&id)
131                    .await
132                    .unwrap()
133                    .add_members(vec![bob])
134                    .await
135                    .unwrap();
136
137                let welcome = alice_central.mls_transport.latest_welcome_message().await;
138                // Bob accepts the welcome message, and as such, it should prune the used keypackage from the store
139                bob_central
140                    .context
141                    .process_welcome_message(welcome.into(), case.custom_cfg())
142                    .await
143                    .unwrap();
144
145                // Ensure we're left with 1 less keypackage bundle in the store, because it was consumed with the OpenMLS Welcome message
146                let next_count = bob_central.context.count_entities().await;
147                assert_eq!(next_count.key_package, prev_count.key_package - 1);
148                assert_eq!(next_count.hpke_private_key, prev_count.hpke_private_key - 1);
149                assert_eq!(next_count.encryption_keypair, prev_count.encryption_keypair - 1);
150            })
151        })
152        .await;
153    }
154
155    #[apply(all_cred_cipher)]
156    #[wasm_bindgen_test]
157    async fn process_welcome_should_fail_when_already_exists(case: TestCase) {
158        use crate::LeafError;
159
160        run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
161            Box::pin(async move {
162                let id = conversation_id();
163                alice_central
164                    .context
165                    .new_conversation(&id, case.credential_type, case.cfg.clone())
166                    .await
167                    .unwrap();
168                let bob = bob_central.rand_key_package(&case).await;
169                alice_central
170                    .context
171                    .conversation(&id)
172                    .await
173                    .unwrap()
174                    .add_members(vec![bob])
175                    .await
176                    .unwrap();
177
178                let welcome = alice_central.mls_transport.latest_welcome_message().await;
179                // Meanwhile Bob creates a conversation with the exact same id as the one he's trying to join
180                bob_central
181                    .context
182                    .new_conversation(&id, case.credential_type, case.cfg.clone())
183                    .await
184                    .unwrap();
185                let join_welcome = bob_central
186                    .context
187                    .process_welcome_message(welcome.into(), case.custom_cfg())
188                    .await;
189                assert!(
190                    matches!(join_welcome.unwrap_err(),
191                    Error::Recursive(crate::RecursiveError::MlsConversation { source, .. })
192                        if matches!(*source, crate::mls::conversation::Error::Leaf(LeafError::ConversationAlreadyExists(ref i)) if i == &id
193                        )
194                    )
195                );
196            })
197        })
198        .await;
199    }
200}