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, KeystoreError};
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 inner = self.inner().await?;
35        let conversation = inner
36            .transaction()
37            .with_savepoint(
38                "process_welcome_message_savepoint",
39                async || {
40                    self.persist_conversation_from_welcome_message(welcome, configuration)
41                        .await
42                },
43                |context| Box::new(move |err| KeystoreError::wrap(context)(err).into()),
44            )
45            .await?;
46
47        let id = conversation.id().to_owned();
48
49        Ok(id)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use crate::test_utils::*;
56
57    #[apply(all_cred_cipher)]
58    async fn joining_from_welcome_should_prune_local_key_material(case: TestContext) {
59        let [alice, bob] = case.sessions().await;
60        Box::pin(async move {
61            // has to be before the original key_package count because it creates one
62            // Create a conversation from alice, where she invites bob
63            let commit_guard = case.create_conversation([&alice]).await.invite([&bob]).await;
64
65            // Keep track of the whatever amount was initially generated
66            let prev_count = bob.transaction.count_entities().await;
67            // Bob accepts the welcome message, and as such, it should prune the used keypackage from the store
68            commit_guard.notify_members().await;
69
70            // Ensure we're left with 1 less keypackage bundle in the store, because it was consumed with the OpenMLS
71            // Welcome message
72            let next_count = bob.transaction.count_entities().await;
73            assert_eq!(next_count.key_package, prev_count.key_package - 1);
74            assert_eq!(next_count.hpke_private_key, prev_count.hpke_private_key - 1);
75            assert_eq!(next_count.encryption_keypair, prev_count.encryption_keypair - 1);
76        })
77        .await;
78    }
79
80    #[apply(all_cred_cipher)]
81    async fn process_welcome_should_fail_when_already_exists(case: TestContext) {
82        use crate::LeafError;
83
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(), LeafError::ConversationAlreadyExists(i) if i == &id));
118            })
119        .await;
120    }
121
122    /// Processing a welcome message makes openmls consume the key package it was addressed to; when
123    /// persisting the conversation then fails, the savepoint in `process_welcome_message` must put
124    /// that key material back.
125    ///
126    /// Unlike [`process_welcome_should_fail_when_already_exists`], this does not commit before
127    /// processing the welcome: a savepoint rolls back within the transaction which created the key
128    /// package, so restoration must work even then.
129    #[apply(all_cred_cipher)]
130    async fn failed_welcome_should_restore_key_material_in_same_transaction(case: TestContext) {
131        let [alice, bob] = case.sessions().await;
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
137        // Bob creates a conversation with the exact same id as the one he's trying to join, so
138        // persisting the conversation from the welcome message is bound to fail
139        bob.transaction
140            .new_conversation(&id, credential_ref, case.cfg.clone())
141            .await
142            .unwrap();
143
144        let welcome = conversation.transport().await.latest_welcome_message().await;
145
146        let count_before = bob.transaction.count_entities().await;
147        let key_package_refs_before = bob.transaction.get_key_package_refs().await.unwrap();
148        assert!(!key_package_refs_before.is_empty());
149
150        let join_welcome = bob.transaction.process_welcome_message(welcome).await;
151        assert!(innermost_source_matches!(
152            join_welcome.unwrap_err(),
153            crate::LeafError::ConversationAlreadyExists(i) if i == &id
154        ));
155
156        // Every entity the welcome touched must be back where it was: the key package itself,
157        // its hpke init private key, and its leaf node encryption keypair.
158        let count_after = bob.transaction.count_entities().await;
159        assert_eq!(count_before, count_after);
160        let key_package_refs_after = bob.transaction.get_key_package_refs().await.unwrap();
161        assert_eq!(key_package_refs_before, key_package_refs_after);
162    }
163
164    /// Restoring the key material is only worth anything if it is complete: a key package whose
165    /// private keys were not restored is unusable. So once the reason the welcome failed is gone,
166    /// processing that same welcome message again has to succeed.
167    #[apply(all_cred_cipher)]
168    async fn restored_key_material_should_still_be_able_to_join_from_welcome(case: TestContext) {
169        let [alice, bob] = case.sessions().await;
170        let credential_ref = &bob.initial_credential;
171        let commit = case.create_conversation([&alice]).await.invite([&bob]).await;
172        let conversation = commit.conversation();
173        let id = conversation.id().clone();
174
175        // The conflicting conversation which makes the first attempt fail
176        bob.transaction
177            .new_conversation(&id, credential_ref, case.cfg.clone())
178            .await
179            .unwrap();
180
181        let welcome = conversation.transport().await.latest_welcome_message().await;
182
183        let join_welcome = bob.transaction.process_welcome_message(welcome.clone()).await;
184        assert!(innermost_source_matches!(
185            join_welcome.unwrap_err(),
186            crate::LeafError::ConversationAlreadyExists(i) if i == &id
187        ));
188
189        // Bob gets rid of the conversation which was in the way
190        bob.transaction.conversation(&id).await.unwrap().wipe().await.unwrap();
191
192        // The restored key material is complete, so the second attempt goes through
193        let joined_id = bob.transaction.process_welcome_message(welcome).await.unwrap();
194        assert_eq!(joined_id, id);
195
196        // And the conversation Bob joined is the real thing: he can talk in it
197        let message = bob
198            .transaction
199            .conversation(&id)
200            .await
201            .unwrap()
202            .encrypt_message(b"hello")
203            .await
204            .unwrap();
205        let decrypted = conversation
206            .guard_of(&alice)
207            .await
208            .decrypt_message(&message)
209            .await
210            .unwrap();
211        assert_eq!(decrypted.as_text().unwrap().plaintext, b"hello");
212    }
213}