core_crypto/transaction_context/credential/
mod.rs1mod check;
2
3use std::sync::Arc;
4
5use super::{Error, Result};
6use crate::{Credential, CredentialRef, RecursiveError, transaction_context::TransactionContext};
7
8impl TransactionContext {
9 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 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 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 pub async fn remove_credential(&self, credential_ref: &CredentialRef) -> Result<bool> {
47 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 {
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 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 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 #[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 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 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}