Skip to main content

core_crypto/transaction_context/conversation/
welcome.rs

1//! This module contains transactional conversation operations that are related to processing welcome messages.
2
3use openmls::prelude::{MlsMessageIn, MlsMessageInBody};
4
5use super::{Error, Result, TransactionContext};
6use crate::{ConversationConfiguration, ConversationId};
7
8impl TransactionContext {
9    /// Create a conversation from a received MLS Welcome message
10    ///
11    /// # Arguments
12    /// * `welcome` - a `Welcome` message received as a result of a commit adding new members to a group
13    ///
14    /// # Return type
15    /// This function will return the conversation/group id
16    ///
17    /// # Errors
18    /// Errors can be originating from the KeyStore of from OpenMls:
19    /// * if no [openmls::key_packages::KeyPackage] can be read from the KeyStore
20    /// * if the message can't be decrypted
21    #[cfg_attr(test, crate::dispotent)]
22    pub async fn process_welcome_message(&self, welcome: impl Into<MlsMessageIn>) -> Result<ConversationId> {
23        let MlsMessageInBody::Welcome(welcome) = welcome.into().extract() else {
24            return Err(Error::CallerError(
25                "the message provided to process_welcome_message was not a welcome message",
26            ));
27        };
28
29        let configuration = ConversationConfiguration {
30            cipher_suite: welcome.ciphersuite().into(),
31            ..Default::default()
32        };
33
34        let welcome_clone = welcome.clone();
35        let conversation_result = self
36            .persist_conversation_from_welcome_message(welcome_clone, configuration)
37            .await;
38
39        if conversation_result.is_err() {
40            // If persisting failed, we want to pretend we didn't process the welcome at all and restore the key
41            // package that may have been marked for deletion by openmls:
42            // https://github.com/wireapp/openmls/blob/c9cde17076508968c9cbead5728454f0a1f60c4f/openmls/src/group/mls_group/creation.rs#L166
43            for key_package_hash_ref in welcome.secrets().iter().map(|secret| secret.new_member().as_slice()) {
44                self.restore_key_package(key_package_hash_ref).await?;
45            }
46        }
47
48        let conversation = conversation_result?;
49        let id = conversation.id().to_owned();
50
51        Ok(id)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use crate::test_utils::*;
58
59    #[apply(all_cred_cipher)]
60    async fn joining_from_welcome_should_prune_local_key_material(case: TestContext) {
61        let [alice, bob] = case.sessions().await;
62        Box::pin(async move {
63            // has to be before the original key_package count because it creates one
64            // Create a conversation from alice, where she invites bob
65            let commit_guard = case.create_conversation([&alice]).await.invite([&bob]).await;
66
67            // Keep track of the whatever amount was initially generated
68            let prev_count = bob.transaction.count_entities().await;
69            // Bob accepts the welcome message, and as such, it should prune the used keypackage from the store
70            commit_guard.notify_members().await;
71
72            // Ensure we're left with 1 less keypackage bundle in the store, because it was consumed with the OpenMLS
73            // Welcome message
74            let next_count = bob.transaction.count_entities().await;
75            assert_eq!(next_count.key_package, prev_count.key_package - 1);
76            assert_eq!(next_count.hpke_private_key, prev_count.hpke_private_key - 1);
77            assert_eq!(next_count.encryption_keypair, prev_count.encryption_keypair - 1);
78        })
79        .await;
80    }
81
82    #[apply(all_cred_cipher)]
83    async fn process_welcome_should_fail_when_already_exists(case: TestContext) {
84        use crate::LeafError;
85
86        let [alice, mut bob] = case.sessions().await;
87        Box::pin(async move {
88            let credential_ref = &bob.initial_credential;
89            let commit = case.create_conversation([&alice]).await.invite([&bob]).await;
90            let conversation = commit.conversation();
91            let id = conversation.id().clone();
92                // Meanwhile Bob creates a conversation with the exact same id as the one he's trying to join
93                bob
94                    .transaction
95                    .new_conversation(&id, credential_ref, case.cfg.clone())
96                    .await
97                    .unwrap();
98
99                // Bob's key packages before processing the welcome
100                let key_package_refs_before = bob.transaction.get_key_package_refs().await.unwrap();
101
102                let welcome = conversation.transport().await.latest_welcome_message().await;
103
104                // We need Bob's created key package to be persisted, so we can restore it on error.
105                // Assuming that key package creation happens in its own transaction matches a sufficiently large
106                // portion of real-world usage.
107                bob.commit_transaction().await;
108
109                let join_welcome = bob
110                    .transaction
111                    .process_welcome_message(welcome)
112                    .await;
113
114                // Bob's key packages after processing the welcome
115                let key_package_refs_after = bob.transaction.get_key_package_refs().await.unwrap();
116
117                assert!(!key_package_refs_before.is_empty());
118                assert_eq!(key_package_refs_before, key_package_refs_after);
119                assert!(innermost_source_matches!(join_welcome.unwrap_err(), LeafError::ConversationAlreadyExists(i) if i == &id));
120            })
121        .await;
122    }
123}