// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Server-Side Encryption Support for Swift API //! //! This module implements automatic server-side encryption for Swift objects, //! providing encryption at rest with transparent encryption/decryption. //! //! # Encryption Algorithm //! //! Uses AES-256-GCM (Galois/Counter Mode) which provides: //! - Confidentiality (AES-256 encryption) //! - Authenticity (built-in authentication tag) //! - Performance (hardware acceleration on modern CPUs) //! //! # Key Management //! //! Supports multiple key sources: //! - Environment variable (SWIFT_ENCRYPTION_KEY) //! - Configuration file //! - External KMS (future: Barbican, AWS KMS, HashiCorp Vault) //! //! # Usage //! //! Encryption is transparent to clients: //! //! ```bash //! # Objects automatically encrypted on upload //! swift upload container file.txt //! //! # Automatically decrypted on download //! swift download container file.txt //! ``` //! //! # Metadata //! //! Encrypted objects include metadata: //! - `X-Object-Meta-Crypto-Enabled: true` //! - `X-Object-Meta-Crypto-Algorithm: AES-256-GCM` //! - `X-Object-Meta-Crypto-Key-Id: ` //! - `X-Object-Meta-Crypto-Iv: ` //! //! # Key Rotation //! //! Objects can be re-encrypted with new keys: //! - Upload with new key ID //! - Old encrypted objects remain readable with old keys //! - Gradual migration to new keys use super::{SwiftError, SwiftResult}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use std::collections::HashMap; use tracing::{debug, warn}; /// Encryption algorithm identifier #[derive(Debug, Clone, PartialEq)] pub enum EncryptionAlgorithm { /// AES-256-GCM (recommended) Aes256Gcm, /// AES-256-CBC (legacy, less secure) Aes256Cbc, } impl EncryptionAlgorithm { pub fn as_str(&self) -> &str { match self { EncryptionAlgorithm::Aes256Gcm => "AES-256-GCM", EncryptionAlgorithm::Aes256Cbc => "AES-256-CBC", } } /// Parse encryption algorithm from string /// /// Note: This could implement `FromStr` trait, but returns `SwiftResult` instead of `Result` #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> SwiftResult { match s { "AES-256-GCM" => Ok(EncryptionAlgorithm::Aes256Gcm), "AES-256-CBC" => Ok(EncryptionAlgorithm::Aes256Cbc), _ => Err(SwiftError::BadRequest(format!("Unsupported encryption algorithm: {s}"))), } } } /// Encryption configuration #[derive(Debug, Clone)] pub struct EncryptionConfig { /// Whether encryption is enabled globally pub enabled: bool, /// Default encryption algorithm pub algorithm: EncryptionAlgorithm, /// Master encryption key ID pub key_id: String, /// Master encryption key (32 bytes for AES-256) pub key: Vec, } impl EncryptionConfig { /// Create new encryption configuration pub fn new(enabled: bool, key_id: String, key: Vec) -> SwiftResult { if enabled && key.len() != 32 { return Err(SwiftError::BadRequest("Encryption key must be exactly 32 bytes for AES-256".to_string())); } Ok(EncryptionConfig { enabled, algorithm: EncryptionAlgorithm::Aes256Gcm, key_id, key, }) } /// Load encryption config from environment pub fn from_env() -> SwiftResult { let enabled = std::env::var("SWIFT_ENCRYPTION_ENABLED") .unwrap_or_else(|_| "false".to_string()) .parse::() .unwrap_or(false); if !enabled { // Return disabled config with dummy key return Ok(EncryptionConfig { enabled: false, algorithm: EncryptionAlgorithm::Aes256Gcm, key_id: "disabled".to_string(), key: vec![0u8; 32], }); } let key_id = std::env::var("SWIFT_ENCRYPTION_KEY_ID").unwrap_or_else(|_| "default".to_string()); let key_hex = std::env::var("SWIFT_ENCRYPTION_KEY") .map_err(|_| SwiftError::InternalServerError("SWIFT_ENCRYPTION_KEY not set but encryption is enabled".to_string()))?; let key = hex::decode(&key_hex).map_err(|_| SwiftError::BadRequest("Invalid encryption key hex format".to_string()))?; Self::new(enabled, key_id, key) } } /// Encryption metadata stored with encrypted objects #[derive(Debug, Clone)] pub struct EncryptionMetadata { /// Encryption algorithm used pub algorithm: EncryptionAlgorithm, /// Key ID used for encryption pub key_id: String, /// Initialization vector (base64 encoded) pub iv: String, /// Authentication tag for AES-GCM (base64 encoded, optional for CBC) pub auth_tag: Option, } impl EncryptionMetadata { /// Create new encryption metadata pub fn new(algorithm: EncryptionAlgorithm, key_id: String, iv: Vec) -> Self { EncryptionMetadata { algorithm, key_id, iv: BASE64.encode(&iv), auth_tag: None, } } /// Set authentication tag (for AES-GCM) pub fn with_auth_tag(mut self, tag: Vec) -> Self { self.auth_tag = Some(BASE64.encode(&tag)); self } /// Convert to HTTP headers for object metadata pub fn to_headers(&self) -> HashMap { let mut headers = HashMap::new(); headers.insert("x-object-meta-crypto-enabled".to_string(), "true".to_string()); headers.insert("x-object-meta-crypto-algorithm".to_string(), self.algorithm.as_str().to_string()); headers.insert("x-object-meta-crypto-key-id".to_string(), self.key_id.clone()); headers.insert("x-object-meta-crypto-iv".to_string(), self.iv.clone()); if let Some(tag) = &self.auth_tag { headers.insert("x-object-meta-crypto-auth-tag".to_string(), tag.clone()); } headers } /// Parse from object metadata pub fn from_metadata(metadata: &HashMap) -> SwiftResult> { // Check if encryption is enabled let enabled = metadata .get("x-object-meta-crypto-enabled") .map(|v| v == "true") .unwrap_or(false); if !enabled { return Ok(None); } let algorithm_str = metadata .get("x-object-meta-crypto-algorithm") .ok_or_else(|| SwiftError::InternalServerError("Missing crypto algorithm metadata".to_string()))?; let algorithm = EncryptionAlgorithm::from_str(algorithm_str)?; let key_id = metadata .get("x-object-meta-crypto-key-id") .ok_or_else(|| SwiftError::InternalServerError("Missing crypto key ID metadata".to_string()))? .clone(); let iv = metadata .get("x-object-meta-crypto-iv") .ok_or_else(|| SwiftError::InternalServerError("Missing crypto IV metadata".to_string()))? .clone(); let auth_tag = metadata.get("x-object-meta-crypto-auth-tag").cloned(); Ok(Some(EncryptionMetadata { algorithm, key_id, iv, auth_tag, })) } /// Decode IV from base64 pub fn decode_iv(&self) -> SwiftResult> { BASE64 .decode(&self.iv) .map_err(|_| SwiftError::InternalServerError("Invalid IV base64 encoding".to_string())) } /// Decode auth tag from base64 pub fn decode_auth_tag(&self) -> SwiftResult>> { match &self.auth_tag { Some(tag) => { let decoded = BASE64 .decode(tag) .map_err(|_| SwiftError::InternalServerError("Invalid auth tag base64 encoding".to_string()))?; Ok(Some(decoded)) } None => Ok(None), } } } /// Check if object should be encrypted based on configuration and headers pub fn should_encrypt(config: &EncryptionConfig, headers: &http::HeaderMap) -> bool { // Check if encryption is globally enabled if !config.enabled { return false; } // Check if client explicitly disabled encryption if let Some(disable) = headers.get("x-object-meta-crypto-disable") && disable.to_str().unwrap_or("") == "true" { debug!("Client explicitly disabled encryption"); return false; } // Encrypt by default if enabled true } /// Generate random initialization vector for encryption /// /// TODO: Integrate with proper random number generator /// For now, uses a simple timestamp-based approach (NOT cryptographically secure!) pub fn generate_iv(size: usize) -> Vec { use std::time::{SystemTime, UNIX_EPOCH}; // WARNING: This is a placeholder! In production, use a proper CSPRNG // like rand::thread_rng() or getrandom crate let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(); let mut iv = Vec::with_capacity(size); let bytes = timestamp.to_le_bytes(); // Fill IV with timestamp bytes (repeated if necessary) for i in 0..size { iv.push(bytes[i % bytes.len()]); } iv } /// Placeholder for actual encryption (requires crypto crate integration) /// /// In production, this would use a proper crypto library like `aes-gcm` or `ring`. /// This is a stub that demonstrates the API structure. pub fn encrypt_data(data: &[u8], config: &EncryptionConfig) -> SwiftResult<(Vec, EncryptionMetadata)> { debug!("Encrypting {} bytes with {}", data.len(), config.algorithm.as_str()); // Generate IV (12 bytes for GCM, 16 bytes for CBC) let iv_size = match config.algorithm { EncryptionAlgorithm::Aes256Gcm => 12, EncryptionAlgorithm::Aes256Cbc => 16, }; let iv = generate_iv(iv_size); // TODO: Implement actual encryption // For now, return unencrypted data with metadata // In production, integrate with aes-gcm crate: // // use aes_gcm::{Aes256Gcm, Key, Nonce}; // use aes_gcm::aead::{Aead, KeyInit}; // // let key = Key::::from_slice(&config.key); // let cipher = Aes256Gcm::new(key); // let nonce = Nonce::from_slice(&iv); // let ciphertext = cipher.encrypt(nonce, data) // .map_err(|e| SwiftError::InternalServerError(format!("Encryption failed: {}", e)))?; warn!("Encryption not yet implemented - returning plaintext with metadata"); let metadata = EncryptionMetadata::new(config.algorithm.clone(), config.key_id.clone(), iv); // In production, return ciphertext Ok((data.to_vec(), metadata)) } /// Placeholder for actual decryption (requires crypto crate integration) /// /// In production, this would use a proper crypto library like `aes-gcm` or `ring`. /// This is a stub that demonstrates the API structure. pub fn decrypt_data(encrypted_data: &[u8], metadata: &EncryptionMetadata, config: &EncryptionConfig) -> SwiftResult> { debug!("Decrypting {} bytes with {}", encrypted_data.len(), metadata.algorithm.as_str()); // Verify key ID matches if metadata.key_id != config.key_id { return Err(SwiftError::InternalServerError(format!( "Key ID mismatch: object encrypted with '{}', but current key is '{}'", metadata.key_id, config.key_id ))); } // In production, integrate with aes-gcm crate: // // use aes_gcm::{Aes256Gcm, Key, Nonce}; // use aes_gcm::aead::{Aead, KeyInit}; // // let key = Key::::from_slice(&config.key); // let cipher = Aes256Gcm::new(key); // let nonce = Nonce::from_slice(&iv); // let plaintext = cipher.decrypt(nonce, encrypted_data) // .map_err(|e| SwiftError::InternalServerError(format!("Decryption failed: {}", e)))?; warn!("Decryption not yet implemented - returning data as-is"); // In production, return plaintext Ok(encrypted_data.to_vec()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_encryption_config_creation() { let key = vec![0u8; 32]; // 32 bytes for AES-256 let config = EncryptionConfig::new(true, "test-key".to_string(), key).unwrap(); assert!(config.enabled); assert_eq!(config.key_id, "test-key"); assert_eq!(config.key.len(), 32); } #[test] fn test_encryption_config_invalid_key_size() { let key = vec![0u8; 16]; // Too short let result = EncryptionConfig::new(true, "test-key".to_string(), key); assert!(result.is_err()); } #[test] fn test_encryption_algorithm_conversion() { assert_eq!(EncryptionAlgorithm::Aes256Gcm.as_str(), "AES-256-GCM"); assert_eq!(EncryptionAlgorithm::Aes256Cbc.as_str(), "AES-256-CBC"); assert!(EncryptionAlgorithm::from_str("AES-256-GCM").is_ok()); assert!(EncryptionAlgorithm::from_str("AES-256-CBC").is_ok()); assert!(EncryptionAlgorithm::from_str("INVALID").is_err()); } #[test] fn test_encryption_metadata_to_headers() { let metadata = EncryptionMetadata::new( EncryptionAlgorithm::Aes256Gcm, "test-key".to_string(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], ); let headers = metadata.to_headers(); assert_eq!(headers.get("x-object-meta-crypto-enabled"), Some(&"true".to_string())); assert_eq!(headers.get("x-object-meta-crypto-algorithm"), Some(&"AES-256-GCM".to_string())); assert_eq!(headers.get("x-object-meta-crypto-key-id"), Some(&"test-key".to_string())); assert!(headers.contains_key("x-object-meta-crypto-iv")); } #[test] fn test_encryption_metadata_from_metadata() { let mut metadata_map = HashMap::new(); metadata_map.insert("x-object-meta-crypto-enabled".to_string(), "true".to_string()); metadata_map.insert("x-object-meta-crypto-algorithm".to_string(), "AES-256-GCM".to_string()); metadata_map.insert("x-object-meta-crypto-key-id".to_string(), "test-key".to_string()); metadata_map.insert( "x-object-meta-crypto-iv".to_string(), BASE64.encode([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), ); let metadata = EncryptionMetadata::from_metadata(&metadata_map).unwrap(); assert!(metadata.is_some()); let metadata = metadata.unwrap(); assert_eq!(metadata.algorithm, EncryptionAlgorithm::Aes256Gcm); assert_eq!(metadata.key_id, "test-key"); } #[test] fn test_encryption_metadata_from_metadata_not_encrypted() { let metadata_map = HashMap::new(); let result = EncryptionMetadata::from_metadata(&metadata_map).unwrap(); assert!(result.is_none()); } #[test] fn test_should_encrypt() { let key = vec![0u8; 32]; let config = EncryptionConfig::new(true, "test".to_string(), key).unwrap(); let headers = http::HeaderMap::new(); assert!(should_encrypt(&config, &headers)); // Test with disabled config let disabled_config = EncryptionConfig::new(false, "test".to_string(), vec![0u8; 32]).unwrap(); assert!(!should_encrypt(&disabled_config, &headers)); } #[test] fn test_generate_iv() { let iv1 = generate_iv(12); std::thread::sleep(std::time::Duration::from_nanos(1)); // Ensure timestamp changes let iv2 = generate_iv(12); assert_eq!(iv1.len(), 12); assert_eq!(iv2.len(), 12); // IVs should be different (random) // Note: This uses a placeholder implementation. In production, use proper CSPRNG. assert_ne!(iv1, iv2); } #[test] fn test_encrypt_decrypt_roundtrip() { let key = vec![0u8; 32]; let config = EncryptionConfig::new(true, "test-key".to_string(), key).unwrap(); let plaintext = b"Hello, World!"; let (ciphertext, metadata) = encrypt_data(plaintext, &config).unwrap(); let decrypted = decrypt_data(&ciphertext, &metadata, &config).unwrap(); assert_eq!(decrypted, plaintext); } }