mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 11:56:38 +00:00
feature: support kms && encryt (#573)
* feat(kms): implement key management service with local and vault backends Signed-off-by: junxiang Mu <1948535941@qq.com> * feat(kms): enhance security with zeroize for sensitive data and improve key management Signed-off-by: junxiang Mu <1948535941@qq.com> * remove Hashi word Signed-off-by: junxiang Mu <1948535941@qq.com> * refactor: remove unused request structs from kms handlers Signed-off-by: junxiang Mu <1948535941@qq.com> --------- Signed-off-by: junxiang Mu <1948535941@qq.com>
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
// 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.
|
||||
|
||||
//! Cipher implementations for object encryption
|
||||
|
||||
use crate::error::{KmsError, Result};
|
||||
use crate::types::EncryptionAlgorithm;
|
||||
use aes_gcm::aead::rand_core::RngCore;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key, Nonce,
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
};
|
||||
use chacha20poly1305::ChaCha20Poly1305;
|
||||
|
||||
/// Trait for object encryption ciphers
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub trait ObjectCipher: Send + Sync {
|
||||
/// Encrypt data with the given IV and AAD
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)>;
|
||||
|
||||
/// Decrypt data with the given IV, tag, and AAD
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>>;
|
||||
|
||||
/// Get the algorithm name
|
||||
fn algorithm(&self) -> &'static str;
|
||||
|
||||
/// Get the required key size in bytes
|
||||
fn key_size(&self) -> usize;
|
||||
|
||||
/// Get the required IV size in bytes
|
||||
fn iv_size(&self) -> usize;
|
||||
|
||||
/// Get the tag size in bytes
|
||||
fn tag_size(&self) -> usize;
|
||||
}
|
||||
|
||||
/// AES-256-GCM cipher implementation
|
||||
pub struct AesCipher {
|
||||
cipher: Aes256Gcm,
|
||||
}
|
||||
|
||||
impl AesCipher {
|
||||
/// Create a new AES cipher with the given key
|
||||
pub fn new(key: &[u8]) -> Result<Self> {
|
||||
if key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, key.len()));
|
||||
}
|
||||
|
||||
let key = Key::<Aes256Gcm>::from_slice(key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
Ok(Self { cipher })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectCipher for AesCipher {
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(iv);
|
||||
|
||||
// AES-GCM includes the tag in the ciphertext
|
||||
let ciphertext_with_tag = self
|
||||
.cipher
|
||||
.encrypt(nonce, aes_gcm::aead::Payload { msg: plaintext, aad })
|
||||
.map_err(KmsError::from_aes_gcm_error)?;
|
||||
|
||||
// Split ciphertext and tag
|
||||
let tag_size = self.tag_size();
|
||||
if ciphertext_with_tag.len() < tag_size {
|
||||
return Err(KmsError::cryptographic_error("AES-GCM encrypt", "Ciphertext too short for tag"));
|
||||
}
|
||||
|
||||
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
|
||||
|
||||
Ok((ciphertext.to_vec(), tag.to_vec()))
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
if tag.len() != self.tag_size() {
|
||||
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(iv);
|
||||
|
||||
// Combine ciphertext and tag for AES-GCM
|
||||
let mut ciphertext_with_tag = ciphertext.to_vec();
|
||||
ciphertext_with_tag.extend_from_slice(tag);
|
||||
|
||||
let plaintext = self
|
||||
.cipher
|
||||
.decrypt(
|
||||
nonce,
|
||||
aes_gcm::aead::Payload {
|
||||
msg: &ciphertext_with_tag,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.map_err(KmsError::from_aes_gcm_error)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static str {
|
||||
"AES-256-GCM"
|
||||
}
|
||||
|
||||
fn key_size(&self) -> usize {
|
||||
32 // 256 bits
|
||||
}
|
||||
|
||||
fn iv_size(&self) -> usize {
|
||||
12 // 96 bits for GCM
|
||||
}
|
||||
|
||||
fn tag_size(&self) -> usize {
|
||||
16 // 128 bits
|
||||
}
|
||||
}
|
||||
|
||||
/// ChaCha20-Poly1305 cipher implementation
|
||||
pub struct ChaCha20Cipher {
|
||||
cipher: ChaCha20Poly1305,
|
||||
}
|
||||
|
||||
impl ChaCha20Cipher {
|
||||
/// Create a new ChaCha20 cipher with the given key
|
||||
pub fn new(key: &[u8]) -> Result<Self> {
|
||||
if key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, key.len()));
|
||||
}
|
||||
|
||||
let key = chacha20poly1305::Key::from_slice(key);
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
|
||||
Ok(Self { cipher })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectCipher for ChaCha20Cipher {
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
let nonce = chacha20poly1305::Nonce::from_slice(iv);
|
||||
|
||||
// ChaCha20-Poly1305 includes the tag in the ciphertext
|
||||
let ciphertext_with_tag = self
|
||||
.cipher
|
||||
.encrypt(nonce, chacha20poly1305::aead::Payload { msg: plaintext, aad })
|
||||
.map_err(KmsError::from_chacha20_error)?;
|
||||
|
||||
// Split ciphertext and tag
|
||||
let tag_size = self.tag_size();
|
||||
if ciphertext_with_tag.len() < tag_size {
|
||||
return Err(KmsError::cryptographic_error("ChaCha20-Poly1305 encrypt", "Ciphertext too short for tag"));
|
||||
}
|
||||
|
||||
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
|
||||
|
||||
Ok((ciphertext.to_vec(), tag.to_vec()))
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
if tag.len() != self.tag_size() {
|
||||
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
|
||||
}
|
||||
|
||||
let nonce = chacha20poly1305::Nonce::from_slice(iv);
|
||||
|
||||
// Combine ciphertext and tag for ChaCha20-Poly1305
|
||||
let mut ciphertext_with_tag = ciphertext.to_vec();
|
||||
ciphertext_with_tag.extend_from_slice(tag);
|
||||
|
||||
let plaintext = self
|
||||
.cipher
|
||||
.decrypt(
|
||||
nonce,
|
||||
chacha20poly1305::aead::Payload {
|
||||
msg: &ciphertext_with_tag,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.map_err(KmsError::from_chacha20_error)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static str {
|
||||
"ChaCha20-Poly1305"
|
||||
}
|
||||
|
||||
fn key_size(&self) -> usize {
|
||||
32 // 256 bits
|
||||
}
|
||||
|
||||
fn iv_size(&self) -> usize {
|
||||
12 // 96 bits
|
||||
}
|
||||
|
||||
fn tag_size(&self) -> usize {
|
||||
16 // 128 bits
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cipher instance for the given algorithm and key
|
||||
pub fn create_cipher(algorithm: &EncryptionAlgorithm, key: &[u8]) -> Result<Box<dyn ObjectCipher>> {
|
||||
match algorithm {
|
||||
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => Ok(Box::new(AesCipher::new(key)?)),
|
||||
EncryptionAlgorithm::ChaCha20Poly1305 => Ok(Box::new(ChaCha20Cipher::new(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random IV for the given algorithm
|
||||
pub fn generate_iv(algorithm: &EncryptionAlgorithm) -> Vec<u8> {
|
||||
let iv_size = match algorithm {
|
||||
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => 12,
|
||||
EncryptionAlgorithm::ChaCha20Poly1305 => 12,
|
||||
};
|
||||
|
||||
let mut iv = vec![0u8; iv_size];
|
||||
OsRng.fill_bytes(&mut iv);
|
||||
iv
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_aes_cipher() {
|
||||
let key = [0u8; 32]; // 256-bit key
|
||||
let cipher = AesCipher::new(&key).expect("Failed to create AES cipher");
|
||||
|
||||
let plaintext = b"Hello, World!";
|
||||
let iv = [0u8; 12]; // 96-bit IV
|
||||
let aad = b"additional data";
|
||||
|
||||
// Test encryption
|
||||
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
|
||||
assert!(!ciphertext.is_empty());
|
||||
assert_eq!(tag.len(), 16); // 128-bit tag
|
||||
|
||||
// Test decryption
|
||||
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Test properties
|
||||
assert_eq!(cipher.algorithm(), "AES-256-GCM");
|
||||
assert_eq!(cipher.key_size(), 32);
|
||||
assert_eq!(cipher.iv_size(), 12);
|
||||
assert_eq!(cipher.tag_size(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chacha20_cipher() {
|
||||
let key = [0u8; 32]; // 256-bit key
|
||||
let cipher = ChaCha20Cipher::new(&key).expect("Failed to create ChaCha20 cipher");
|
||||
|
||||
let plaintext = b"Hello, ChaCha20!";
|
||||
let iv = [0u8; 12]; // 96-bit IV
|
||||
let aad = b"additional data";
|
||||
|
||||
// Test encryption
|
||||
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
|
||||
assert!(!ciphertext.is_empty());
|
||||
assert_eq!(tag.len(), 16); // 128-bit tag
|
||||
|
||||
// Test decryption
|
||||
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Test properties
|
||||
assert_eq!(cipher.algorithm(), "ChaCha20-Poly1305");
|
||||
assert_eq!(cipher.key_size(), 32);
|
||||
assert_eq!(cipher.iv_size(), 12);
|
||||
assert_eq!(cipher.tag_size(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_cipher() {
|
||||
let key = [0u8; 32];
|
||||
|
||||
// Test AES creation
|
||||
let aes_cipher = create_cipher(&EncryptionAlgorithm::Aes256, &key).expect("Failed to create AES cipher");
|
||||
assert_eq!(aes_cipher.algorithm(), "AES-256-GCM");
|
||||
|
||||
// Test ChaCha20 creation
|
||||
let chacha_cipher =
|
||||
create_cipher(&EncryptionAlgorithm::ChaCha20Poly1305, &key).expect("Failed to create ChaCha20 cipher");
|
||||
assert_eq!(chacha_cipher.algorithm(), "ChaCha20-Poly1305");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_iv() {
|
||||
let aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
|
||||
assert_eq!(aes_iv.len(), 12);
|
||||
|
||||
let chacha_iv = generate_iv(&EncryptionAlgorithm::ChaCha20Poly1305);
|
||||
assert_eq!(chacha_iv.len(), 12);
|
||||
|
||||
// IVs should be different
|
||||
let another_aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
|
||||
assert_ne!(aes_iv, another_aes_iv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_key_size() {
|
||||
let short_key = [0u8; 16]; // Too short
|
||||
|
||||
assert!(AesCipher::new(&short_key).is_err());
|
||||
assert!(ChaCha20Cipher::new(&short_key).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_iv_size() {
|
||||
let key = [0u8; 32];
|
||||
let cipher = AesCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let plaintext = b"test";
|
||||
let short_iv = [0u8; 8]; // Too short
|
||||
let aad = b"";
|
||||
|
||||
assert!(cipher.encrypt(plaintext, &short_iv, aad).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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 implementation
|
||||
|
||||
mod ciphers;
|
||||
pub mod service;
|
||||
|
||||
pub use service::ObjectEncryptionService;
|
||||
@@ -0,0 +1,754 @@
|
||||
// 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 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();
|
||||
}
|
||||
}
|
||||
use base64::Engine;
|
||||
use rand::random;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// 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
|
||||
pub fn new(kms_manager: KmsManager) -> Self {
|
||||
Self { kms_manager }
|
||||
}
|
||||
|
||||
/// Create a new master key (delegates to KMS manager)
|
||||
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)
|
||||
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
||||
self.kms_manager.describe_key(request).await
|
||||
}
|
||||
|
||||
/// List master keys (delegates to KMS manager)
|
||||
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)
|
||||
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||
self.kms_manager.generate_data_key(request).await
|
||||
}
|
||||
|
||||
/// Get the default key ID
|
||||
pub fn get_default_key_id(&self) -> Option<&String> {
|
||||
self.kms_manager.get_default_key_id()
|
||||
}
|
||||
|
||||
/// Get cache statistics
|
||||
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
|
||||
self.kms_manager.cache_stats().await
|
||||
}
|
||||
|
||||
/// Clear the cache
|
||||
pub async fn clear_cache(&self) -> Result<()> {
|
||||
self.kms_manager.clear_cache().await
|
||||
}
|
||||
|
||||
/// Get backend health status
|
||||
pub async fn health_check(&self) -> Result<bool> {
|
||||
self.kms_manager.health_check().await
|
||||
}
|
||||
|
||||
/// Create a data encryption key for object encryption
|
||||
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
|
||||
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 '{}' not found. Please create it first.", actual_key_id))
|
||||
})?;
|
||||
}
|
||||
|
||||
// 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: chrono::Utc::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!("{:x}", actual_md5);
|
||||
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: chrono::Utc::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)
|
||||
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
|
||||
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 '{}': expected '{}', got '{}'",
|
||||
key, expected_value, actual_value
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(KmsError::context_mismatch(format!("Missing context key '{}'", key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert encryption metadata to HTTP headers for S3 compatibility
|
||||
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
|
||||
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: chrono::Utc::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: chrono::Utc::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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user