use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionConfig {
pub enabled: bool,
pub key_source: KeySource,
pub algorithm: EncryptionAlgorithm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeySource {
File(PathBuf),
EnvVar(String),
Keyring(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EncryptionAlgorithm {
Aes256Gcm,
Aes256Cbc,
}
impl Default for EncryptionConfig {
fn default() -> Self {
Self {
enabled: false,
key_source: KeySource::EnvVar("CRAWLKIT_ENCRYPTION_KEY".to_string()),
algorithm: EncryptionAlgorithm::Aes256Gcm,
}
}
}
pub struct EncryptionManager {
config: EncryptionConfig,
initialized: Arc<RwLock<bool>>,
key: Arc<RwLock<Option<Vec<u8>>>>,
}
impl EncryptionManager {
#[must_use]
pub fn new(config: EncryptionConfig) -> Self {
Self {
config,
initialized: Arc::new(RwLock::new(false)),
key: Arc::new(RwLock::new(None)),
}
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn initialize(&self) -> Result<(), EncryptionError> {
if !self.config.enabled {
return Ok(());
}
let key = self.load_key()?;
if key.len() != 32 {
return Err(EncryptionError::InvalidKeyFormat(format!(
"Expected 32 bytes, got {} bytes",
key.len()
)));
}
*self.key.write() = Some(key);
*self.initialized.write() = true;
Ok(())
}
fn load_key(&self) -> Result<Vec<u8>, EncryptionError> {
match &self.config.key_source {
KeySource::File(path) => std::fs::read(path).map_err(|e| {
EncryptionError::KeyNotFound(format!(
"Failed to read key file {}: {}",
path.display(),
e
))
}),
KeySource::EnvVar(var_name) => {
std::env::var(var_name)
.map(|v| v.into_bytes())
.map_err(|_| {
EncryptionError::KeyNotFound(format!(
"Environment variable {} not set",
var_name
))
})
}
KeySource::Keyring(service) => {
let env_key = format!(
"CRAWLKIT_KEYRING_{}",
service.to_uppercase().replace('-', "_")
);
if let Ok(key) = std::env::var(&env_key) {
return Ok(key.into_bytes());
}
let keyring_path = dirs::home_dir()
.map(|h| {
h.join(".crawlkit")
.join("keyrings")
.join(format!("{}.key", service))
})
.ok_or_else(|| {
EncryptionError::KeyNotFound("Cannot determine home directory".to_string())
})?;
if keyring_path.exists() {
std::fs::read(&keyring_path).map_err(|e| {
EncryptionError::KeyNotFound(format!(
"Failed to read keyring file {}: {}",
keyring_path.display(),
e
))
})
} else {
Err(EncryptionError::KeyNotFound(format!(
"Keyring '{}' not found. Set {} environment variable or create keyring file at {}",
service, env_key, keyring_path.display()
)))
}
}
}
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if !self.config.enabled {
return Ok(plaintext.to_vec());
}
let key = self.key.read();
let key = key.as_ref().ok_or_else(|| {
EncryptionError::InitializationFailed("Encryption not initialized".to_string())
})?;
let mut nonce = [0u8; 12];
getrandom::getrandom(&mut nonce).map_err(|e| {
EncryptionError::InitializationFailed(format!("Failed to generate nonce: {}", e))
})?;
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|e| EncryptionError::InvalidKeyFormat(format!("Invalid key: {}", e)))?;
let nonce = Nonce::from_slice(&nonce);
let ciphertext = cipher.encrypt(nonce, plaintext).map_err(|e| {
EncryptionError::InitializationFailed(format!("Encryption failed: {}", e))
})?;
let mut result = nonce.to_vec();
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
if !self.config.enabled {
return Ok(ciphertext.to_vec());
}
let key = self.key.read();
let key = key.as_ref().ok_or_else(|| {
EncryptionError::InitializationFailed("Encryption not initialized".to_string())
})?;
if ciphertext.len() < 12 {
return Err(EncryptionError::InvalidKeyFormat(
"Ciphertext too short".to_string(),
));
}
let (nonce_bytes, actual_ciphertext) = ciphertext.split_at(12);
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
let cipher = Aes256Gcm::new_from_slice(key)
.map_err(|e| EncryptionError::InvalidKeyFormat(format!("Invalid key: {}", e)))?;
let nonce = Nonce::from_slice(nonce_bytes);
let plaintext = cipher.decrypt(nonce, actual_ciphertext).map_err(|e| {
EncryptionError::InitializationFailed(format!("Decryption failed: {}", e))
})?;
Ok(plaintext)
}
#[must_use]
pub fn is_initialized(&self) -> bool {
*self.initialized.read()
}
#[must_use]
pub fn config(&self) -> &EncryptionConfig {
&self.config
}
}
#[derive(Debug, thiserror::Error)]
pub enum EncryptionError {
#[error("encryption key not found: {0}")]
KeyNotFound(String),
#[error("invalid key format: {0}")]
InvalidKeyFormat(String),
#[error("encryption initialization failed: {0}")]
InitializationFailed(String),
}
impl Default for EncryptionManager {
fn default() -> Self {
Self::new(EncryptionConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encryption_config_default() {
let config = EncryptionConfig::default();
assert!(!config.enabled);
}
#[test]
fn test_encryption_manager_disabled() {
let manager = EncryptionManager::default();
assert!(!manager.is_enabled());
assert!(manager.initialize().is_ok());
}
#[test]
fn test_encryption_decrypt_roundtrip() {
let key = vec![0u8; 32];
let config = EncryptionConfig {
enabled: true,
key_source: KeySource::EnvVar("TEST_KEY".to_string()),
algorithm: EncryptionAlgorithm::Aes256Gcm,
};
let manager = EncryptionManager::new(config);
*manager.key.write() = Some(key);
*manager.initialized.write() = true;
let plaintext = b"Hello, World!";
let ciphertext = manager.encrypt(plaintext).unwrap();
let decrypted = manager.decrypt(&ciphertext).unwrap();
assert_eq!(plaintext.to_vec(), decrypted);
}
#[test]
fn test_encryption_different_plaintexts() {
let key = vec![42u8; 32];
let config = EncryptionConfig {
enabled: true,
key_source: KeySource::EnvVar("TEST_KEY".to_string()),
algorithm: EncryptionAlgorithm::Aes256Gcm,
};
let manager = EncryptionManager::new(config);
*manager.key.write() = Some(key);
*manager.initialized.write() = true;
let ct1 = manager.encrypt(b"message1").unwrap();
let ct2 = manager.encrypt(b"message2").unwrap();
assert_ne!(ct1, ct2);
}
#[test]
fn test_encryption_disabled_passthrough() {
let manager = EncryptionManager::default();
let data = b"test data";
let encrypted = manager.encrypt(data).unwrap();
let decrypted = manager.decrypt(&encrypted).unwrap();
assert_eq!(data.to_vec(), decrypted);
}
}