use async_trait::async_trait;
use basil_keystore_backend::{
CryptoError, SecretStore, StoreConfig, StoreError, decrypt_aead, encrypt_aead,
generate_key_material, keystore_version, public_ed25519, sign_ed25519, verify_ed25519,
};
use basil_proto::{AeadAlgorithm, CiphertextEnvelope, KeyMaterial, KeyType};
use zeroize::Zeroizing;
use super::{Backend, BackendError, KeyMetadata, KvSecret, KvValue, NewKey, PublicKey};
pub struct KeystoreBackend {
store: SecretStore,
}
impl KeystoreBackend {
pub fn open(config: StoreConfig) -> Result<Self, BackendError> {
Ok(Self {
store: SecretStore::open(config).map_err(store_error)?,
})
}
fn get_secret(&self, key_id: &str) -> Result<Zeroizing<Vec<u8>>, BackendError> {
self.store
.get_secret(key_id)
.map(|secret| Zeroizing::new(secret.into_vec()))
.map_err(store_error)
}
}
#[async_trait]
impl Backend for KeystoreBackend {
fn kind(&self) -> &'static str {
"keystore"
}
async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
let key_id = uuid::Uuid::new_v4().to_string();
self.create_named_key(&key_id, key_type).await
}
async fn create_named_key(
&self,
key_id: &str,
key_type: KeyType,
) -> Result<NewKey, BackendError> {
if key_type != KeyType::Ed25519 {
return Err(BackendError::UnsupportedKeyType(key_type));
}
let seed = generate_key_material();
self.store
.put(key_id, seed.as_slice())
.map_err(store_error)?;
let public = public_ed25519(seed.as_slice()).map_err(|err| crypto_error(&err))?;
Ok(NewKey {
key_id: key_id.to_owned(),
public_key: public.to_vec(),
})
}
async fn create_named_aead(
&self,
key_id: &str,
_aead: AeadAlgorithm,
) -> Result<(), BackendError> {
let key = generate_key_material();
self.store.put(key_id, key.as_slice()).map_err(store_error)
}
async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
let seed = self.get_secret(key_id)?;
public_ed25519(seed.as_slice())
.map(|p| p.to_vec())
.map_err(|err| crypto_error(&err))
}
async fn public_key_with_meta(&self, key_id: &str) -> Result<PublicKey, BackendError> {
Ok(PublicKey {
public_key: self.public_key(key_id).await?,
key_type: KeyType::Ed25519,
version: keystore_version(),
})
}
async fn key_metadata(&self, key_id: &str) -> Result<KeyMetadata, BackendError> {
let _present = self.get_secret(key_id)?;
Ok(KeyMetadata {
key_type: None,
latest_version: keystore_version(),
})
}
async fn import(
&self,
key_id: &str,
key_type: KeyType,
material: &KeyMaterial,
) -> Result<NewKey, BackendError> {
if key_type != KeyType::Ed25519 {
return Err(BackendError::UnsupportedKeyType(key_type));
}
let KeyMaterial::Ed25519Seed(seed) = material else {
return Err(BackendError::Unsupported("import material"));
};
let public = public_ed25519(seed).map_err(|err| crypto_error(&err))?;
self.store.put(key_id, seed).map_err(store_error)?;
Ok(NewKey {
key_id: key_id.to_owned(),
public_key: public.to_vec(),
})
}
async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
let seed = self.get_secret(key_id)?;
sign_ed25519(seed.as_slice(), message)
.map(|s| s.to_vec())
.map_err(|err| crypto_error(&err))
}
async fn verify(
&self,
key_id: &str,
message: &[u8],
signature: &[u8],
) -> Result<bool, BackendError> {
let public = self.public_key(key_id).await?;
verify_ed25519(&public, message, signature).map_err(|err| crypto_error(&err))
}
async fn encrypt(
&self,
key_id: &str,
algorithm: AeadAlgorithm,
plaintext: &[u8],
aad: Option<&[u8]>,
) -> Result<CiphertextEnvelope, BackendError> {
let key = self.get_secret(key_id)?;
encrypt_aead(key.as_slice(), algorithm, plaintext, aad).map_err(|err| crypto_error(&err))
}
async fn decrypt(
&self,
key_id: &str,
envelope: &CiphertextEnvelope,
aad: Option<&[u8]>,
) -> Result<Vec<u8>, BackendError> {
let key = self.get_secret(key_id)?;
decrypt_aead(key.as_slice(), envelope, aad)
.map(|plaintext| plaintext.to_vec())
.map_err(|err| crypto_error(&err))
}
async fn kv_get(&self, key_id: &str, version: Option<u32>) -> Result<KvValue, BackendError> {
if let Some(v) = version
&& v != keystore_version()
{
return Err(BackendError::KeyNotFound(key_id.to_owned()));
}
Ok(KvValue {
value: self.store.get(key_id).map_err(store_error)?,
version: keystore_version(),
})
}
async fn kv_get_secret(
&self,
key_id: &str,
version: Option<u32>,
) -> Result<KvSecret, BackendError> {
if let Some(v) = version
&& v != keystore_version()
{
return Err(BackendError::KeyNotFound(key_id.to_owned()));
}
Ok(KvSecret {
value: self.get_secret(key_id)?,
version: keystore_version(),
})
}
async fn kv_put(&self, key_id: &str, value: &[u8]) -> Result<u32, BackendError> {
self.store.put(key_id, value).map_err(store_error)?;
Ok(keystore_version())
}
}
fn store_error(err: StoreError) -> BackendError {
match err {
StoreError::NotFound(key) => BackendError::KeyNotFound(key),
StoreError::Backend(summary) => BackendError::Backend(summary),
StoreError::NonUtf8Value => BackendError::Backend("non-utf8-secret-for-provider".into()),
}
}
fn crypto_error(err: &CryptoError) -> BackendError {
match err {
CryptoError::DecryptFailed => BackendError::DecryptFailed,
CryptoError::UnsupportedAlgorithm(algorithm) => {
BackendError::UnsupportedAlgorithm(*algorithm)
}
CryptoError::BadKeyLength { .. }
| CryptoError::BadSignatureLength { .. }
| CryptoError::BadNonceLength { .. }
| CryptoError::EncryptFailed => BackendError::Backend(err.to_string()),
}
}
#[cfg(all(test, feature = "db-keystore"))]
fn unique_temp_path(stem: &str, ext: &str) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"basil-keystore-e2e-{stem}-{}-{n}.{ext}",
std::process::id()
))
}
#[cfg(all(test, feature = "db-keystore"))]
mod db_keystore_e2e {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::{AeadAlgorithm, Backend as _, BackendError, KeyType, KeystoreBackend, StoreConfig};
use crate::seal::{
BackendCred, CredBundle, MethodRegistry, PassphraseMethod, SlotSpec, format, open_bundle,
seal,
};
use basil_keystore_backend::verify_ed25519;
use zero_secrets::SecretArray;
use zeroize::Zeroizing;
const FAST: crate::seal::Argon2Params = crate::seal::Argon2Params {
m_cost_kib: 256,
t_cost: 1,
p_cost: 1,
};
fn seal_and_unlock_dek(dek: [u8; 32]) -> SecretArray<32> {
let mut payload = CredBundle::empty();
payload.set(
"db-keystore",
BackendCred::DbKeystoreDek {
dek: SecretArray::new(dek),
},
);
let method = PassphraseMethod::with_params(Zeroizing::new(b"e2e-pass".to_vec()), FAST);
let file = seal(
&payload,
&[SlotSpec {
method: &method,
label: "passphrase".into(),
}],
)
.unwrap();
let parsed = format::decode(&file).unwrap();
let registry = MethodRegistry::new().with(&method);
let opened = open_bundle(&parsed, ®istry).unwrap();
opened
.backends
.get("db-keystore")
.and_then(|cred| match cred {
BackendCred::DbKeystoreDek { dek } => Some(dek.clone()),
_ => None,
})
.expect("sealed DbKeystoreDek must round-trip")
}
#[tokio::test]
async fn unlock_then_materialize_sign_encrypt_decrypt() {
let dek = seal_and_unlock_dek([0x33u8; 32]);
let path = super::unique_temp_path("db", "db");
let backend = KeystoreBackend::open(StoreConfig::DbKeystore {
path: path.clone(),
cipher: "aegis256".to_string(),
dek,
})
.expect("open encrypted db-keystore backend from the unlocked DEK");
let new_key = backend
.create_named_key("sign-key", KeyType::Ed25519)
.await
.expect("provision an Ed25519 key in the store");
let message = b"db-keystore e2e materialize-to-sign";
let signature = backend.sign("sign-key", message).await.expect("sign");
assert!(
backend
.verify("sign-key", message, &signature)
.await
.unwrap(),
"the broker verifies its own signature"
);
assert!(
!backend
.verify("sign-key", b"other message", &signature)
.await
.unwrap(),
"a signature must not verify over a different message"
);
assert!(
verify_ed25519(&new_key.public_key, message, &signature).unwrap(),
"the signature verifies under the key's derived public half"
);
backend
.create_named_aead("aead-key", AeadAlgorithm::Aes256Gcm)
.await
.expect("provision an AEAD key");
let plaintext = b"db-keystore e2e materialize-to-use aead";
let mut envelope = backend
.encrypt("aead-key", AeadAlgorithm::Aes256Gcm, plaintext, None)
.await
.expect("encrypt");
let recovered = backend
.decrypt("aead-key", &envelope, None)
.await
.expect("decrypt");
assert_eq!(recovered.as_slice(), plaintext.as_slice());
envelope.ciphertext[0] ^= 0x01;
assert!(
matches!(
backend.decrypt("aead-key", &envelope, None).await,
Err(BackendError::DecryptFailed)
),
"a tampered envelope must fail closed"
);
drop(backend);
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn wrong_dek_cannot_open_the_sealed_store() {
let path = super::unique_temp_path("db-wrongdek", "db");
let dek_a = seal_and_unlock_dek([0x01u8; 32]);
{
let backend = KeystoreBackend::open(StoreConfig::DbKeystore {
path: path.clone(),
cipher: "aegis256".to_string(),
dek: dek_a,
})
.expect("open with the correct DEK");
backend
.create_named_aead("aead-key", AeadAlgorithm::Aes256Gcm)
.await
.expect("provision a key");
drop(backend);
}
let dek_b = seal_and_unlock_dek([0x02u8; 32]);
if let Ok(backend) = KeystoreBackend::open(StoreConfig::DbKeystore {
path: path.clone(),
cipher: "aegis256".to_string(),
dek: dek_b,
}) {
let err = backend
.encrypt("aead-key", AeadAlgorithm::Aes256Gcm, b"x", None)
.await;
assert!(err.is_err(), "wrong-DEK read must fail closed");
}
let _ = std::fs::remove_file(&path);
}
}