Skip to main content

core_crypto/transaction_context/credential/
mod.rs

1mod check;
2
3use std::sync::Arc;
4
5use super::{Error, Result};
6use crate::{Credential, CredentialRef, RecursiveError, transaction_context::TransactionContext};
7
8impl TransactionContext {
9    /// Add a credential to the database of this session without validating that its client ID matches the session
10    /// client id.
11    ///
12    /// This is rarely useful and should only be used when absolutely necessary. You'll know it if you need it.
13    ///
14    /// Prefer [`Self::add_credential`].
15    pub(crate) fn add_credential_without_clientid_check(&self, mut credential: Credential) -> Result<Arc<Credential>> {
16        let inner = self.inner()?;
17        let _credential_ref = credential
18            .save(&inner.transaction)
19            .map_err(RecursiveError::context("saving credential"))?;
20
21        Ok(Arc::new(credential))
22    }
23    /// Add a credential to the database of this session.
24    pub async fn add_credential(&self, credential: Credential) -> Result<CredentialRef> {
25        let credential = self.add_credential_producing_arc(credential).await?;
26        Ok(CredentialRef::from_credential(&credential))
27    }
28
29    /// Add a credential to the database of this session.
30    ///
31    /// Returns the actual credential instance which was loaded from the DB.
32    /// This is a convenience for internal use and should _not_ be propagated across
33    /// the FFI boundary. Instead, use [`Self::add_credential`] to produce a [`CredentialRef`].
34    pub(crate) async fn add_credential_producing_arc(&self, credential: Credential) -> Result<Arc<Credential>> {
35        if *credential.client_id() != self.session().await?.id() {
36            return Err(Error::WrongCredential);
37        }
38
39        self.add_credential_without_clientid_check(credential)
40    }
41
42    /// Remove a credential from the database of this session.
43    ///
44    /// First checks that the credential is not used in any conversation.
45    /// Removes both the credential itself and also any key packages which were generated from it.
46    pub async fn remove_credential(&self, credential_ref: &CredentialRef) -> Result<bool> {
47        // setup
48        if *credential_ref.client_id() != self.session().await?.id() {
49            return Err(Error::WrongCredential);
50        }
51
52        let inner = self.inner()?;
53
54        let removed = credential_ref.delete(&inner.transaction).map_err(|err| -> Error {
55            let crate::KeystoreError { source, .. } = &err;
56            let Some(core_crypto_keystore::CryptoKeystoreError::DbError(rusqlite_err)) = source.downcast_ref() else {
57                return err.into();
58            };
59            if rusqlite_err
60                .sqlite_error_code()
61                .is_some_and(|code| code == core_crypto_keystore::RusqliteErrorCode::ConstraintViolation)
62            {
63                Error::CredentialStillInUse
64            } else {
65                err.into()
66            }
67        })?;
68
69        Ok(removed)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use core_crypto_keystore::{
76        entities::{StoredCredential, StoredCredentialPk, StoredKeyPackage},
77        traits::FetchFromDatabase as _,
78    };
79
80    use super::*;
81    use crate::{CertificateBundle, test_utils::*};
82
83    async fn credential_is_stored(session: &SessionContext, credential: &CredentialRef) -> bool {
84        session
85            .transaction
86            .database()
87            .unwrap()
88            .get::<StoredCredential>(&StoredCredentialPk::new(
89                credential.public_key_hash(),
90                credential.r#type().into(),
91            ))
92            .await
93            .unwrap()
94            .is_some()
95    }
96
97    /// How many stored key packages were generated from `credential`.
98    async fn count_key_packages_for(session: &SessionContext, credential: &CredentialRef) -> usize {
99        session
100            .transaction
101            .database()
102            .unwrap()
103            .search::<StoredKeyPackage, _>(&credential.keystore_pk())
104            .await
105            .unwrap()
106            .len()
107    }
108
109    /// A credential a conversation still uses cannot be removed, and the refused removal writes nothing: neither the
110    /// credential nor the key packages generated from it go missing. Once the conversation moves on to another
111    /// credential, the old one can be removed.
112    #[apply(all_cred_cipher)]
113    async fn credential_in_use_by_a_conversation_cannot_be_removed(case: TestContext) {
114        const KEY_PACKAGES: usize = 2;
115
116        let [alice] = case.sessions().await;
117        let conversation = case.create_conversation([&alice]).await;
118        let credential = alice.initial_credential.clone();
119
120        for _ in 0..KEY_PACKAGES {
121            alice.new_keypackage_from_ref(credential.clone(), None).await;
122        }
123        assert_eq!(count_key_packages_for(&alice, &credential).await, KEY_PACKAGES);
124
125        let result = alice.transaction.remove_credential(&credential).await;
126        assert!(
127            matches!(result, Err(Error::CredentialStillInUse)),
128            "removing a credential in use must fail with `CredentialStillInUse`, but got {result:?}"
129        );
130
131        // The caller may catch the error and carry on with this transaction, so the failed removal must not have
132        // written anything.
133        assert!(
134            credential_is_stored(&alice, &credential).await,
135            "a refused removal must keep the credential"
136        );
137        assert_eq!(
138            count_key_packages_for(&alice, &credential).await,
139            KEY_PACKAGES,
140            "a refused removal must keep the key packages generated from the credential"
141        );
142
143        // A basic credential is just our client id, so a "new" one for the same client compares equal to the old one
144        // and openmls declines to rotate anything. Only x509 can rotate here.
145        if !case.is_x509() {
146            return;
147        }
148
149        let intermediate_ca = alice.x509_chain_unchecked().find_local_intermediate_ca();
150        let certificate = CertificateBundle::new_with_default_values(intermediate_ca, None);
151        let new_credential = Credential::x509(case.cipher_suite(), certificate).unwrap();
152        alice
153            .transaction
154            .add_credential_without_clientid_check(new_credential.clone())
155            .unwrap();
156        let new_credential = CredentialRef::from_credential(&new_credential);
157        conversation.set_credential_by_ref_notify(&new_credential).await;
158
159        let removed = alice
160            .transaction
161            .remove_credential(&credential)
162            .await
163            .expect("a credential no conversation uses any more can be removed");
164        assert!(removed, "removal must report that the credential was deleted");
165        assert!(!credential_is_stored(&alice, &credential).await);
166        assert_eq!(count_key_packages_for(&alice, &credential).await, 0);
167    }
168}