Files
foxhunt/services/ml_training_service/src/encryption.rs
jgrusewski c79cca5564 chore(clippy): add deny(unwrap_used) to ml_training_service, fix 58 violations
Add #![deny(clippy::unwrap_used, clippy::expect_used)] to lib.rs and main.rs.

Fix all violations by category:
- training_metrics.rs / simple_metrics.rs: file-level #![allow] with safety
  comment (Prometheus register_*!() macros with literal names are infallible)
- asset_parser.rs: function-level #[allow] for invariant regex literal expect()
- technical_indicators.rs: replace unwrap() on VecDeque::back()/get() with
  let-else early returns
- data_config.rs: bind start/end before assigning to avoid unwrap()
- data_loader.rs: convert 3x database.as_ref().expect() to .ok_or_else()?;
  fix Price construction chain with .or_else().map_err()?
- dbn_data_loader.rs: fix Price::from_f64().unwrap_or_else() chains with
  .or_else().unwrap_or_default()
- checkpoint_manager.rs: convert serde_json::to_value().unwrap() to .map_err()?
- orchestrator.rs: use unwrap_or_default() for Price in map() closures
- main.rs: fix rustls expect, metrics encoder, spawn closure error handling
- validation_pipeline.rs: fix path UTF-8 expect and last().expect() calls
- batch_tuning_manager.rs: fix current_dir().expect() with unwrap_or_else
- All test modules: add #[allow(clippy::unwrap_used, clippy::expect_used)]

Result: ml_training_service generates zero clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:56:22 +01:00

893 lines
28 KiB
Rust

