use crate::core::config::Config;
#[cfg(any(feature = "aws", feature = "gcp"))]
use crate::error::ConfigError;
use crate::error::{CipherError, Result};
use tracing::debug;
#[derive(Debug)]
pub enum CipherBackend {
Age,
#[cfg(feature = "aws")]
AwsKms { key_id: String },
#[cfg(feature = "gcp")]
GcpKms { resource: String },
#[cfg(feature = "gpg")]
Gpg,
}
#[allow(dead_code)] impl CipherBackend {
pub fn from_config(config: &Config) -> Result<Self> {
let cipher_type = config.dugout.cipher.as_deref().unwrap_or("age");
debug!(cipher = %cipher_type, "creating cipher backend");
match cipher_type {
"age" => Ok(Self::Age),
"aws-kms" => {
#[cfg(feature = "aws")]
{
let key_id =
config
.dugout
.kms_key_id
.clone()
.ok_or(ConfigError::MissingField {
field: "kms_key_id",
})?;
Ok(Self::AwsKms { key_id })
}
#[cfg(not(feature = "aws"))]
{
Err(CipherError::EncryptionFailed(
"AWS KMS support not compiled. Rebuild with: cargo install dugout --features aws".to_string()
).into())
}
}
"gcp-kms" => {
#[cfg(feature = "gcp")]
{
let resource =
config
.dugout
.gcp_resource
.clone()
.ok_or(ConfigError::MissingField {
field: "gcp_resource",
})?;
Ok(Self::GcpKms { resource })
}
#[cfg(not(feature = "gcp"))]
{
Err(CipherError::EncryptionFailed(
"GCP KMS support not compiled. Rebuild with: cargo install dugout --features gcp".to_string()
).into())
}
}
"gpg" => {
#[cfg(feature = "gpg")]
{
Ok(Self::Gpg)
}
#[cfg(not(feature = "gpg"))]
{
Err(CipherError::EncryptionFailed(
"GPG support not compiled. Rebuild with: cargo install dugout --features gpg".to_string()
).into())
}
}
other => Err(CipherError::EncryptionFailed(format!(
"unknown cipher type: {}. Supported: age, aws-kms, gcp-kms, gpg",
other
))
.into()),
}
}
pub fn encrypt(&self, plaintext: &str, recipients: &[String]) -> Result<String> {
use super::Cipher;
match self {
Self::Age => {
let age_recipients: Result<Vec<_>> = recipients
.iter()
.map(|r| super::parse_recipient(r))
.collect();
let age_recipients = age_recipients?;
super::Age.encrypt(plaintext, &age_recipients)
}
#[cfg(feature = "aws")]
Self::AwsKms { key_id } => {
let kms = super::aws::AwsKms::new(key_id.clone());
kms.encrypt(plaintext, &[])
}
#[cfg(feature = "gcp")]
Self::GcpKms { resource } => {
let gcp = super::gcp::GcpKms::new(resource.clone());
gcp.encrypt(plaintext, &[])
}
#[cfg(feature = "gpg")]
Self::Gpg => super::gpg::Gpg.encrypt(plaintext, recipients),
}
}
pub fn decrypt(&self, ciphertext: &str, identity: &age::x25519::Identity) -> Result<String> {
use super::Cipher;
match self {
Self::Age => super::Age.decrypt(ciphertext, identity),
#[cfg(feature = "aws")]
Self::AwsKms { .. } => {
let kms = super::aws::AwsKms::new(String::new()); kms.decrypt(ciphertext, &())
}
#[cfg(feature = "gcp")]
Self::GcpKms { resource } => {
let gcp = super::gcp::GcpKms::new(resource.clone());
gcp.decrypt(ciphertext, &())
}
#[cfg(feature = "gpg")]
Self::Gpg => super::gpg::Gpg.decrypt(ciphertext, &()),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Age => "age",
#[cfg(feature = "aws")]
Self::AwsKms { .. } => "aws-kms",
#[cfg(feature = "gcp")]
Self::GcpKms { .. } => "gcp-kms",
#[cfg(feature = "gpg")]
Self::Gpg => "gpg",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_from_config_default_age() {
let config = Config::new();
let backend = CipherBackend::from_config(&config).unwrap();
assert_eq!(backend.name(), "age");
}
#[test]
fn test_backend_from_config_explicit_age() {
let mut config = Config::new();
config.dugout.cipher = Some("age".to_string());
let backend = CipherBackend::from_config(&config).unwrap();
assert_eq!(backend.name(), "age");
}
#[test]
#[cfg(feature = "aws")]
fn test_backend_from_config_aws_kms() {
let mut config = Config::new();
config.dugout.cipher = Some("aws-kms".to_string());
config.dugout.kms_key_id = Some("arn:aws:kms:us-east-1:123456789012:key/test".to_string());
let backend = CipherBackend::from_config(&config).unwrap();
assert_eq!(backend.name(), "aws-kms");
}
#[test]
#[cfg(feature = "aws")]
fn test_backend_from_config_aws_kms_missing_key() {
let mut config = Config::new();
config.dugout.cipher = Some("aws-kms".to_string());
let result = CipherBackend::from_config(&config);
assert!(result.is_err());
}
#[test]
fn test_backend_from_config_unknown_cipher() {
let mut config = Config::new();
config.dugout.cipher = Some("unknown".to_string());
let result = CipherBackend::from_config(&config);
assert!(result.is_err());
}
#[test]
fn test_backend_encrypt_decrypt_age() {
let config = Config::new();
let backend = CipherBackend::from_config(&config).unwrap();
let identity = age::x25519::Identity::generate();
let recipient = identity.to_public().to_string();
let plaintext = "test secret";
let encrypted = backend.encrypt(plaintext, &[recipient]).unwrap();
let decrypted = backend.decrypt(&encrypted, &identity).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
#[cfg(not(feature = "aws"))]
fn test_backend_aws_kms_not_compiled() {
let mut config = Config::new();
config.dugout.cipher = Some("aws-kms".to_string());
config.dugout.kms_key_id = Some("test-key".to_string());
let result = CipherBackend::from_config(&config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not compiled"));
}
#[test]
#[cfg(not(feature = "gcp"))]
fn test_backend_gcp_kms_not_compiled() {
let mut config = Config::new();
config.dugout.cipher = Some("gcp-kms".to_string());
config.dugout.gcp_resource = Some("test-resource".to_string());
let result = CipherBackend::from_config(&config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not compiled"));
}
#[test]
#[cfg(not(feature = "gpg"))]
fn test_backend_gpg_not_compiled() {
let mut config = Config::new();
config.dugout.cipher = Some("gpg".to_string());
let result = CipherBackend::from_config(&config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not compiled"));
}
}