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 openmls::prelude::{MlsMessageIn, MlsMessageInBody};
6use tls_codec::Deserialize as _;
7
8use super::{Error, Result, TransactionContext};
9use crate::{
10    MlsConversation, MlsConversationConfiguration, RecursiveError, WelcomeBundle,
11    mls::credential::crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
12};
13
14impl TransactionContext {
15    /// Create a conversation from a TLS serialized MLS Welcome message. The `MlsConversationConfiguration` used in this
16    /// function will be the default implementation.
17    ///
18    /// # Arguments
19    /// * `welcome` - a TLS serialized welcome message
20    /// * `configuration` - configuration of the MLS conversation fetched from the Delivery Service
21    ///
22    /// # Return type
23    /// This function will return the conversation/group id
24    ///
25    /// # Errors
26    /// see [TransactionContext::process_welcome_message]
27    #[cfg_attr(test, crate::dispotent)]
28    pub async fn process_raw_welcome_message(&self, welcome: &[u8]) -> Result<WelcomeBundle> {
29        let mut cursor = std::io::Cursor::new(welcome);
30        let welcome =
31            MlsMessageIn::tls_deserialize(&mut cursor).map_err(Error::tls_deserialize("mls message in (welcome)"))?;
32        self.process_welcome_message(welcome).await
33    }
34
35    /// Create a conversation from a received MLS Welcome message
36    ///
37    /// # Arguments
38    /// * `welcome` - a `Welcome` message received as a result of a commit adding new members to a group
39    /// * `configuration` - configuration of the group/conversation
40    ///
41    /// # Return type
42    /// This function will return the conversation/group id
43    ///
44    /// # Errors
45    /// Errors can be originating from the KeyStore of from OpenMls:
46    /// * if no [openmls::key_packages::KeyPackage] can be read from the KeyStore
47    /// * if the message can't be decrypted
48    #[cfg_attr(test, crate::dispotent)]
49    pub async fn process_welcome_message(&self, welcome: MlsMessageIn) -> Result<WelcomeBundle> {
50        let database = &self.database().await?;
51        let MlsMessageInBody::Welcome(welcome) = welcome.extract() else {
52            return Err(Error::CallerError(
53                "the message provided to process_welcome_message was not a welcome message",
54            ));
55        };
56        let cs = welcome.ciphersuite().into();
57        let configuration = MlsConversationConfiguration {
58            ciphersuite: cs,
59            ..Default::default()
60        };
61        let mls_provider = self
62            .mls_provider()
63            .await
64            .map_err(RecursiveError::transaction("getting mls provider"))?;
65        let mut mls_groups = self
66            .mls_groups()
67            .await
68            .map_err(RecursiveError::transaction("getting mls groups"))?;
69        let conversation = MlsConversation::from_welcome_message(
70            welcome,
71            configuration,
72            &mls_provider,
73            database,
74            mls_groups.borrow_mut(),
75        )
76        .await
77        .map_err(RecursiveError::mls_conversation("creating conversation from welcome"))?;
78
79        // We wait for the group to be created then we iterate through all members
80        let crl_new_distribution_points = get_new_crl_distribution_points(
81            database,
82            extract_crl_uris_from_group(&conversation.group)
83                .map_err(RecursiveError::mls_credential("extracting crl uris from group"))?,
84        )
85        .await
86        .map_err(RecursiveError::mls_credential("getting new crl distribution points"))?;
87
88        let id = conversation.id.clone();
89        mls_groups.insert(&id, conversation);
90
91        Ok(WelcomeBundle {
92            id,
93            crl_new_distribution_points,
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::test_utils::*;
102
103    #[apply(all_cred_cipher)]
104    async fn joining_from_welcome_should_prune_local_key_material(case: TestContext) {
105        let [alice, bob] = case.sessions().await;
106        Box::pin(async move {
107            // has to be before the original key_package count because it creates one
108            // Create a conversation from alice, where she invites bob
109            let commit_guard = case.create_conversation([&alice]).await.invite([&bob]).await;
110
111            // Keep track of the whatever amount was initially generated
112            let prev_count = bob.transaction.count_entities().await;
113            // Bob accepts the welcome message, and as such, it should prune the used keypackage from the store
114            commit_guard.notify_members().await;
115
116            // Ensure we're left with 1 less keypackage bundle in the store, because it was consumed with the OpenMLS
117            // Welcome message
118            let next_count = bob.transaction.count_entities().await;
119            assert_eq!(next_count.key_package, prev_count.key_package - 1);
120            assert_eq!(next_count.hpke_private_key, prev_count.hpke_private_key - 1);
121            assert_eq!(next_count.encryption_keypair, prev_count.encryption_keypair - 1);
122        })
123        .await;
124    }
125
126    #[apply(all_cred_cipher)]
127    async fn process_welcome_should_fail_when_already_exists(case: TestContext) {
128        use crate::LeafError;
129
130        let [alice, bob] = case.sessions().await;
131        Box::pin(async move {
132            let credential_ref = &bob.initial_credential;
133            let commit = case.create_conversation([&alice]).await.invite([&bob]).await;
134            let conversation = commit.conversation();
135            let id = conversation.id().clone();
136                // Meanwhile Bob creates a conversation with the exact same id as the one he's trying to join
137                bob
138                    .transaction
139                    .new_conversation(&id, credential_ref, case.cfg.clone())
140                    .await
141                    .unwrap();
142
143                let welcome = conversation.transport().await.latest_welcome_message().await;
144                let join_welcome = bob
145                    .transaction
146                    .process_welcome_message(welcome.into())
147                    .await;
148                assert!(
149                    matches!(join_welcome.unwrap_err(),
150                    Error::Recursive(crate::RecursiveError::MlsConversation { source, .. })
151                        if matches!(*source, crate::mls::conversation::Error::Leaf(LeafError::ConversationAlreadyExists(ref i)) if i == &id
152                        )
153                    )
154                );
155            })
156        .await;
157    }
158}