Skip to main content

core_crypto/transaction_context/conversation/
mod.rs

1//! This module contains all [super::TransactionContext] methods related to a conversation.
2
3pub mod external_commit;
4mod persistence;
5pub mod welcome;
6
7use std::sync::Arc;
8
9use core_crypto_keystore::{
10    Transaction,
11    entities::{MlsPendingMessage, PersistedMlsGroup, StoredBufferedCommit},
12    traits::{DeletableBySearchKey, EntityDeleteBorrowed, FetchFromDatabase as _},
13};
14use openmls::group::MlsGroup;
15
16use super::{Error, Result, TransactionContext};
17use crate::{
18    ConversationConfiguration, CredentialRef, KeystoreError, OpenMlsError, RecursiveError,
19    mls::conversation::{ConversationIdRef, ConversationMut, PendingConversation},
20};
21
22impl TransactionContext {
23    /// Checks if a given conversation id exists locally.
24    ///
25    /// Somewhat cheaper than `self.conversation(id).is_ok()`.
26    pub async fn conversation_exists(&self, id: &ConversationIdRef) -> Result<bool> {
27        let database = self.database()?.into();
28        self.mls_groups()
29            .await?
30            .exists(id, &database)
31            .await
32            .map_err(RecursiveError::context("checking for conversation existence"))
33            .map_err(Into::into)
34    }
35
36    /// Acquire a conversation guard.
37    ///
38    /// This helper struct permits mutations on a conversation.
39    pub async fn conversation(&self, id: &ConversationIdRef) -> Result<ConversationMut> {
40        let inner = self.inner()?;
41        let session = self.session().await?;
42        let conversation = self
43            .mls_groups()
44            .await?
45            .get_or_fetch(id, &inner.transaction, session)
46            .await
47            .map_err(RecursiveError::context("fetching conversation from mls groups by id"))?;
48
49        if let Some(conversation) = conversation {
50            return Ok(ConversationMut::new(conversation, self.clone()));
51        }
52        // Check if there is a pending conversation with
53        // the same id
54        let pending = self.pending_conversation(id).await.map(Error::PendingConversation)?;
55        Err(pending)
56    }
57
58    /// See [`clear_orphaned_conversation_buffers`].
59    ///
60    /// Callers must have staged their own deletion before calling this, since that deletion is
61    /// exactly what this reads back. It is also the reason this consults the keystore rather than
62    /// [`Self::conversation_exists`]: the question is which rows will exist once the transaction
63    /// commits, which the in-memory conversation cache does not answer.
64    pub(crate) async fn clear_orphaned_conversation_buffers(&self, id: &ConversationIdRef) -> Result<()> {
65        let inner = self.inner()?;
66        clear_orphaned_conversation_buffers(inner.transaction(), id).await?;
67        Ok(())
68    }
69
70    pub(crate) async fn pending_conversation(&self, id: &ConversationIdRef) -> Result<PendingConversation> {
71        let inner = self.inner()?;
72        let group = inner
73            .transaction
74            .get_borrowed::<PersistedMlsGroup>(id.keystore())
75            .await
76            .map_err(KeystoreError::wrap("finding persisted mls group"))?
77            .filter(|group| group.is_pending);
78        let Some(group) = group else {
79            return Err(Error::ConversationNotFound(id.to_owned()));
80        };
81        let group = Arc::unwrap_or_clone(group);
82        Ok(PendingConversation::new(group, self.clone()))
83    }
84
85    /// Create a new empty conversation
86    ///
87    /// # Arguments
88    /// * `id` - identifier of the group/conversation (must be unique otherwise the existing group will be overridden)
89    /// * `creator_credential_type` - kind of credential the creator wants to create the group with
90    /// * `config` - configuration of the group/conversation
91    ///
92    /// # Errors
93    /// Errors can happen from the KeyStore or from OpenMls for ex if no [openmls::key_packages::KeyPackage] can
94    /// be found in the KeyStore
95    #[cfg_attr(test, crate::dispotent)]
96    pub async fn new_conversation(
97        &self,
98        id: &ConversationIdRef,
99        credential_ref: &CredentialRef,
100        configuration: ConversationConfiguration,
101    ) -> Result<()> {
102        let database = self.database()?;
103        let provider = self.crypto_provider().await?;
104        if self.conversation_exists(id).await? || self.pending_conversation_exists(id).await? {
105            return Err(Error::ConversationAlreadyExists(id.to_owned()));
106        }
107
108        let credential = credential_ref.load(&*database).await.map_err(RecursiveError::context(
109            "loading credential from database to create new conversation",
110        ))?;
111
112        let config = configuration
113            .as_openmls_default_configuration()
114            .map_err(RecursiveError::context("converting config to openmls default"))?;
115
116        // there's a deferred constraint on epoch encryption keypairs.
117        // openmls creates the keypairs before it hands us the `group` which we can use to
118        // persist the conversation, so the constraint _can't_ be fulfilled immediately;
119        // it gets checked at the outermost commit.
120        // wrapping these two operations in an explicit savepoint means that these two operations
121        // are bundled together; failing to persist the group un-persists the keypairs.
122        self.inner()?
123            .transaction()
124            .with_savepoint(
125                "new_conversation",
126                async || {
127                    let group = MlsGroup::new_with_group_id(
128                        &provider,
129                        &credential.signature_key_pair,
130                        &config,
131                        openmls::prelude::GroupId::from_slice(id.as_ref()),
132                        credential.to_mls_credential_with_key(),
133                    )
134                    .await
135                    .map_err(OpenMlsError::wrap("creating group with id"))?;
136
137                    self.persist_conversation_from_mls_group(group, configuration).await?;
138
139                    Ok(())
140                },
141                Error::savepoint,
142            )
143            .await
144    }
145}
146
147/// Discard everything buffered for a conversation which no longer exists in any form.
148///
149/// Buffered messages and buffered commits are keyed by conversation id, and both are only ever
150/// read on behalf of a conversation. Once no conversation holds that id, they are unreachable:
151/// nothing can restore them and nothing else will ever delete them. So whichever operation
152/// removes the last trace of a conversation has to take its buffers along, and this is that step.
153///
154/// Takes the transaction directly rather than a [`TransactionContext`], because one caller
155/// ([`ConversationCache::get_or_fetch`][crate::mls::conversation_cache::ConversationCache]) deletes
156/// a conversation while holding nothing else.
157///
158/// Callers must have staged their own deletion first; this reads it back to decide whether the
159/// conversation is really gone.
160pub(crate) async fn clear_orphaned_conversation_buffers(
161    tx: &Transaction,
162    id: &ConversationIdRef,
163) -> Result<(), KeystoreError> {
164    let group_exists = tx
165        .get_borrowed::<PersistedMlsGroup>(id.keystore())
166        .await
167        .map_err(KeystoreError::wrap("looking for a group of a removed conversation"))?
168        .is_some();
169    if group_exists {
170        return Ok(());
171    }
172
173    MlsPendingMessage::delete_all_matching(tx, id.keystore()).map_err(KeystoreError::wrap(
174        "clearing the pending messages of a removed conversation",
175    ))?;
176    StoredBufferedCommit::delete_borrowed(tx, id.as_ref()).map_err(KeystoreError::wrap(
177        "clearing the buffered commit of a removed conversation",
178    ))?;
179
180    Ok(())
181}