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        // this happens before propagating removal to the keypackages so we can error appropriately
55        let removed = credential_ref.delete(&inner.transaction).map_err(|err| -> Error {
56            let crate::KeystoreError { source, .. } = &err;
57            let Some(core_crypto_keystore::CryptoKeystoreError::DbError(rusqlite_err)) = source.downcast_ref() else {
58                return err.into();
59            };
60            if rusqlite_err
61                .sqlite_error_code()
62                .is_some_and(|code| code == core_crypto_keystore::RusqliteErrorCode::ConstraintViolation)
63            {
64                Error::CredentialStillInUse
65            } else {
66                err.into()
67            }
68        })?;
69
70        // remove any key packages generated by this credential
71        self.remove_key_packages_for(credential_ref).await?;
72
73        Ok(removed)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use core_crypto_keystore::{
80        Sha256Hash,
81        entities::{StoredCredential, StoredCredentialPk, StoredKeyPackage},
82        traits::FetchFromDatabase as _,
83    };
84    use openmls::prelude::KeyPackage;
85
86    use super::*;
87    use crate::{CertificateBundle, test_utils::*};
88
89    async fn credential_is_stored(session: &SessionContext, credential: &CredentialRef) -> bool {
90        session
91            .transaction
92            .database()
93            .unwrap()
94            .get::<StoredCredential>(&StoredCredentialPk::new(
95                credential.public_key_hash(),
96                credential.r#type().into(),
97            ))
98            .await
99            .unwrap()
100            .is_some()
101    }
102
103    /// How many stored key packages were generated from `credential`.
104    async fn count_key_packages_for(session: &SessionContext, credential: &CredentialRef) -> usize {
105        session
106            .transaction
107            .database()
108            .unwrap()
109            .load_all::<StoredKeyPackage>()
110            .await
111            .unwrap()
112            .into_iter()
113            .map(|stored| core_crypto_keystore::deser::<KeyPackage>(&stored.key_package).unwrap())
114            .filter(|key_package| {
115                Sha256Hash::hash_from(key_package.leaf_node().signature_key().as_slice())
116                    == credential.public_key_hash()
117            })
118            .count()
119    }
120
121    /// A credential a conversation still uses cannot be removed, and the refused removal writes nothing: neither the
122    /// credential nor the key packages generated from it go missing. Once the conversation moves on to another
123    /// credential, the old one can be removed.
124    #[apply(all_cred_cipher)]
125    async fn credential_in_use_by_a_conversation_cannot_be_removed(case: TestContext) {
126        const KEY_PACKAGES: usize = 2;
127
128        let [alice] = case.sessions().await;
129        let conversation = case.create_conversation([&alice]).await;
130        let credential = alice.initial_credential.clone();
131
132        for _ in 0..KEY_PACKAGES {
133            alice.new_keypackage_from_ref(credential.clone(), None).await;
134        }
135        assert_eq!(count_key_packages_for(&alice, &credential).await, KEY_PACKAGES);
136
137        let result = alice.transaction.remove_credential(&credential).await;
138        assert!(
139            matches!(result, Err(Error::CredentialStillInUse)),
140            "removing a credential in use must fail with `CredentialStillInUse`, but got {result:?}"
141        );
142
143        // The caller may catch the error and carry on with this transaction, so the failed removal must not have
144        // written anything.
145        assert!(
146            credential_is_stored(&alice, &credential).await,
147            "a refused removal must keep the credential"
148        );
149        assert_eq!(
150            count_key_packages_for(&alice, &credential).await,
151            KEY_PACKAGES,
152            "a refused removal must keep the key packages generated from the credential"
153        );
154
155        // A basic credential is just our client id, so a "new" one for the same client compares equal to the old one
156        // and openmls declines to rotate anything. Only x509 can rotate here.
157        if !case.is_x509() {
158            return;
159        }
160
161        let intermediate_ca = alice.x509_chain_unchecked().find_local_intermediate_ca();
162        let certificate = CertificateBundle::new_with_default_values(intermediate_ca, None);
163        let new_credential = Credential::x509(case.cipher_suite(), certificate).unwrap();
164        alice
165            .transaction
166            .add_credential_without_clientid_check(new_credential.clone())
167            .unwrap();
168        let new_credential = CredentialRef::from_credential(&new_credential);
169        conversation.set_credential_by_ref_notify(&new_credential).await;
170
171        let removed = alice
172            .transaction
173            .remove_credential(&credential)
174            .await
175            .expect("a credential no conversation uses any more can be removed");
176        assert!(removed, "removal must report that the credential was deleted");
177        assert!(!credential_is_stored(&alice, &credential).await);
178        assert_eq!(count_key_packages_for(&alice, &credential).await, 0);
179    }
180}