Refactor: refactor SSE layer and KMS subsystem (#1703)

Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
唐小鸭
2026-02-04 16:10:33 +08:00
committed by GitHub
parent 4d19b069c3
commit 7a42af922d
25 changed files with 3919 additions and 975 deletions
+314
View File
@@ -0,0 +1,314 @@
// 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.
//! Data Encryption Key (DEK) encryption interface and implementations
//!
//! This module provides a unified interface for encrypting and decrypting
//! data encryption keys using master keys. It abstracts the encryption
//! operations so that different backends can share the same encryption logic.
#![allow(dead_code)] // Trait methods may be used by implementations
use crate::error::{KmsError, Result};
use async_trait::async_trait;
use jiff::Zoned;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Data key envelope for encrypting/decrypting data keys
///
/// This structure stores the encrypted DEK along with metadata needed for decryption.
/// The `master_key_version` field records which version of the KEK (Key Encryption Key)
/// was used to encrypt this DEK, enabling proper key rotation support.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataKeyEnvelope {
pub key_id: String,
pub master_key_id: String,
pub key_spec: String,
pub encrypted_key: Vec<u8>,
pub nonce: Vec<u8>,
pub encryption_context: HashMap<String, String>,
pub created_at: Zoned,
}
/// Trait for encrypting and decrypting data encryption keys (DEK)
///
/// This trait abstracts the encryption operations used to protect
/// data encryption keys with master keys. Different implementations
/// can use different encryption algorithms (e.g., AES-256-GCM).
#[async_trait]
pub trait DekCrypto: Send + Sync {
/// Encrypt plaintext data using a master key material
///
/// # Arguments
/// * `key_material` - The master key material (raw bytes)
/// * `plaintext` - The data to encrypt
///
/// # Returns
/// A tuple of (ciphertext, nonce) where:
/// - `ciphertext` - The encrypted data
/// - `nonce` - The nonce used for encryption (should be stored with ciphertext)
async fn encrypt(&self, key_material: &[u8], plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>)>;
/// Decrypt ciphertext data using a master key material
///
/// # Arguments
/// * `key_material` - The master key material (raw bytes)
/// * `ciphertext` - The encrypted data
/// * `nonce` - The nonce used for encryption
///
/// # Returns
/// The decrypted plaintext data
async fn decrypt(&self, key_material: &[u8], ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>>;
/// Get the algorithm name used by this implementation
#[allow(dead_code)] // May be used by implementations or for debugging
fn algorithm(&self) -> &'static str;
/// Get the required key material size in bytes
#[allow(dead_code)] // May be used by implementations or for debugging
fn key_size(&self) -> usize;
}
/// AES-256-GCM implementation of DEK encryption
pub struct AesDekCrypto;
impl AesDekCrypto {
/// Create a new AES-256-GCM DEK crypto instance
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl DekCrypto for AesDekCrypto {
async fn encrypt(&self, key_material: &[u8], plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit},
};
// Validate key material length
if key_material.len() != 32 {
return Err(KmsError::cryptographic_error(
"key",
format!("Invalid key length: expected 32 bytes, got {}", key_material.len()),
));
}
// Create cipher from key material
let key =
Key::<Aes256Gcm>::try_from(key_material).map_err(|_| KmsError::cryptographic_error("key", "Invalid key length"))?;
let cipher = Aes256Gcm::new(&key);
// Generate random nonce (12 bytes for GCM)
let mut nonce_bytes = [0u8; 12];
rand::rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from(nonce_bytes);
// Encrypt plaintext
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| KmsError::cryptographic_error("encrypt", e.to_string()))?;
Ok((ciphertext, nonce_bytes.to_vec()))
}
async fn decrypt(&self, key_material: &[u8], ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit},
};
// Validate nonce length
if nonce.len() != 12 {
return Err(KmsError::cryptographic_error("nonce", "Invalid nonce length: expected 12 bytes"));
}
// Validate key material length
if key_material.len() != 32 {
return Err(KmsError::cryptographic_error(
"key",
format!("Invalid key length: expected 32 bytes, got {}", key_material.len()),
));
}
// Create cipher from key material
let key =
Key::<Aes256Gcm>::try_from(key_material).map_err(|_| KmsError::cryptographic_error("key", "Invalid key length"))?;
let cipher = Aes256Gcm::new(&key);
// Convert nonce
let mut nonce_array = [0u8; 12];
nonce_array.copy_from_slice(nonce);
let nonce_ref = Nonce::from(nonce_array);
// Decrypt ciphertext
let plaintext = cipher
.decrypt(&nonce_ref, ciphertext)
.map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))?;
Ok(plaintext)
}
#[allow(dead_code)] // Trait method, may be used by implementations
fn algorithm(&self) -> &'static str {
"AES-256-GCM"
}
#[allow(dead_code)] // Trait method, may be used by implementations
fn key_size(&self) -> usize {
32 // 256 bits
}
}
impl Default for AesDekCrypto {
fn default() -> Self {
Self::new()
}
}
/// Generate random key material for the given algorithm
///
/// # Arguments
/// * `algorithm` - The key algorithm (e.g., "AES_256", "AES_128")
///
/// # Returns
/// A vector containing the generated key material
pub fn generate_key_material(algorithm: &str) -> Result<Vec<u8>> {
let key_size = match algorithm {
"AES_256" => 32,
"AES_128" => 16,
_ => return Err(KmsError::unsupported_algorithm(algorithm)),
};
let mut key_material = vec![0u8; key_size];
rand::rng().fill_bytes(&mut key_material);
Ok(key_material)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_aes_dek_crypto_encrypt_decrypt() {
let crypto = AesDekCrypto::new();
// Generate test key material
let key_material = generate_key_material("AES_256").expect("Failed to generate key material");
let plaintext = b"Hello, World! This is a test message.";
// Test encryption
let (ciphertext, nonce) = crypto
.encrypt(&key_material, plaintext)
.await
.expect("Encryption should succeed");
assert!(!ciphertext.is_empty());
assert_eq!(nonce.len(), 12);
assert_ne!(ciphertext, plaintext);
// Test decryption
let decrypted = crypto
.decrypt(&key_material, &ciphertext, &nonce)
.await
.expect("Decryption should succeed");
assert_eq!(decrypted, plaintext);
}
#[tokio::test]
async fn test_aes_dek_crypto_invalid_key_size() {
let crypto = AesDekCrypto::new();
let invalid_key = vec![0u8; 16]; // Too short
let plaintext = b"test";
let result = crypto.encrypt(&invalid_key, plaintext).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_aes_dek_crypto_invalid_nonce() {
let crypto = AesDekCrypto::new();
let key_material = generate_key_material("AES_256").expect("Failed to generate key material");
let ciphertext = vec![0u8; 16];
let invalid_nonce = vec![0u8; 8]; // Too short
let result = crypto.decrypt(&key_material, &ciphertext, &invalid_nonce).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_generate_key_material() {
let key_256 = generate_key_material("AES_256").expect("Should generate AES_256 key");
assert_eq!(key_256.len(), 32);
let key_128 = generate_key_material("AES_128").expect("Should generate AES_128 key");
assert_eq!(key_128.len(), 16);
// Keys should be different
let key_256_2 = generate_key_material("AES_256").expect("Should generate AES_256 key");
assert_ne!(key_256, key_256_2);
// Invalid algorithm
assert!(generate_key_material("INVALID").is_err());
}
#[tokio::test]
async fn test_data_key_envelope_serialization() {
let envelope = DataKeyEnvelope {
key_id: "test-key-id".to_string(),
master_key_id: "master-key-id".to_string(),
key_spec: "AES_256".to_string(),
encrypted_key: vec![1, 2, 3, 4],
nonce: vec![5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
encryption_context: {
let mut map = HashMap::new();
map.insert("bucket".to_string(), "test-bucket".to_string());
map
},
created_at: Zoned::now(),
};
// Test serialization
let serialized = serde_json::to_vec(&envelope).expect("Serialization should succeed");
assert!(!serialized.is_empty());
// Test deserialization
let deserialized: DataKeyEnvelope = serde_json::from_slice(&serialized).expect("Deserialization should succeed");
assert_eq!(deserialized.key_id, envelope.key_id);
assert_eq!(deserialized.master_key_id, envelope.master_key_id);
assert_eq!(deserialized.encrypted_key, envelope.encrypted_key);
}
#[tokio::test]
async fn test_data_key_envelope_backward_compatibility() {
// Test deserialization with current Zoned format (with timezone annotation)
let envelope_json = r#"{
"key_id": "test-key-id",
"master_key_id": "master-key-id",
"key_spec": "AES_256",
"encrypted_key": [1, 2, 3, 4],
"nonce": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
"encryption_context": {"bucket": "test-bucket"},
"created_at": "2024-01-01T00:00:00+00:00[UTC]"
}"#;
let deserialized: DataKeyEnvelope = serde_json::from_str(envelope_json).expect("Should deserialize current format");
assert_eq!(deserialized.key_id, "test-key-id");
assert_eq!(deserialized.master_key_id, "master-key-id");
}
}
+3 -3
View File
@@ -14,7 +14,7 @@
//! Object encryption service implementation
mod ciphers;
pub mod service;
pub mod ciphers;
pub mod dek;
pub use service::ObjectEncryptionService;
pub use dek::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
-854
View File
@@ -1,854 +0,0 @@
// 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.
//! Object encryption service for S3-compatible encryption
use crate::encryption::ciphers::{create_cipher, generate_iv};
use crate::error::{KmsError, Result};
use crate::manager::KmsManager;
use crate::types::*;
use base64::Engine;
use jiff::Zoned;
use rand::random;
use std::collections::HashMap;
use std::io::Cursor;
use tokio::io::{AsyncRead, AsyncReadExt};
use tracing::{debug, info};
use zeroize::Zeroize;
/// Data key for object encryption
/// SECURITY: This struct automatically zeros sensitive key material when dropped
#[derive(Debug, Clone)]
pub struct DataKey {
/// 256-bit encryption key - automatically zeroed on drop
pub plaintext_key: [u8; 32],
/// 96-bit nonce for GCM mode - not secret so no need to zero
pub nonce: [u8; 12],
}
// SECURITY: Implement Drop to automatically zero sensitive key material
impl Drop for DataKey {
fn drop(&mut self) {
self.plaintext_key.zeroize();
}
}
/// Service for encrypting and decrypting S3 objects with KMS integration
pub struct ObjectEncryptionService {
kms_manager: KmsManager,
}
/// Result of object encryption
#[derive(Debug, Clone)]
pub struct EncryptionResult {
/// Encrypted data
pub ciphertext: Vec<u8>,
/// Encryption metadata to be stored with the object
pub metadata: EncryptionMetadata,
}
impl ObjectEncryptionService {
/// Create a new object encryption service
///
/// # Arguments
/// * `kms_manager` - KMS manager to use for key operations
///
/// # Returns
/// New ObjectEncryptionService instance
///
pub fn new(kms_manager: KmsManager) -> Self {
Self { kms_manager }
}
/// Create a new master key (delegates to KMS manager)
///
/// # Arguments
/// * `request` - CreateKeyRequest with key parameters
///
/// # Returns
/// CreateKeyResponse with created key details
///
pub async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
self.kms_manager.create_key(request).await
}
/// Describe a master key (delegates to KMS manager)
///
/// # Arguments
/// * `request` - DescribeKeyRequest with key ID
///
/// # Returns
/// DescribeKeyResponse with key metadata
///
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
self.kms_manager.describe_key(request).await
}
/// List master keys (delegates to KMS manager)
///
/// # Arguments
/// * `request` - ListKeysRequest with listing parameters
///
/// # Returns
/// ListKeysResponse with list of keys
///
pub async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
self.kms_manager.list_keys(request).await
}
/// Generate a data encryption key (delegates to KMS manager)
///
/// # Arguments
/// * `request` - GenerateDataKeyRequest with key parameters
///
/// # Returns
/// GenerateDataKeyResponse with generated key details
///
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
self.kms_manager.generate_data_key(request).await
}
/// Get the default key ID
///
/// # Returns
/// Option with default key ID if configured
///
pub fn get_default_key_id(&self) -> Option<&String> {
self.kms_manager.get_default_key_id()
}
/// Get cache statistics
///
/// # Returns
/// Option with (hits, misses) if caching is enabled
///
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
self.kms_manager.cache_stats().await
}
/// Clear the cache
///
/// # Returns
/// Result indicating success or failure
///
pub async fn clear_cache(&self) -> Result<()> {
self.kms_manager.clear_cache().await
}
/// Get backend health status
///
/// # Returns
/// Result indicating if backend is healthy
///
pub async fn health_check(&self) -> Result<bool> {
self.kms_manager.health_check().await
}
/// Create a data encryption key for object encryption
///
/// # Arguments
/// * `kms_key_id` - Optional KMS key ID to use (uses default if None)
/// * `context` - ObjectEncryptionContext with bucket and object key
///
/// # Returns
/// Tuple with DataKey and encrypted key blob
///
pub async fn create_data_key(
&self,
kms_key_id: &Option<String>,
context: &ObjectEncryptionContext,
) -> Result<(DataKey, Vec<u8>)> {
// Determine the KMS key ID to use
let actual_key_id = kms_key_id
.as_ref()
.map(|s| s.as_str())
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
// Build encryption context
let mut enc_context = context.encryption_context.clone();
enc_context.insert("bucket".to_string(), context.bucket.clone());
enc_context.insert("object_key".to_string(), context.object_key.clone());
let request = GenerateDataKeyRequest {
key_id: actual_key_id.to_string(),
key_spec: KeySpec::Aes256,
encryption_context: enc_context,
};
let data_key_response = self.kms_manager.generate_data_key(request).await?;
// Generate a unique random nonce for this data key
// This ensures each object/part gets a unique base nonce for streaming encryption
let nonce: [u8; 12] = random();
tracing::info!("Generated random nonce for data key: {:02x?}", nonce);
let data_key = DataKey {
plaintext_key: data_key_response
.plaintext_key
.try_into()
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
nonce,
};
Ok((data_key, data_key_response.ciphertext_blob))
}
/// Decrypt a data encryption key
///
/// # Arguments
/// * `encrypted_key` - Encrypted data key blob
/// * `context` - ObjectEncryptionContext with bucket and object key
///
/// # Returns
/// DataKey with decrypted key
///
pub async fn decrypt_data_key(&self, encrypted_key: &[u8], _context: &ObjectEncryptionContext) -> Result<DataKey> {
let decrypt_request = DecryptRequest {
ciphertext: encrypted_key.to_vec(),
encryption_context: HashMap::new(),
grant_tokens: Vec::new(),
};
let decrypt_response = self.kms_manager.decrypt(decrypt_request).await?;
let data_key = DataKey {
plaintext_key: decrypt_response
.plaintext
.try_into()
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
nonce: [0u8; 12], // This will be replaced by stored nonce during GET
};
Ok(data_key)
}
/// Encrypt object data using server-side encryption
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `reader` - Data reader
/// * `algorithm` - Encryption algorithm to use
/// * `kms_key_id` - Optional KMS key ID (uses default if None)
/// * `encryption_context` - Additional encryption context
///
/// # Returns
/// EncryptionResult containing encrypted data and metadata
pub async fn encrypt_object<R>(
&self,
bucket: &str,
object_key: &str,
mut reader: R,
algorithm: &EncryptionAlgorithm,
kms_key_id: Option<&str>,
encryption_context: Option<&HashMap<String, String>>,
) -> Result<EncryptionResult>
where
R: AsyncRead + Unpin,
{
debug!("Encrypting object {}/{} with algorithm {:?}", bucket, object_key, algorithm);
// Read all data (for simplicity - in production, use streaming)
let mut data = Vec::new();
reader.read_to_end(&mut data).await?;
let original_size = data.len() as u64;
// Determine the KMS key ID to use
let actual_key_id = kms_key_id
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
// Build encryption context
let mut context = encryption_context.cloned().unwrap_or_default();
context.insert("bucket".to_string(), bucket.to_string());
context.insert("object".to_string(), object_key.to_string());
context.insert("algorithm".to_string(), algorithm.as_str().to_string());
// Auto-create key for SSE-S3 if it doesn't exist
if algorithm == &EncryptionAlgorithm::Aes256 {
let describe_req = DescribeKeyRequest {
key_id: actual_key_id.to_string(),
};
if let Err(KmsError::KeyNotFound { .. }) = self.kms_manager.describe_key(describe_req).await {
info!("Auto-creating SSE-S3 key: {}", actual_key_id);
let create_req = CreateKeyRequest {
key_name: Some(actual_key_id.to_string()),
key_usage: KeyUsage::EncryptDecrypt,
description: Some("Auto-created SSE-S3 key".to_string()),
policy: None,
tags: HashMap::new(),
origin: None,
};
self.kms_manager
.create_key(create_req)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to auto-create SSE-S3 key {actual_key_id}: {e}")))?;
}
} else {
// For SSE-KMS, key must exist
let describe_req = DescribeKeyRequest {
key_id: actual_key_id.to_string(),
};
self.kms_manager.describe_key(describe_req).await.map_err(|_| {
KmsError::invalid_operation(format!("SSE-KMS key '{actual_key_id}' not found. Please create it first."))
})?;
}
// Generate data encryption key
let request = GenerateDataKeyRequest {
key_id: actual_key_id.to_string(),
key_spec: KeySpec::Aes256,
encryption_context: context.clone(),
};
let data_key = self
.kms_manager
.generate_data_key(request)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to generate data key: {e}")))?;
let plaintext_key = data_key.plaintext_key;
// Create cipher and generate IV
let cipher = create_cipher(algorithm, &plaintext_key)?;
let iv = generate_iv(algorithm);
// Build AAD from encryption context
let aad = serde_json::to_vec(&context)?;
// Encrypt the data
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
// Create encryption metadata
let metadata = EncryptionMetadata {
algorithm: algorithm.as_str().to_string(),
key_id: actual_key_id.to_string(),
key_version: 1, // Default to version 1 for now
iv,
tag: Some(tag),
encryption_context: context,
encrypted_at: Zoned::now(),
original_size,
encrypted_data_key: data_key.ciphertext_blob,
};
info!("Successfully encrypted object {}/{} ({} bytes)", bucket, object_key, original_size);
Ok(EncryptionResult { ciphertext, metadata })
}
/// Decrypt object data
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `ciphertext` - Encrypted data
/// * `metadata` - Encryption metadata
/// * `expected_context` - Expected encryption context for validation
///
/// # Returns
/// Decrypted data as a reader
pub async fn decrypt_object(
&self,
bucket: &str,
object_key: &str,
ciphertext: Vec<u8>,
metadata: &EncryptionMetadata,
expected_context: Option<&HashMap<String, String>>,
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
debug!("Decrypting object {}/{} with algorithm {}", bucket, object_key, metadata.algorithm);
// Validate encryption context if provided
if let Some(expected) = expected_context {
self.validate_encryption_context(&metadata.encryption_context, expected)?;
}
// Parse algorithm
let algorithm = metadata
.algorithm
.parse::<EncryptionAlgorithm>()
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
// Decrypt the data key
let decrypt_request = DecryptRequest {
ciphertext: metadata.encrypted_data_key.clone(),
encryption_context: metadata.encryption_context.clone(),
grant_tokens: Vec::new(),
};
let decrypt_response = self
.kms_manager
.decrypt(decrypt_request)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to decrypt data key: {e}")))?;
// Create cipher
let cipher = create_cipher(&algorithm, &decrypt_response.plaintext)?;
// Build AAD from encryption context
let aad = serde_json::to_vec(&metadata.encryption_context)?;
// Get tag from metadata
let tag = metadata
.tag
.as_ref()
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
// Decrypt the data
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
info!("Successfully decrypted object {}/{} ({} bytes)", bucket, object_key, plaintext.len());
Ok(Box::new(Cursor::new(plaintext)))
}
/// Encrypt object with customer-provided key (SSE-C)
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `reader` - Data reader
/// * `customer_key` - Customer-provided 256-bit key
/// * `customer_key_md5` - Optional MD5 hash of the customer key for validation
///
/// # Returns
/// EncryptionResult with SSE-C metadata
pub async fn encrypt_object_with_customer_key<R>(
&self,
bucket: &str,
object_key: &str,
mut reader: R,
customer_key: &[u8],
customer_key_md5: Option<&str>,
) -> Result<EncryptionResult>
where
R: AsyncRead + Unpin,
{
debug!("Encrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
// Validate key size
if customer_key.len() != 32 {
return Err(KmsError::invalid_key_size(32, customer_key.len()));
}
// Validate key MD5 if provided
if let Some(expected_md5) = customer_key_md5 {
let actual_md5 = md5::compute(customer_key);
let actual_md5_hex = format!("{actual_md5:x}");
if actual_md5_hex != expected_md5.to_lowercase() {
return Err(KmsError::validation_error("Customer key MD5 mismatch"));
}
}
// Read all data
let mut data = Vec::new();
reader.read_to_end(&mut data).await?;
let original_size = data.len() as u64;
// Create cipher and generate IV
let algorithm = EncryptionAlgorithm::Aes256;
let cipher = create_cipher(&algorithm, customer_key)?;
let iv = generate_iv(&algorithm);
// Build minimal encryption context for SSE-C
let context = HashMap::from([
("bucket".to_string(), bucket.to_string()),
("object".to_string(), object_key.to_string()),
("sse_type".to_string(), "customer".to_string()),
]);
let aad = serde_json::to_vec(&context)?;
// Encrypt the data
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
// Create metadata (no encrypted data key for SSE-C)
let metadata = EncryptionMetadata {
algorithm: algorithm.as_str().to_string(),
key_id: "sse-c".to_string(), // Special marker for SSE-C
key_version: 1,
iv,
tag: Some(tag),
encryption_context: context,
encrypted_at: Zoned::now(),
original_size,
encrypted_data_key: Vec::new(), // Empty for SSE-C
};
info!(
"Successfully encrypted object {}/{} with SSE-C ({} bytes)",
bucket, object_key, original_size
);
Ok(EncryptionResult { ciphertext, metadata })
}
/// Decrypt object with customer-provided key (SSE-C)
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `ciphertext` - Encrypted data
/// * `metadata` - Encryption metadata
/// * `customer_key` - Customer-provided 256-bit key
///
/// # Returns
/// Decrypted data as a reader
///
pub async fn decrypt_object_with_customer_key(
&self,
bucket: &str,
object_key: &str,
ciphertext: Vec<u8>,
metadata: &EncryptionMetadata,
customer_key: &[u8],
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
debug!("Decrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
// Validate key size
if customer_key.len() != 32 {
return Err(KmsError::invalid_key_size(32, customer_key.len()));
}
// Validate that this is SSE-C
if metadata.key_id != "sse-c" {
return Err(KmsError::invalid_operation("This object was not encrypted with SSE-C"));
}
// Parse algorithm
let algorithm = metadata
.algorithm
.parse::<EncryptionAlgorithm>()
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
// Create cipher
let cipher = create_cipher(&algorithm, customer_key)?;
// Build AAD from encryption context
let aad = serde_json::to_vec(&metadata.encryption_context)?;
// Get tag from metadata
let tag = metadata
.tag
.as_ref()
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
// Decrypt the data
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
info!(
"Successfully decrypted SSE-C object {}/{} ({} bytes)",
bucket,
object_key,
plaintext.len()
);
Ok(Box::new(Cursor::new(plaintext)))
}
/// Validate encryption context
///
/// # Arguments
/// * `actual` - Actual encryption context from metadata
/// * `expected` - Expected encryption context to validate against
///
/// # Returns
/// Result indicating success or context mismatch
///
fn validate_encryption_context(&self, actual: &HashMap<String, String>, expected: &HashMap<String, String>) -> Result<()> {
for (key, expected_value) in expected {
match actual.get(key) {
Some(actual_value) if actual_value == expected_value => continue,
Some(actual_value) => {
return Err(KmsError::context_mismatch(format!(
"Context mismatch for '{key}': expected '{expected_value}', got '{actual_value}'"
)));
}
None => {
return Err(KmsError::context_mismatch(format!("Missing context key '{key}'")));
}
}
}
Ok(())
}
/// Convert encryption metadata to HTTP headers for S3 compatibility
///
/// # Arguments
/// * `metadata` - EncryptionMetadata to convert
///
/// # Returns
/// HashMap of HTTP headers
///
pub fn metadata_to_headers(&self, metadata: &EncryptionMetadata) -> HashMap<String, String> {
let mut headers = HashMap::new();
// Standard S3 encryption headers
if metadata.key_id == "sse-c" {
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
headers.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string());
} else if metadata.algorithm == "AES256" {
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
// For SSE-S3, we still need to store the key ID for internal use
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
} else {
headers.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string());
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
}
// Internal headers for decryption
headers.insert(
"x-rustfs-encryption-iv".to_string(),
base64::engine::general_purpose::STANDARD.encode(&metadata.iv),
);
if let Some(ref tag) = metadata.tag {
headers.insert(
"x-rustfs-encryption-tag".to_string(),
base64::engine::general_purpose::STANDARD.encode(tag),
);
}
headers.insert(
"x-rustfs-encryption-key".to_string(),
base64::engine::general_purpose::STANDARD.encode(&metadata.encrypted_data_key),
);
headers.insert(
"x-rustfs-encryption-context".to_string(),
serde_json::to_string(&metadata.encryption_context).unwrap_or_default(),
);
headers
}
/// Parse encryption metadata from HTTP headers
///
/// # Arguments
/// * `headers` - HashMap of HTTP headers
///
/// # Returns
/// EncryptionMetadata parsed from headers
///
pub fn headers_to_metadata(&self, headers: &HashMap<String, String>) -> Result<EncryptionMetadata> {
let algorithm = headers
.get("x-amz-server-side-encryption")
.ok_or_else(|| KmsError::validation_error("Missing encryption algorithm header"))?
.clone();
let key_id = if algorithm == "AES256" && headers.contains_key("x-amz-server-side-encryption-customer-algorithm") {
"sse-c".to_string()
} else if let Some(kms_key_id) = headers.get("x-amz-server-side-encryption-aws-kms-key-id") {
kms_key_id.clone()
} else {
return Err(KmsError::validation_error("Missing key ID"));
};
let iv = headers
.get("x-rustfs-encryption-iv")
.ok_or_else(|| KmsError::validation_error("Missing IV header"))?;
let iv = base64::engine::general_purpose::STANDARD
.decode(iv)
.map_err(|e| KmsError::validation_error(format!("Invalid IV: {e}")))?;
let tag = if let Some(tag_str) = headers.get("x-rustfs-encryption-tag") {
Some(
base64::engine::general_purpose::STANDARD
.decode(tag_str)
.map_err(|e| KmsError::validation_error(format!("Invalid tag: {e}")))?,
)
} else {
None
};
let encrypted_data_key = if let Some(key_str) = headers.get("x-rustfs-encryption-key") {
base64::engine::general_purpose::STANDARD
.decode(key_str)
.map_err(|e| KmsError::validation_error(format!("Invalid encrypted key: {e}")))?
} else {
Vec::new() // Empty for SSE-C
};
let encryption_context = if let Some(context_str) = headers.get("x-rustfs-encryption-context") {
serde_json::from_str(context_str)
.map_err(|e| KmsError::validation_error(format!("Invalid encryption context: {e}")))?
} else {
HashMap::new()
};
Ok(EncryptionMetadata {
algorithm,
key_id,
key_version: 1, // Default for parsing
iv,
tag,
encryption_context,
encrypted_at: Zoned::now(),
original_size: 0, // Not available from headers
encrypted_data_key,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KmsConfig;
use std::sync::Arc;
use tempfile::TempDir;
async fn create_test_service() -> (ObjectEncryptionService, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_default_key("test-key".to_string());
let backend = Arc::new(
crate::backends::local::LocalKmsBackend::new(config.clone())
.await
.expect("local backend should initialize"),
);
let kms_manager = KmsManager::new(backend, config);
let service = ObjectEncryptionService::new(kms_manager);
(service, temp_dir)
}
#[tokio::test]
async fn test_sse_s3_encryption() {
let (service, _temp_dir) = create_test_service().await;
let bucket = "test-bucket";
let object_key = "test-object";
let data = b"Hello, SSE-S3!";
let reader = Cursor::new(data.to_vec());
// Encrypt with SSE-S3 (auto-create key)
let result = service
.encrypt_object(
bucket,
object_key,
reader,
&EncryptionAlgorithm::Aes256,
None, // Use default key
None,
)
.await
.expect("Encryption failed");
assert!(!result.ciphertext.is_empty());
assert_eq!(result.metadata.algorithm, "AES256");
assert_eq!(result.metadata.original_size, data.len() as u64);
// Decrypt
let decrypted_reader = service
.decrypt_object(bucket, object_key, result.ciphertext, &result.metadata, None)
.await
.expect("Decryption failed");
let mut decrypted_data = Vec::new();
let mut reader = decrypted_reader;
reader
.read_to_end(&mut decrypted_data)
.await
.expect("Failed to read decrypted data");
assert_eq!(decrypted_data, data);
}
#[tokio::test]
async fn test_sse_c_encryption() {
let (service, _temp_dir) = create_test_service().await;
let bucket = "test-bucket";
let object_key = "test-object";
let data = b"Hello, SSE-C!";
let reader = Cursor::new(data.to_vec());
let customer_key = [0u8; 32]; // 256-bit key
// Encrypt with SSE-C
let result = service
.encrypt_object_with_customer_key(bucket, object_key, reader, &customer_key, None)
.await
.expect("SSE-C encryption failed");
assert!(!result.ciphertext.is_empty());
assert_eq!(result.metadata.key_id, "sse-c");
assert_eq!(result.metadata.original_size, data.len() as u64);
// Decrypt with same customer key
let decrypted_reader = service
.decrypt_object_with_customer_key(bucket, object_key, result.ciphertext, &result.metadata, &customer_key)
.await
.expect("SSE-C decryption failed");
let mut decrypted_data = Vec::new();
let mut reader = decrypted_reader;
reader
.read_to_end(&mut decrypted_data)
.await
.expect("Failed to read decrypted data");
assert_eq!(decrypted_data, data);
}
#[tokio::test]
async fn test_metadata_headers_conversion() {
let (service, _temp_dir) = create_test_service().await;
let metadata = EncryptionMetadata {
algorithm: "AES256".to_string(),
key_id: "test-key".to_string(),
key_version: 1,
iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
tag: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
encryption_context: HashMap::from([("bucket".to_string(), "test-bucket".to_string())]),
encrypted_at: Zoned::now(),
original_size: 100,
encrypted_data_key: vec![1, 2, 3, 4],
};
// Convert to headers
let headers = service.metadata_to_headers(&metadata);
assert!(headers.contains_key("x-amz-server-side-encryption"));
assert!(headers.contains_key("x-rustfs-encryption-iv"));
// Convert back to metadata
let parsed_metadata = service.headers_to_metadata(&headers).expect("Failed to parse headers");
assert_eq!(parsed_metadata.algorithm, metadata.algorithm);
assert_eq!(parsed_metadata.key_id, metadata.key_id);
assert_eq!(parsed_metadata.iv, metadata.iv);
assert_eq!(parsed_metadata.tag, metadata.tag);
}
#[tokio::test]
async fn test_encryption_context_validation() {
let (service, _temp_dir) = create_test_service().await;
let actual_context = HashMap::from([
("bucket".to_string(), "test-bucket".to_string()),
("object".to_string(), "test-object".to_string()),
]);
let valid_expected = HashMap::from([("bucket".to_string(), "test-bucket".to_string())]);
let invalid_expected = HashMap::from([("bucket".to_string(), "wrong-bucket".to_string())]);
// Valid context should pass
assert!(service.validate_encryption_context(&actual_context, &valid_expected).is_ok());
// Invalid context should fail
assert!(
service
.validate_encryption_context(&actual_context, &invalid_expected)
.is_err()
);
}
}