pub mod engine;
pub mod key_manager;
pub use engine::EncryptionEngine;
pub use key_manager::{
EncryptionKey, ExternalKmsConfig, KeyAuditRecord, KeyDerivationConfig, KeyManager,
KeyManagerConfig, KeyManagerStats, KeyOperation, KeyPurpose, KeyStatus, parse_algorithm,
parse_key_purpose, parse_key_source, parse_key_status,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
use base64::Engine;
#[derive(Error, Debug)]
pub enum EncryptionError {
#[error("Key management error: {0}")]
KeyManagement(String),
#[error("Encryption failed: {0}")]
EncryptionFailed(String),
#[error("Decryption failed: {0}")]
DecryptionFailed(String),
#[error("Invalid configuration: {0}")]
InvalidConfiguration(String),
#[error("Field processing error: {0}")]
FieldProcessing(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Base64 error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Cryptographic error: {0}")]
Cryptographic(String),
#[error("Database error: {0}")]
DatabaseError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EncryptionAlgorithm {
AES256GCM,
ChaCha20Poly1305,
}
impl Default for EncryptionAlgorithm {
fn default() -> Self {
Self::AES256GCM
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KeySource {
Static(String),
Environment(String),
External(String),
Generated(String),
}
impl Default for KeySource {
fn default() -> Self {
Self::Environment("HAMMERWORK_ENCRYPTION_KEY".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionConfig {
pub algorithm: EncryptionAlgorithm,
pub key_source: KeySource,
pub key_rotation_enabled: bool,
pub key_rotation_interval: Option<Duration>,
pub default_retention: Option<Duration>,
pub compression_enabled: bool,
pub key_id: Option<String>,
pub version: u32,
}
impl EncryptionConfig {
pub fn new(algorithm: EncryptionAlgorithm) -> Self {
Self {
algorithm,
key_source: KeySource::default(),
key_rotation_enabled: false,
key_rotation_interval: None,
default_retention: None,
compression_enabled: false,
key_id: None,
version: 1,
}
}
pub fn with_key_source(mut self, key_source: KeySource) -> Self {
self.key_source = key_source;
self
}
pub fn with_key_rotation_enabled(mut self, enabled: bool) -> Self {
self.key_rotation_enabled = enabled;
self
}
pub fn with_key_rotation_interval(mut self, interval: Duration) -> Self {
self.key_rotation_interval = Some(interval);
self
}
pub fn with_default_retention(mut self, retention: Duration) -> Self {
self.default_retention = Some(retention);
self
}
pub fn with_compression_enabled(mut self, enabled: bool) -> Self {
self.compression_enabled = enabled;
self
}
pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
self.key_id = Some(key_id.into());
self
}
pub fn key_size_bytes(&self) -> usize {
match self.algorithm {
EncryptionAlgorithm::AES256GCM => 32, EncryptionAlgorithm::ChaCha20Poly1305 => 32, }
}
pub fn nonce_size_bytes(&self) -> usize {
match self.algorithm {
EncryptionAlgorithm::AES256GCM => 12, EncryptionAlgorithm::ChaCha20Poly1305 => 12, }
}
pub fn tag_size_bytes(&self) -> usize {
match self.algorithm {
EncryptionAlgorithm::AES256GCM => 16, EncryptionAlgorithm::ChaCha20Poly1305 => 16, }
}
}
impl Default for EncryptionConfig {
fn default() -> Self {
Self::new(EncryptionAlgorithm::default())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RetentionPolicy {
DeleteAfter(Duration),
DeleteAt(DateTime<Utc>),
KeepIndefinitely,
DeleteImmediately,
UseDefault,
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self::UseDefault
}
}
impl RetentionPolicy {
pub fn calculate_deletion_time(
&self,
created_at: DateTime<Utc>,
completed_at: Option<DateTime<Utc>>,
default_retention: Option<Duration>,
) -> Option<DateTime<Utc>> {
match self {
RetentionPolicy::DeleteAfter(duration) => {
let duration_chrono = chrono::Duration::from_std(*duration).ok()?;
Some(created_at + duration_chrono)
}
RetentionPolicy::DeleteAt(timestamp) => Some(*timestamp),
RetentionPolicy::KeepIndefinitely => None,
RetentionPolicy::DeleteImmediately => completed_at.or(Some(created_at)),
RetentionPolicy::UseDefault => {
if let Some(default_duration) = default_retention {
let duration_chrono = chrono::Duration::from_std(default_duration).ok()?;
Some(created_at + duration_chrono)
} else {
None
}
}
}
}
pub fn should_delete_now(
&self,
created_at: DateTime<Utc>,
completed_at: Option<DateTime<Utc>>,
default_retention: Option<Duration>,
) -> bool {
match self.calculate_deletion_time(created_at, completed_at, default_retention) {
Some(delete_at) => Utc::now() >= delete_at,
None => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionMetadata {
pub algorithm: EncryptionAlgorithm,
pub key_id: String,
pub config_version: u32,
pub compressed: bool,
pub encrypted_fields: Vec<String>,
pub retention_policy: RetentionPolicy,
pub encrypted_at: DateTime<Utc>,
pub delete_at: Option<DateTime<Utc>>,
pub payload_hash: String,
}
impl EncryptionMetadata {
pub fn new(
config: &EncryptionConfig,
encrypted_fields: Vec<String>,
retention_policy: RetentionPolicy,
payload_hash: String,
) -> Self {
let now = Utc::now();
let delete_at =
retention_policy.calculate_deletion_time(now, None, config.default_retention);
Self {
algorithm: config.algorithm.clone(),
key_id: config
.key_id
.clone()
.unwrap_or_else(|| "default".to_string()),
config_version: config.version,
compressed: config.compression_enabled,
encrypted_fields,
retention_policy,
encrypted_at: now,
delete_at,
payload_hash,
}
}
pub fn should_delete_now(&self) -> bool {
match self.delete_at {
Some(delete_at) => Utc::now() >= delete_at,
None => false,
}
}
pub fn update_deletion_time(
&mut self,
completed_at: DateTime<Utc>,
default_retention: Option<Duration>,
) {
self.delete_at = self.retention_policy.calculate_deletion_time(
self.encrypted_at,
Some(completed_at),
default_retention,
);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedPayload {
pub ciphertext: String,
pub nonce: String,
pub tag: String,
pub metadata: EncryptionMetadata,
}
impl EncryptedPayload {
pub fn new(
ciphertext: Vec<u8>,
nonce: Vec<u8>,
tag: Vec<u8>,
metadata: EncryptionMetadata,
) -> Self {
Self {
ciphertext: base64::engine::general_purpose::STANDARD.encode(ciphertext),
nonce: base64::engine::general_purpose::STANDARD.encode(nonce),
tag: base64::engine::general_purpose::STANDARD.encode(tag),
metadata,
}
}
pub fn decode_ciphertext(&self) -> Result<Vec<u8>, EncryptionError> {
base64::engine::general_purpose::STANDARD
.decode(&self.ciphertext)
.map_err(EncryptionError::Base64)
}
pub fn decode_nonce(&self) -> Result<Vec<u8>, EncryptionError> {
base64::engine::general_purpose::STANDARD
.decode(&self.nonce)
.map_err(EncryptionError::Base64)
}
pub fn decode_tag(&self) -> Result<Vec<u8>, EncryptionError> {
base64::engine::general_purpose::STANDARD
.decode(&self.tag)
.map_err(EncryptionError::Base64)
}
pub fn should_delete_now(&self) -> bool {
self.metadata.should_delete_now()
}
pub fn size_bytes(&self) -> usize {
self.ciphertext.len() + self.nonce.len() + self.tag.len()
}
pub fn contains_pii_field(&self, field_name: &str) -> bool {
self.metadata
.encrypted_fields
.contains(&field_name.to_string())
}
pub fn pii_fields(&self) -> &[String] {
&self.metadata.encrypted_fields
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EncryptionStats {
pub jobs_encrypted: u64,
pub jobs_decrypted: u64,
pub pii_fields_encrypted: u64,
pub total_encrypted_bytes: u64,
pub total_decrypted_bytes: u64,
pub encryption_errors: u64,
pub decryption_errors: u64,
pub key_rotations: u64,
pub retention_cleanups: u64,
pub avg_encryption_time_ms: f64,
pub avg_decryption_time_ms: f64,
pub algorithm_usage: HashMap<String, u64>,
pub last_updated: DateTime<Utc>,
}
impl EncryptionStats {
pub fn new() -> Self {
Self {
last_updated: Utc::now(),
..Default::default()
}
}
pub fn record_encryption(
&mut self,
algorithm: &EncryptionAlgorithm,
size_bytes: usize,
duration_ms: f64,
) {
self.jobs_encrypted += 1;
self.total_encrypted_bytes += size_bytes as u64;
if self.jobs_encrypted == 1 {
self.avg_encryption_time_ms = duration_ms;
} else {
let total_time =
self.avg_encryption_time_ms * (self.jobs_encrypted - 1) as f64 + duration_ms;
self.avg_encryption_time_ms = total_time / self.jobs_encrypted as f64;
}
let algo_name = format!("{:?}", algorithm);
*self.algorithm_usage.entry(algo_name).or_insert(0) += 1;
self.last_updated = Utc::now();
}
pub fn record_decryption(&mut self, size_bytes: usize, duration_ms: f64) {
self.jobs_decrypted += 1;
self.total_decrypted_bytes += size_bytes as u64;
if self.jobs_decrypted == 1 {
self.avg_decryption_time_ms = duration_ms;
} else {
let total_time =
self.avg_decryption_time_ms * (self.jobs_decrypted - 1) as f64 + duration_ms;
self.avg_decryption_time_ms = total_time / self.jobs_decrypted as f64;
}
self.last_updated = Utc::now();
}
pub fn record_pii_encryption(&mut self, field_count: usize) {
self.pii_fields_encrypted += field_count as u64;
self.last_updated = Utc::now();
}
pub fn record_encryption_error(&mut self) {
self.encryption_errors += 1;
self.last_updated = Utc::now();
}
pub fn record_decryption_error(&mut self) {
self.decryption_errors += 1;
self.last_updated = Utc::now();
}
pub fn record_key_rotation(&mut self) {
self.key_rotations += 1;
self.last_updated = Utc::now();
}
pub fn record_retention_cleanup(&mut self, jobs_cleaned: u64) {
self.retention_cleanups += jobs_cleaned;
self.last_updated = Utc::now();
}
pub fn encryption_success_rate(&self) -> f64 {
let total_attempts = self.jobs_encrypted + self.encryption_errors;
if total_attempts == 0 {
100.0
} else {
(self.jobs_encrypted as f64 / total_attempts as f64) * 100.0
}
}
pub fn decryption_success_rate(&self) -> f64 {
let total_attempts = self.jobs_decrypted + self.decryption_errors;
if total_attempts == 0 {
100.0
} else {
(self.jobs_decrypted as f64 / total_attempts as f64) * 100.0
}
}
pub fn most_used_algorithm(&self) -> Option<String> {
self.algorithm_usage
.iter()
.max_by_key(|(_, count)| *count)
.map(|(algo, _)| algo.clone())
}
}
pub fn generate_deterministic_key(service_prefix: &str, inputs: &[&str]) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(service_prefix.as_bytes());
for input in inputs {
hasher.update(input.as_bytes());
}
let hash = hasher.finalize();
hash[0..32].to_vec()
}
pub fn generate_deterministic_key_with_size(
service_prefix: &str,
inputs: &[&str],
key_size: usize,
) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(service_prefix.as_bytes());
for input in inputs {
hasher.update(input.as_bytes());
}
let hash = hasher.finalize();
let mut key = vec![0u8; key_size];
let hash_bytes = hash.as_slice();
for (i, byte) in key.iter_mut().enumerate() {
*byte = hash_bytes[i % hash_bytes.len()];
}
key
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_encryption_config_creation() {
let config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM);
assert_eq!(config.algorithm, EncryptionAlgorithm::AES256GCM);
assert!(!config.key_rotation_enabled);
assert!(!config.compression_enabled);
assert_eq!(config.version, 1);
}
#[test]
fn test_encryption_config_builder() {
let config = EncryptionConfig::new(EncryptionAlgorithm::ChaCha20Poly1305)
.with_key_source(KeySource::Environment("TEST_KEY".to_string()))
.with_key_rotation_enabled(true)
.with_compression_enabled(true)
.with_key_id("test-key-1");
assert_eq!(config.algorithm, EncryptionAlgorithm::ChaCha20Poly1305);
assert_eq!(
config.key_source,
KeySource::Environment("TEST_KEY".to_string())
);
assert!(config.key_rotation_enabled);
assert!(config.compression_enabled);
assert_eq!(config.key_id, Some("test-key-1".to_string()));
}
#[test]
fn test_algorithm_key_sizes() {
let aes_config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM);
assert_eq!(aes_config.key_size_bytes(), 32);
assert_eq!(aes_config.nonce_size_bytes(), 12);
assert_eq!(aes_config.tag_size_bytes(), 16);
let chacha_config = EncryptionConfig::new(EncryptionAlgorithm::ChaCha20Poly1305);
assert_eq!(chacha_config.key_size_bytes(), 32);
assert_eq!(chacha_config.nonce_size_bytes(), 12);
assert_eq!(chacha_config.tag_size_bytes(), 16);
}
#[test]
fn test_retention_policy_calculation() {
let now = Utc::now();
let one_day = Duration::from_secs(24 * 60 * 60);
let policy = RetentionPolicy::DeleteAfter(one_day);
let delete_time = policy.calculate_deletion_time(now, None, None).unwrap();
let expected = now + chrono::Duration::from_std(one_day).unwrap();
assert!((delete_time - expected).num_seconds().abs() < 1);
}
#[test]
fn test_retention_policy_should_delete() {
let past = Utc::now() - chrono::Duration::seconds(2);
let policy = RetentionPolicy::DeleteAfter(Duration::from_secs(1));
assert!(policy.should_delete_now(past, None, None));
let future = Utc::now() + chrono::Duration::seconds(10);
assert!(!policy.should_delete_now(future, None, None));
}
#[test]
fn test_encryption_metadata_creation() {
let config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM).with_key_id("test-key");
let fields = vec!["ssn".to_string(), "credit_card".to_string()];
let policy = RetentionPolicy::DeleteAfter(Duration::from_secs(86400));
let metadata =
EncryptionMetadata::new(&config, fields.clone(), policy, "hash123".to_string());
assert_eq!(metadata.algorithm, EncryptionAlgorithm::AES256GCM);
assert_eq!(metadata.key_id, "test-key");
assert_eq!(metadata.encrypted_fields, fields);
assert_eq!(metadata.payload_hash, "hash123");
assert!(!metadata.compressed);
}
#[test]
fn test_encrypted_payload_creation() {
let config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM);
let metadata = EncryptionMetadata::new(
&config,
vec!["test_field".to_string()],
RetentionPolicy::KeepIndefinitely,
"hash".to_string(),
);
let payload = EncryptedPayload::new(
b"encrypted_data".to_vec(),
b"nonce123".to_vec(),
b"tag12345".to_vec(),
metadata,
);
assert!(payload.contains_pii_field("test_field"));
assert!(!payload.contains_pii_field("other_field"));
assert_eq!(payload.pii_fields().len(), 1);
assert!(!payload.should_delete_now());
}
#[test]
fn test_encryption_stats() {
let mut stats = EncryptionStats::new();
stats.record_encryption(&EncryptionAlgorithm::AES256GCM, 1024, 10.5);
stats.record_encryption(&EncryptionAlgorithm::AES256GCM, 2048, 15.0);
stats.record_pii_encryption(2);
assert_eq!(stats.jobs_encrypted, 2);
assert_eq!(stats.total_encrypted_bytes, 3072);
assert_eq!(stats.pii_fields_encrypted, 2);
assert_eq!(stats.avg_encryption_time_ms, 12.75); assert_eq!(stats.encryption_success_rate(), 100.0);
stats.record_encryption_error();
assert!(stats.encryption_success_rate() < 100.0);
}
#[test]
fn test_key_source_equality() {
assert_eq!(
KeySource::Environment("KEY1".to_string()),
KeySource::Environment("KEY1".to_string())
);
assert_ne!(
KeySource::Environment("KEY1".to_string()),
KeySource::Environment("KEY2".to_string())
);
assert_ne!(
KeySource::Environment("KEY1".to_string()),
KeySource::Static("KEY1".to_string())
);
}
#[test]
fn test_serialization() {
let config = EncryptionConfig::new(EncryptionAlgorithm::ChaCha20Poly1305)
.with_key_rotation_enabled(true)
.with_compression_enabled(true);
let serialized = serde_json::to_string(&config).unwrap();
let deserialized: EncryptionConfig = serde_json::from_str(&serialized).unwrap();
assert_eq!(config.algorithm, deserialized.algorithm);
assert_eq!(
config.key_rotation_enabled,
deserialized.key_rotation_enabled
);
assert_eq!(config.compression_enabled, deserialized.compression_enabled);
}
}