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        let [alice, mut bob] = case.sessions().await;
85        Box::pin(async move {
86            let credential_ref = &bob.initial_credential;
87            let commit = case.create_conversation([&alice]).await.invite([&bob]).await;
88            let conversation = commit.conversation();
89            let id = conversation.id().clone();
90                // Meanwhile Bob creates a conversation with the exact same id as the one he's trying to join
91                bob
92                    .transaction
93                    .new_conversation(&id, credential_ref, case.cfg.clone())
94                    .await
95                    .unwrap();
96
97                // Bob's key packages before processing the welcome
98                let key_package_refs_before = bob.transaction.get_key_package_refs().await.unwrap();
99
100                let welcome = conversation.transport().await.latest_welcome_message().await;
101
102                // We need Bob's created key package to be persisted, so we can restore it on error.
103                // Assuming that key package creation happens in its own transaction matches a sufficiently large
104                // portion of real-world usage.
105                bob.commit_transaction().await;
106
107                let join_welcome = bob
108                    .transaction
109                    .process_welcome_message(welcome)
110                    .await;
111
112                // Bob's key packages after processing the welcome
113                let key_package_refs_after = bob.transaction.get_key_package_refs().await.unwrap();
114
115                assert!(!key_package_refs_before.is_empty());
116                assert_eq!(key_package_refs_before, key_package_refs_after);
117                assert!(innermost_source_matches!(join_welcome.unwrap_err(), super::Error::ConversationAlreadyExists(i) if i == &id));
118            })
119        .await;
120    }
121}