//! Model Encryption Key Management with Vault Integration
//!
//! This module provides secure encryption key management for ML model storage,
//! supporting key rotation, multiple algorithms, and secure key retrieval from Vault.
use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce as AesNonce};
use chacha20poly1305::{ChaCha20Poly1305, Nonce as ChaChaNonce};
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroize;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use config::structures::EncryptionConfig;
/// Encryption keys structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionKeys {
pub primary_key: String,
pub key_id: String,
pub algorithm: String,
pub created_at: SystemTime,
}
impl EncryptionKeys {
/// Check if key should be rotated based on age
pub fn should_rotate(&self, rotation_days: u64) -> bool {
match self.created_at.elapsed() {
Ok(elapsed) => elapsed.as_secs() > rotation_days * 86400,
Err(_) => true, // If we can't determine age, assume rotation needed
}
}
}
/// Encryption key manager with secure configuration
pub struct EncryptionKeyManager {
config: EncryptionConfig,
config_loader: Option<Arc<config::database::PostgresConfigLoader>>,
cached_keys: Arc<RwLock<Option<CachedEncryptionKeys>>>,
}
/// Cached encryption keys with metadata
#[derive(Debug, Clone)]
struct CachedEncryptionKeys {
keys: EncryptionKeys,
cached_at: SystemTime,
cache_ttl_secs: u64,
}
impl CachedEncryptionKeys {
fn new(keys: EncryptionKeys, cache_ttl_secs: u64) -> Self {
Self {
keys,
cached_at: SystemTime::now(),
cache_ttl_secs,
}
}
fn is_expired(&self) -> bool {
match self.cached_at.elapsed() {
Ok(elapsed) => elapsed.as_secs() > self.cache_ttl_secs,
Err(_) => true, // If we can't determine time, assume expired
}
}
#[allow(dead_code)]
fn needs_rotation(&self, rotation_days: u64) -> bool {
self.keys.should_rotate(rotation_days)
}
}
/// Encryption algorithm configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionAlgorithmConfig {
pub algorithm: EncryptionAlgorithm,
pub key_size_bits: usize,
pub iv_size_bytes: usize,
pub tag_size_bytes: usize,
}
/// Supported encryption algorithms
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EncryptionAlgorithm {
#[serde(rename = "AES-256-GCM")]
Aes256Gcm,
#[serde(rename = "ChaCha20Poly1305")]
ChaCha20Poly1305,
#[serde(rename = "AES-256-CTR")]
Aes256Ctr,
}
impl EncryptionAlgorithm {
/// Get algorithm configuration
#[allow(dead_code)]
pub fn get_config(&self) -> EncryptionAlgorithmConfig {
match self {
Self::Aes256Gcm => EncryptionAlgorithmConfig {
algorithm: self.clone(),
key_size_bits: 256,
iv_size_bytes: 12, // 96-bit IV for GCM
tag_size_bytes: 16, // 128-bit authentication tag
},
Self::ChaCha20Poly1305 => EncryptionAlgorithmConfig {
algorithm: self.clone(),
key_size_bits: 256,
iv_size_bytes: 12, // 96-bit nonce
tag_size_bytes: 16, // 128-bit authentication tag
},
Self::Aes256Ctr => EncryptionAlgorithmConfig {
algorithm: self.clone(),
key_size_bits: 256,
iv_size_bytes: 16, // 128-bit IV for CTR
tag_size_bytes: 0, // No authentication tag (not AEAD)
},
}
}
/// Check if algorithm provides authenticated encryption
#[allow(dead_code)]
pub fn is_authenticated(&self) -> bool {
matches!(self, Self::Aes256Gcm | Self::ChaCha20Poly1305)
}
}
impl std::str::FromStr for EncryptionAlgorithm {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"AES-256-GCM" => Ok(Self::Aes256Gcm),
"ChaCha20Poly1305" => Ok(Self::ChaCha20Poly1305),
"AES-256-CTR" => Ok(Self::Aes256Ctr),
_ => Err(anyhow::anyhow!("Unsupported encryption algorithm: {}", s)),
}
}
}
impl std::fmt::Display for EncryptionAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::Aes256Gcm => "AES-256-GCM",
Self::ChaCha20Poly1305 => "ChaCha20Poly1305",
Self::Aes256Ctr => "AES-256-CTR",
};
write!(f, "{}", name)
}
}
/// Encryption metadata for stored models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionMetadata {
pub algorithm: EncryptionAlgorithm,
pub key_id: String,
pub nonce: Vec<u8>, // 96-bit nonce (12 bytes)
pub salt: Vec<u8>, // Salt for key derivation (16 bytes)
pub tag: Option<Vec<u8>>, // For AEAD algorithms
pub encrypted_at: SystemTime,
pub key_version: u32,
}
#[allow(dead_code)]
impl EncryptionKeyManager {
/// Create a new encryption key manager
pub fn new(
config: EncryptionConfig,
config_loader: Option<Arc<config::database::PostgresConfigLoader>>,
) -> Self {
Self {
config,
config_loader,
cached_keys: Arc::new(RwLock::new(None)),
}
}
/// Check if encryption is enabled
pub fn is_encryption_enabled(&self) -> bool {
self.config.enable_encryption
}
/// Get current encryption algorithm
pub fn get_algorithm(&self) -> Result<EncryptionAlgorithm> {
self.config.algorithm.parse()
}
/// Load encryption keys from secure configuration or fallback source
pub async fn load_encryption_keys(&self) -> Result<EncryptionKeys> {
// Check cache first
{
let cached_guard = self.cached_keys.read().await;
if let Some(cached) = cached_guard.as_ref() {
if !cached.is_expired() {
debug!("Using cached encryption keys");
return Ok(cached.keys.clone());
}
}
}
// Load from fallback source (local file or generated)
// Note: PostgresConfigLoader doesn't provide encryption key storage
// Keys should be managed through local files or generated securely
let keys = if self.config_loader.is_some() {
debug!("Config loader available but encryption keys managed locally");
self.load_fallback_keys().await?
} else {
info!("Loading encryption keys from fallback source (no secure configuration)");
self.load_fallback_keys().await?
};
// Cache the loaded keys
{
let mut cached_guard = self.cached_keys.write().await;
*cached_guard = Some(CachedEncryptionKeys::new(keys.clone(), 300)); // 5-minute cache
}
debug!("Encryption keys loaded and cached");
Ok(keys)
}
/// Load encryption keys from fallback source (local file or generated)
async fn load_fallback_keys(&self) -> Result<EncryptionKeys> {
if let Some(key_file) = &self.config.local_key_file {
let path = PathBuf::from(key_file);
self.load_keys_from_file(&path).await
} else {
warn!("No fallback key source configured, generating temporary keys");
self.generate_temporary_keys().await
}
}
/// Generate temporary encryption keys (wrapper for generate_secure_keys)
async fn generate_temporary_keys(&self) -> Result<EncryptionKeys> {
self.generate_secure_keys().await
}
/// Load encryption keys from local file
async fn load_keys_from_file(&self, key_file: &PathBuf) -> Result<EncryptionKeys> {
let key_data = fs::read_to_string(key_file)
.await
.context("Failed to read encryption key file")?;
let keys: EncryptionKeys =
serde_json::from_str(&key_data).context("Failed to parse encryption key file")?;
info!("Loaded encryption keys from file: {}", key_file.display());
Ok(keys)
}
/// Derive encryption key from base key using PBKDF2
fn derive_key(&self, base_key: &str, salt: &[u8]) -> Result<[u8; 32]> {
let mut derived_key = [0u8; 32];
// Use PBKDF2 with 100,000 iterations (NIST recommendation)
pbkdf2_hmac::<Sha256>(base_key.as_bytes(), salt, 100_000, &mut derived_key);
Ok(derived_key)
}
/// Generate cryptographically secure random nonce
fn generate_nonce(&self, size: usize) -> Result<Vec<u8>> {
use rand::{rngs::OsRng, RngCore};
let mut nonce = vec![0u8; size];
OsRng.fill_bytes(&mut nonce);
Ok(nonce)
}
/// Generate salt for key derivation
fn generate_salt(&self) -> Result<[u8; 16]> {
use rand::{rngs::OsRng, RngCore};
let mut salt = [0u8; 16];
OsRng.fill_bytes(&mut salt);
Ok(salt)
}
/// Generate cryptographically secure encryption keys
async fn generate_secure_keys(&self) -> Result<EncryptionKeys> {
info!("Generating cryptographically secure encryption keys using OsRng");
// Use cryptographically secure random number generator
use base64::Engine;
use rand::{rngs::OsRng, RngCore};
let mut key_bytes = vec![0u8; 32];
OsRng.fill_bytes(&mut key_bytes);
let primary_key = base64::prelude::BASE64_STANDARD.encode(&key_bytes);
let keys = EncryptionKeys {
primary_key,
key_id: format!(
"temp-key-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
),
algorithm: self.config.algorithm.clone(),
created_at: SystemTime::now(),
};
Ok(keys)
}
/// Check if current keys need rotation
pub async fn keys_need_rotation(&self) -> Result<bool> {
let keys = self.load_encryption_keys().await?;
Ok(keys.should_rotate(self.config.key_rotation_days))
}
/// Request key rotation (would trigger Vault key rotation in production)
pub async fn request_key_rotation(&self) -> Result<()> {
info!("Requesting encryption key rotation");
// Clear cache to force reload of new keys
{
let mut cached_guard = self.cached_keys.write().await;
*cached_guard = None;
}
// In a production environment, this would:
// 1. Request new keys from Vault
// 2. Update Vault secrets with new keys
// 3. Notify other services of key rotation
// 4. Schedule old key deprecation
warn!("Key rotation requested - implement Vault key rotation logic for production");
Ok(())
}
/// Encrypt model data
pub async fn encrypt_model_data(&self, data: &[u8]) -> Result<(Vec<u8>, EncryptionMetadata)> {
if !self.config.enable_encryption {
return Err(anyhow::anyhow!("Encryption is disabled"));
}
let keys = self.load_encryption_keys().await?;
let algorithm = self.get_algorithm()?;
// Generate salt for key derivation
let salt = self.generate_salt()?;
// Generate random IV/nonce
let nonce = self.generate_nonce(12)?; // 96-bit nonce for GCM/ChaCha20-Poly1305
// Perform authenticated encryption
let (encrypted_data, tag) =
self.perform_encryption(data, &keys.primary_key, &nonce, &salt, &algorithm)?;
let metadata = EncryptionMetadata {
algorithm,
key_id: keys.key_id,
nonce,
salt: salt.to_vec(),
tag: Some(tag), // Authentication tag from AEAD
encrypted_at: SystemTime::now(),
key_version: 1, // Would track actual key versions
};
debug!(
"Encrypted {} bytes of model data with algorithm {}",
data.len(),
metadata.algorithm
);
Ok((encrypted_data, metadata))
}
/// Decrypt model data
pub async fn decrypt_model_data(
&self,
encrypted_data: &[u8],
metadata: &EncryptionMetadata,
) -> Result<Vec<u8>> {
if !self.config.enable_encryption {
return Err(anyhow::anyhow!("Encryption is disabled"));
}
let keys = self.load_encryption_keys().await?;
// Verify key ID matches (in production, support multiple key versions)
if keys.key_id != metadata.key_id {
return Err(anyhow::anyhow!(
"Key ID mismatch: expected {}, got {}",
keys.key_id,
metadata.key_id
));
}
let decrypted_data = self.perform_decryption(
encrypted_data,
&keys.primary_key,
&metadata.nonce,
&metadata.salt,
metadata.tag.as_deref(),
&metadata.algorithm,
)?;
debug!(
"Decrypted {} bytes of model data with algorithm {}",
decrypted_data.len(),
metadata.algorithm
);
Ok(decrypted_data)
}
/// Perform actual encryption (simplified implementation)
fn perform_encryption(
&self,
data: &[u8],
key: &str,
iv: &[u8],
salt: &[u8],
algorithm: &EncryptionAlgorithm,
) -> Result<(Vec<u8>, Vec<u8>)> {
match algorithm {
EncryptionAlgorithm::Aes256Gcm => {
// Use AES-256-GCM encryption
self.aes_gcm_encrypt(data, key, iv, salt)
},
EncryptionAlgorithm::ChaCha20Poly1305 => {
// Use ChaCha20-Poly1305 encryption
self.chacha20_encrypt(data, key, iv, salt)
},
EncryptionAlgorithm::Aes256Ctr => Err(anyhow::anyhow!(
"AES-256-CTR is not authenticated, use AES-256-GCM instead"
)),
}
}
/// Perform actual decryption (simplified implementation)
fn perform_decryption(
&self,
encrypted_data: &[u8],
key: &str,
iv: &[u8],
salt: &[u8],
tag: Option<&[u8]>,
algorithm: &EncryptionAlgorithm,
) -> Result<Vec<u8>> {
let tag = tag.ok_or_else(|| anyhow::anyhow!("Missing authentication tag for AEAD"))?;
match algorithm {
EncryptionAlgorithm::Aes256Gcm => {
// Use AES-256-GCM decryption
self.aes_gcm_decrypt(encrypted_data, key, iv, salt, tag)
},
EncryptionAlgorithm::ChaCha20Poly1305 => {
// Use ChaCha20-Poly1305 decryption
self.chacha20_decrypt(encrypted_data, key, iv, salt, tag)
},
EncryptionAlgorithm::Aes256Ctr => Err(anyhow::anyhow!(
"AES-256-CTR is not authenticated, use AES-256-GCM instead"
)),
}
}
// Production AES-256-GCM encryption
fn aes_gcm_encrypt(
&self,
data: &[u8],
base_key: &str,
nonce: &[u8],
salt: &[u8],
) -> Result<(Vec<u8>, Vec<u8>)> {
// Derive key using PBKDF2
let mut derived_key = self.derive_key(base_key, salt)?;
// Create cipher
let cipher = Aes256Gcm::new_from_slice(&derived_key)
.map_err(|e| anyhow::anyhow!("Failed to create AES-256-GCM cipher: {}", e))?;
// Create nonce (96 bits = 12 bytes)
let nonce_array = AesNonce::from_slice(nonce);
// Encrypt with additional authenticated data
let ciphertext = cipher
.encrypt(
nonce_array,
Payload {
msg: data,
aad: b"foxhunt-ml-model-v1", // Additional authenticated data
},
)
.map_err(|e| anyhow::anyhow!("AES-256-GCM encryption failed: {}", e))?;
// Zero out derived key
derived_key.zeroize();
// Split ciphertext and tag (tag is last 16 bytes)
let tag_offset = ciphertext.len() - 16;
let encrypted_data = ciphertext[..tag_offset].to_vec();
let tag = ciphertext[tag_offset..].to_vec();
info!(
"Successfully encrypted {} bytes with AES-256-GCM",
data.len()
);
Ok((encrypted_data, tag))
}
fn aes_gcm_decrypt(
&self,
encrypted_data: &[u8],
base_key: &str,
nonce: &[u8],
salt: &[u8],
tag: &[u8],
) -> Result<Vec<u8>> {
// Derive key using PBKDF2
let mut derived_key = self.derive_key(base_key, salt)?;
// Create cipher
let cipher = Aes256Gcm::new_from_slice(&derived_key)
.map_err(|e| anyhow::anyhow!("Failed to create AES-256-GCM cipher: {}", e))?;
// Create nonce
let nonce_array = AesNonce::from_slice(nonce);
// Combine ciphertext and tag
let mut ciphertext_with_tag = encrypted_data.to_vec();
ciphertext_with_tag.extend_from_slice(tag);
// Decrypt with AAD verification
let plaintext = cipher
.decrypt(
nonce_array,
Payload {
msg: &ciphertext_with_tag,
aad: b"foxhunt-ml-model-v1",
},
)
.map_err(|e| {
anyhow::anyhow!(
"AES-256-GCM decryption failed (authentication error): {}",
e
)
})?;
// Zero out derived key
derived_key.zeroize();
info!(
"Successfully decrypted {} bytes with AES-256-GCM",
plaintext.len()
);
Ok(plaintext)
}
fn chacha20_encrypt(
&self,
data: &[u8],
base_key: &str,
nonce: &[u8],
salt: &[u8],
) -> Result<(Vec<u8>, Vec<u8>)> {
// Derive key using PBKDF2
let mut derived_key = self.derive_key(base_key, salt)?;
// Create cipher
let cipher = ChaCha20Poly1305::new_from_slice(&derived_key)
.map_err(|e| anyhow::anyhow!("Failed to create ChaCha20-Poly1305 cipher: {}", e))?;
// Create nonce (96 bits = 12 bytes)
let nonce_array = ChaChaNonce::from_slice(nonce);
// Encrypt with AAD
let ciphertext = cipher
.encrypt(
nonce_array,
Payload {
msg: data,
aad: b"foxhunt-ml-model-v1",
},
)
.map_err(|e| anyhow::anyhow!("ChaCha20-Poly1305 encryption failed: {}", e))?;
// Zero out derived key
derived_key.zeroize();
// Split ciphertext and tag
let tag_offset = ciphertext.len() - 16;
let encrypted_data = ciphertext[..tag_offset].to_vec();
let tag = ciphertext[tag_offset..].to_vec();
info!(
"Successfully encrypted {} bytes with ChaCha20-Poly1305",
data.len()
);
Ok((encrypted_data, tag))
}
fn chacha20_decrypt(
&self,
encrypted_data: &[u8],
base_key: &str,
nonce: &[u8],
salt: &[u8],
tag: &[u8],
) -> Result<Vec<u8>> {
// Derive key using PBKDF2
let mut derived_key = self.derive_key(base_key, salt)?;
// Create cipher
let cipher = ChaCha20Poly1305::new_from_slice(&derived_key)
.map_err(|e| anyhow::anyhow!("Failed to create ChaCha20-Poly1305 cipher: {}", e))?;
// Create nonce
let nonce_array = ChaChaNonce::from_slice(nonce);
// Combine ciphertext and tag
let mut ciphertext_with_tag = encrypted_data.to_vec();
ciphertext_with_tag.extend_from_slice(tag);
// Decrypt with AAD verification
let plaintext = cipher
.decrypt(
nonce_array,
Payload {
msg: &ciphertext_with_tag,
aad: b"foxhunt-ml-model-v1",
},
)
.map_err(|e| {
anyhow::anyhow!(
"ChaCha20-Poly1305 decryption failed (authentication error): {}",
e
)
})?;
// Zero out derived key
derived_key.zeroize();
info!(
"Successfully decrypted {} bytes with ChaCha20-Poly1305",
plaintext.len()
);
Ok(plaintext)
}
/// Get encryption statistics
pub async fn get_encryption_stats(&self) -> Result<EncryptionStats> {
let keys = self.load_encryption_keys().await?;
let needs_rotation = keys.should_rotate(self.config.key_rotation_days);
Ok(EncryptionStats {
encryption_enabled: self.config.enable_encryption,
algorithm: self.get_algorithm()?,
key_id: keys.key_id,
key_age_days: keys
.created_at
.elapsed()
.map(|d| d.as_secs() / 86400)
.unwrap_or(0),
needs_rotation,
rotation_interval_days: self.config.key_rotation_days,
cache_hit_rate: 0.0, // Would track actual cache statistics
})
}
}
/// Encryption statistics
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub struct EncryptionStats {
pub encryption_enabled: bool,
pub algorithm: EncryptionAlgorithm,
pub key_id: String,
pub key_age_days: u64,
pub needs_rotation: bool,
pub rotation_interval_days: u64,
pub cache_hit_rate: f64,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn test_encryption_algorithm_parsing() {
assert_eq!(
"AES-256-GCM".parse::<EncryptionAlgorithm>().unwrap(),
EncryptionAlgorithm::Aes256Gcm
);
assert_eq!(
"ChaCha20Poly1305".parse::<EncryptionAlgorithm>().unwrap(),
EncryptionAlgorithm::ChaCha20Poly1305
);
assert!("Invalid-Algorithm".parse::<EncryptionAlgorithm>().is_err());
}
#[test]
fn test_algorithm_config() {
let aes_config = EncryptionAlgorithm::Aes256Gcm.get_config();
assert_eq!(aes_config.key_size_bits, 256);
assert_eq!(aes_config.iv_size_bytes, 12);
assert!(aes_config.algorithm.is_authenticated());
let ctr_config = EncryptionAlgorithm::Aes256Ctr.get_config();
assert!(!ctr_config.algorithm.is_authenticated());
}
#[tokio::test]
async fn test_encryption_key_manager_creation() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
assert!(manager.is_encryption_enabled());
assert_eq!(
manager.get_algorithm().unwrap(),
EncryptionAlgorithm::Aes256Gcm
);
}
#[tokio::test]
async fn test_temporary_key_generation() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
let keys = manager.generate_temporary_keys().await.unwrap();
assert!(!keys.primary_key.is_empty());
assert!(!keys.key_id.is_empty());
assert!(keys.key_id.starts_with("temp-key-"));
}
#[tokio::test]
async fn test_aes_gcm_encryption_decryption() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
let test_data = b"Hello, encrypted world!";
let (encrypted, metadata) = manager.encrypt_model_data(test_data).await.unwrap();
// Verify encrypted data is different from plaintext
assert_ne!(encrypted.as_slice(), test_data);
// Verify metadata
assert_eq!(metadata.algorithm, EncryptionAlgorithm::Aes256Gcm);
assert_eq!(metadata.nonce.len(), 12); // 96-bit nonce
assert_eq!(metadata.salt.len(), 16); // 128-bit salt
assert!(metadata.tag.is_some());
assert_eq!(metadata.tag.as_ref().expect("INVARIANT: Option should be Some").len(), 16); // 128-bit tag
// Verify decryption works
let decrypted = manager
.decrypt_model_data(&encrypted, &metadata)
.await
.unwrap();
assert_eq!(decrypted, test_data);
}
#[tokio::test]
async fn test_chacha20_encryption_decryption() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "ChaCha20Poly1305".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
let test_data = b"Testing ChaCha20-Poly1305 encryption!";
let (encrypted, metadata) = manager.encrypt_model_data(test_data).await.unwrap();
// Verify encrypted data is different
assert_ne!(encrypted.as_slice(), test_data);
// Verify metadata
assert_eq!(metadata.algorithm, EncryptionAlgorithm::ChaCha20Poly1305);
assert!(metadata.tag.is_some());
// Verify decryption
let decrypted = manager
.decrypt_model_data(&encrypted, &metadata)
.await
.unwrap();
assert_eq!(decrypted, test_data);
}
#[tokio::test]
async fn test_encryption_authentication_tag_validation() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
let test_data = b"Testing authentication tag validation";
let (encrypted, mut metadata) = manager.encrypt_model_data(test_data).await.unwrap();
// Tamper with the tag
if let Some(ref mut tag) = metadata.tag {
if let Some(first_byte) = tag.first_mut() {
*first_byte ^= 0xFF; // Flip bits in first byte
}
}
// Decryption should fail with tampered tag
let result = manager.decrypt_model_data(&encrypted, &metadata).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("authentication error"));
}
#[tokio::test]
async fn test_nonce_uniqueness() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
let test_data = b"Testing nonce uniqueness";
// Encrypt same data multiple times
let (_, metadata1) = manager.encrypt_model_data(test_data).await.unwrap();
let (_, metadata2) = manager.encrypt_model_data(test_data).await.unwrap();
let (_, metadata3) = manager.encrypt_model_data(test_data).await.unwrap();
// Nonces should be different (cryptographically random)
assert_ne!(metadata1.nonce, metadata2.nonce);
assert_ne!(metadata1.nonce, metadata3.nonce);
assert_ne!(metadata2.nonce, metadata3.nonce);
}
#[tokio::test]
async fn test_large_data_encryption() {
let config = EncryptionConfig {
enable_encryption: true,
algorithm: "AES-256-GCM".to_string(),
key_rotation_days: 30,
encryption_keys_vault_path: None,
local_key_file: None,
};
let manager = EncryptionKeyManager::new(config, None);
// Test with 1MB of data
let large_data = vec![0xAB; 1024 * 1024];
let (encrypted, metadata) = manager.encrypt_model_data(&large_data).await.unwrap();
// Verify encryption
assert_ne!(encrypted.as_slice(), large_data.as_slice());
// Verify decryption
let decrypted = manager
.decrypt_model_data(&encrypted, &metadata)
.await
.unwrap();
assert_eq!(decrypted, large_data);
}
}