mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-28 09:08:58 +00:00
8e4a1ef917
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
484 lines
16 KiB
Rust
484 lines
16 KiB
Rust
// 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: <key_id>`
|
|
//! - `X-Object-Meta-Crypto-Iv: <base64_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<Self, ParseError>`
|
|
#[allow(clippy::should_implement_trait)]
|
|
pub fn from_str(s: &str) -> SwiftResult<Self> {
|
|
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<u8>,
|
|
}
|
|
|
|
impl EncryptionConfig {
|
|
/// Create new encryption configuration
|
|
pub fn new(enabled: bool, key_id: String, key: Vec<u8>) -> SwiftResult<Self> {
|
|
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<Self> {
|
|
let enabled = std::env::var("SWIFT_ENCRYPTION_ENABLED")
|
|
.unwrap_or_else(|_| "false".to_string())
|
|
.parse::<bool>()
|
|
.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<String>,
|
|
}
|
|
|
|
impl EncryptionMetadata {
|
|
/// Create new encryption metadata
|
|
pub fn new(algorithm: EncryptionAlgorithm, key_id: String, iv: Vec<u8>) -> 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<u8>) -> Self {
|
|
self.auth_tag = Some(BASE64.encode(&tag));
|
|
self
|
|
}
|
|
|
|
/// Convert to HTTP headers for object metadata
|
|
pub fn to_headers(&self) -> HashMap<String, String> {
|
|
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<String, String>) -> SwiftResult<Option<Self>> {
|
|
// 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<Vec<u8>> {
|
|
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<Option<Vec<u8>>> {
|
|
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<u8> {
|
|
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<u8>, 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::<Aes256Gcm>::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<Vec<u8>> {
|
|
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::<Aes256Gcm>::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);
|
|
}
|
|
}
|