diff --git a/.vscode/launch.json b/.vscode/launch.json index 39f29cd9..1c148313 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -170,6 +170,81 @@ "sourceLanguages": [ "rust" ], - } + }, + { + "name": "Debug executable target/debug/rustfs with sse", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/target/debug/rustfs", + "args": [], + "cwd": "${workspaceFolder}", + //"stopAtEntry": false, + //"preLaunchTask": "cargo build", + "env": { + "RUSTFS_ACCESS_KEY": "rustfsadmin", + "RUSTFS_SECRET_KEY": "rustfsadmin", + "RUSTFS_VOLUMES": "./target/volumes/test{1...4}", + "RUSTFS_ADDRESS": ":9000", + "RUSTFS_CONSOLE_ENABLE": "true", + "RUSTFS_CONSOLE_ADDRESS": "127.0.0.1:9001", + "RUSTFS_OBS_LOG_DIRECTORY": "./target/logs", + // "RUSTFS_OBS_TRACE_ENDPOINT": "http://127.0.0.1:4318/v1/traces", // jeager otlp http endpoint + // "RUSTFS_OBS_METRIC_ENDPOINT": "http://127.0.0.1:4318/v1/metrics", // default otlp http endpoint + // "RUSTFS_OBS_LOG_ENDPOINT": "http://127.0.0.1:4318/v1/logs", // default otlp http endpoint + // "RUSTFS_COMPRESS_ENABLE": "true", + + // 1. simple sse test key (no kms system) + // "__RUSTFS_SSE_SIMPLE_CMK": "2dfNXGHlsEflGVCxb+5DIdGEl1sIvtwX+QfmYasi5QM=", + + // 2. kms local backend test key + "RUSTFS_KMS_ENABLE": "true", + "RUSTFS_KMS_BACKEND": "local", + "RUSTFS_KMS_KEY_DIR": "./target/kms-key-dir", + "RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password + "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + + // 3. kms vault backend test key + // "RUSTFS_KMS_ENABLE": "true", + // "RUSTFS_KMS_BACKEND": "vault", + // "RUSTFS_KMS_VAULT_ADDRESS": "http://127.0.0.1:8200", + // "RUSTFS_KMS_VAULT_TOKEN": "Dev Token", + // "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + + }, + "sourceLanguages": [ + "rust" + ], + }, + { + "name": "E2E test executable target/debug/rustfs", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/target/debug/rustfs", + "args": [], + "cwd": "${workspaceFolder}", + //"stopAtEntry": false, + //"preLaunchTask": "cargo build", + "env": { + "RUST_LOG": "rustfs=debug,ecstore=info,s3s=debug,iam=debug", + "RUST_BACKTRACE": "full", + "RUSTFS_ACCESS_KEY": "rustfsadmin", + "RUSTFS_SECRET_KEY": "rustfsadmin", + "RUSTFS_VOLUMES": "./target/e2e-test/test{1...4}", + "RUSTFS_REGION": "us-east-1", + "RUSTFS_ADDRESS": ":9000", + "RUSTFS_CONSOLE_ENABLE": "true", + "RUSTFS_CONSOLE_ADDRESS": "127.0.0.1:9001", + "RUSTFS_OBS_LOG_DIRECTORY": "./target/logs", + + "RUSTFS_KMS_ENABLE": "true", + "RUSTFS_KMS_BACKEND": "local", + "RUSTFS_KMS_KEY_DIR": "./target/e2e-key-dir", + "RUSTFS_KMS_LOCAL_MASTER_KEY": "my-secret-key", // Some Password + "RUSTFS_KMS_DEFAULT_KEY_ID": "rustfs-master-key", + }, + "sourceLanguages": [ + "rust" + ], + }, ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fe959795..8ac895b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7628,6 +7628,7 @@ dependencies = [ name = "rustfs" version = "0.0.5" dependencies = [ + "aes-gcm 0.11.0-rc.2", "astral-tokio-tar", "async-trait", "atoi", @@ -7990,6 +7991,7 @@ name = "rustfs-kms" version = "0.0.5" dependencies = [ "aes-gcm 0.11.0-rc.2", + "arc-swap", "async-trait", "base64", "chacha20poly1305", diff --git a/crates/ecstore/src/store_api.rs b/crates/ecstore/src/store_api.rs index d1ea39f2..cbb696b4 100644 --- a/crates/ecstore/src/store_api.rs +++ b/crates/ecstore/src/store_api.rs @@ -715,7 +715,16 @@ impl ObjectInfo { return Ok(actual_size); } - // TODO: IsEncrypted + // Check if object is encrypted + // Encrypted objects store original size in x-rustfs-encryption-original-size metadata + if let Some(size_str) = self.user_defined.get("x-rustfs-encryption-original-size") + && !size_str.is_empty() + { + let size = size_str + .parse::() + .map_err(|e| std::io::Error::other(format!("Failed to parse encryption original size: {e}")))?; + return Ok(size); + } Ok(self.size) } diff --git a/crates/kms/.gitignore b/crates/kms/.gitignore new file mode 100644 index 00000000..bc178782 --- /dev/null +++ b/crates/kms/.gitignore @@ -0,0 +1 @@ +examples/local_data/* \ No newline at end of file diff --git a/crates/kms/Cargo.toml b/crates/kms/Cargo.toml index 2a9b1f0f..b22f1271 100644 --- a/crates/kms/Cargo.toml +++ b/crates/kms/Cargo.toml @@ -55,6 +55,7 @@ moka = { workspace = true, features = ["future"] } # Additional dependencies md5 = { workspace = true } +arc-swap = { workspace = true } # HTTP client for Vault reqwest = { workspace = true } diff --git a/crates/kms/examples/kms_local_demo.rs b/crates/kms/examples/kms_local_demo.rs new file mode 100644 index 00000000..e5cd7b5a --- /dev/null +++ b/crates/kms/examples/kms_local_demo.rs @@ -0,0 +1,251 @@ +// 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. + +//! KMS Demo - Comprehensive example demonstrating RustFS KMS capabilities +//! +//! This example demonstrates: +//! - Initializing and configuring KMS service +//! - Creating master keys +//! - Generating data encryption keys +//! - Encrypting and decrypting data using high-level APIs +//! - Key management operations +//! - Cache statistics +//! +//! Run with: `cargo run --example demo1` + +use rustfs_kms::{ + CreateKeyRequest, DescribeKeyRequest, EncryptionAlgorithm, GenerateDataKeyRequest, KeySpec, KeyUsage, KmsConfig, + ListKeysRequest, init_global_kms_service_manager, +}; +use std::collections::HashMap; +use std::fs; +use std::io::Cursor; +use tokio::io::AsyncReadExt; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Note: Tracing is optional - if tracing-subscriber is not available, + // the example will still work but with less detailed logging + + println!("=== RustFS KMS Demo ===\n"); + + // Step 1: Initialize global KMS service manager + println!("1. Initializing KMS service manager..."); + let service_manager = init_global_kms_service_manager(); + println!(" ✓ Service manager initialized\n"); + + // Step 2: Create a temporary directory for local backend + println!("2. Setting up local backend..."); + if fs::metadata("examples/local_data").is_err() { + fs::create_dir_all("examples/local_data")?; + } + let data_dir = std::path::PathBuf::from("examples/local_data"); + println!(" ✓ Using data directory: {}\n", data_dir.display()); + + // Step 3: Configure KMS with local backend + println!("3. Configuring KMS with local backend..."); + let config = KmsConfig::local(data_dir) + .with_default_key("demo-key-default-1".to_string()) + .with_cache(true); + service_manager.configure(config).await?; + println!(" ✓ KMS configured\n"); + + // Step 4: Start the KMS service + println!("4. Starting KMS service..."); + service_manager.start().await?; + println!(" ✓ KMS service started\n"); + + // Step 5: Get the encryption service + println!("5. Getting encryption service..."); + let encryption_service = rustfs_kms::get_global_encryption_service() + .await + .ok_or("Encryption service not available")?; + println!(" ✓ Encryption service obtained\n"); + + // Step 6: Create a master key + println!("6. Creating a master key..."); + let create_request = CreateKeyRequest { + key_name: Some("demo-key-master-1".to_string()), + key_usage: KeyUsage::EncryptDecrypt, + description: Some("Demo master key for encryption".to_string()), + policy: None, + tags: { + let mut tags = HashMap::new(); + tags.insert("environment".to_string(), "demo".to_string()); + tags.insert("purpose".to_string(), "testing".to_string()); + tags + }, + origin: Some("demo1.rs".to_string()), + }; + + let create_response = encryption_service.create_key(create_request).await?; + println!(" ✓ Master key created:"); + println!(" - Key ID: {}", create_response.key_id); + println!(" - Key State: {:?}", create_response.key_metadata.key_state); + println!(" - Key Usage: {:?}", create_response.key_metadata.key_usage); + println!(" - Created: {}\n", create_response.key_metadata.creation_date); + + let master_key_id = create_response.key_id.clone(); + + // Step 7: Describe the key + println!("7. Describing the master key..."); + let describe_request = DescribeKeyRequest { + key_id: master_key_id.clone(), + }; + let describe_response = encryption_service.describe_key(describe_request).await?; + let metadata = describe_response.key_metadata; + println!(" ✓ Key details:"); + println!(" - Key ID: {}", metadata.key_id); + println!(" - Description: {:?}", metadata.description); + println!(" - Key Usage: {:?}", metadata.key_usage); + println!(" - Key State: {:?}", metadata.key_state); + println!(" - Tags: {:?}\n", metadata.tags); + + // Step 8: Generate a data encryption key (OPTIONAL - for demonstration only) + // NOTE: This step is OPTIONAL and only for educational purposes! + // In real usage, you can skip this step and go directly to Step 9. + // encrypt_object() will automatically generate a data key internally. + println!("8. [OPTIONAL] Generating a data encryption key (for demonstration)..."); + println!(" ⚠️ This step is OPTIONAL - only for understanding the two-layer key architecture:"); + println!(" - Master Key (CMK): Used to encrypt/decrypt data keys"); + println!(" - Data Key (DEK): Used to encrypt/decrypt actual data"); + println!(" In production, you can skip this and use encrypt_object() directly!\n"); + + let data_key_request = GenerateDataKeyRequest { + key_id: master_key_id.clone(), + key_spec: KeySpec::Aes256, + encryption_context: { + let mut context = HashMap::new(); + context.insert("bucket".to_string(), "demo-bucket".to_string()); + context.insert("object_key".to_string(), "demo-object.txt".to_string()); + context + }, + }; + + let data_key_response = encryption_service.generate_data_key(data_key_request).await?; + println!(" ✓ Data key generated (for demonstration):"); + println!(" - Master Key ID: {}", data_key_response.key_id); + println!(" - Data Key (plaintext) length: {} bytes", data_key_response.plaintext_key.len()); + println!( + " - Encrypted Data Key (ciphertext blob) length: {} bytes", + data_key_response.ciphertext_blob.len() + ); + println!(" - Note: This data key is NOT used in Step 9 - encrypt_object() generates its own!\n"); + + // Step 9: Encrypt some data using high-level API + // This is the RECOMMENDED way to encrypt data - everything is handled automatically! + println!("9. Encrypting data using object encryption service (RECOMMENDED)..."); + println!(" ✅ This is all you need! encrypt_object() handles everything:"); + println!(" 1. Validates/creates the master key (if needed)"); + println!(" 2. Generates a NEW data key using the master key (independent of Step 8)"); + println!(" 3. Uses the data key to encrypt the actual data"); + println!(" 4. Stores the encrypted data key (ciphertext blob) in metadata"); + println!(" You only need to provide the master_key_id - everything else is handled!\n"); + + let plaintext = b"Hello, RustFS KMS! This is a test message for encryption."; + println!(" Plaintext: {}", String::from_utf8_lossy(plaintext)); + + let reader = Cursor::new(plaintext); + // Just provide the master_key_id - encrypt_object() handles everything internally! + let encryption_result = encryption_service + .encrypt_object( + "demo-bucket", + "demo-object.txt", + reader, + &EncryptionAlgorithm::Aes256, + Some(&master_key_id), // Only need to provide master key ID + None, + ) + .await?; + + println!(" ✓ Data encrypted:"); + println!(" - Encrypted data length: {} bytes", encryption_result.ciphertext.len()); + println!(" - Algorithm: {}", encryption_result.metadata.algorithm); + println!( + " - Master Key ID: {} (used to encrypt the data key)", + encryption_result.metadata.key_id + ); + println!( + " - Encrypted Data Key length: {} bytes (stored in metadata)", + encryption_result.metadata.encrypted_data_key.len() + ); + println!(" - Original size: {} bytes\n", encryption_result.metadata.original_size); + + // Step 10: Decrypt the data using high-level API + println!("10. Decrypting data..."); + println!(" Note: decrypt_object() has the ENTIRE decryption flow built-in:"); + println!(" 1. Extracts the encrypted data key from metadata"); + println!(" 2. Uses master key to decrypt the data key"); + println!(" 3. Uses the decrypted data key to decrypt the actual data"); + println!(" You only need to provide the encrypted data and metadata!\n"); + + let mut decrypted_reader = encryption_service + .decrypt_object( + "demo-bucket", + "demo-object.txt", + encryption_result.ciphertext.clone(), + &encryption_result.metadata, // Contains everything needed for decryption + None, + ) + .await?; + + let mut decrypted_data = Vec::new(); + decrypted_reader.read_to_end(&mut decrypted_data).await?; + + println!(" ✓ Data decrypted:"); + println!(" - Decrypted text: {}\n", String::from_utf8_lossy(&decrypted_data)); + + // Verify decryption + assert_eq!(plaintext, decrypted_data.as_slice()); + println!(" ✓ Decryption verified: plaintext matches original\n"); + + // Step 11: List all keys + println!("11. Listing all keys..."); + let list_request = ListKeysRequest { + limit: Some(10), + marker: None, + usage_filter: None, + status_filter: None, + }; + let list_response = encryption_service.list_keys(list_request).await?; + println!(" ✓ Keys found: {}", list_response.keys.len()); + for (idx, key_info) in list_response.keys.iter().enumerate() { + println!(" {}. {} ({:?})", idx + 1, key_info.key_id, key_info.status); + } + println!(); + + // Step 12: Check cache statistics + println!("12. Checking cache statistics..."); + if let Some((hits, misses)) = encryption_service.cache_stats().await { + println!(" ✓ Cache statistics:"); + println!(" - Cache hits: {}", hits); + println!(" - Cache misses: {}\n", misses); + } else { + println!(" - Cache is disabled\n"); + } + + // Step 13: Health check + println!("13. Performing health check..."); + let is_healthy = encryption_service.health_check().await?; + println!(" ✓ KMS backend is healthy: {}\n", is_healthy); + + // Step 14: Stop the service + println!("14. Stopping KMS service..."); + service_manager.stop().await?; + println!(" ✓ KMS service stopped\n"); + + println!("=== Demo completed successfully! ==="); + + Ok(()) +} diff --git a/crates/kms/examples/kms_vault_kv_demo.rs b/crates/kms/examples/kms_vault_kv_demo.rs new file mode 100644 index 00000000..0d0200ba --- /dev/null +++ b/crates/kms/examples/kms_vault_kv_demo.rs @@ -0,0 +1,292 @@ +// 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. + +//! KMS Demo 2 - Comprehensive example demonstrating RustFS KMS with Vault backend +//! +//! This example demonstrates: +//! - Initializing and configuring KMS service with Vault backend +//! - Creating master keys stored in Vault +//! - Generating data encryption keys +//! - Encrypting and decrypting data using high-level APIs +//! - Key management operations with Vault +//! - Cache statistics +//! +//! Prerequisites: +//! - Vault server running at http://127.0.0.1:8200 (or set RUSTFS_KMS_VAULT_ADDRESS) +//! - Vault token (set RUSTFS_KMS_VAULT_TOKEN environment variable, or use default "dev-token" for dev mode) +//! +//! Run with: `cargo run --example demo2` +//! Or with custom Vault settings: +//! RUSTFS_KMS_VAULT_ADDRESS=http://127.0.0.1:8200 RUSTFS_KMS_VAULT_TOKEN=your-token cargo run --example demo2 + +use rustfs_kms::{ + CreateKeyRequest, DescribeKeyRequest, EncryptionAlgorithm, GenerateDataKeyRequest, KeySpec, KeyUsage, KmsConfig, KmsError, + ListKeysRequest, init_global_kms_service_manager, +}; +use std::collections::HashMap; +use std::io::Cursor; +use tokio::io::AsyncReadExt; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Note: Tracing is optional - if tracing-subscriber is not available, + // the example will still work but with less detailed logging + + println!("=== RustFS KMS Demo 2 (Vault Backend) ===\n"); + + // Step 1: Initialize global KMS service manager + println!("1. Initializing KMS service manager..."); + let service_manager = init_global_kms_service_manager(); + println!(" ✓ Service manager initialized\n"); + + // Step 2: Get Vault configuration from environment or use defaults + println!("2. Configuring Vault backend..."); + let vault_address = std::env::var("RUSTFS_KMS_VAULT_ADDRESS").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()); + let vault_token = std::env::var("RUSTFS_KMS_VAULT_TOKEN").unwrap_or_else(|_| { + println!(" ⚠️ No RUSTFS_KMS_VAULT_TOKEN found, using default 'dev-token'"); + println!(" For production, set RUSTFS_KMS_VAULT_TOKEN environment variable"); + "dev-token".to_string() + }); + + let vault_url = Url::parse(&vault_address).map_err(|e| format!("Invalid Vault address '{}': {}", vault_address, e))?; + + println!(" ✓ Vault address: {}", vault_address); + println!(" ✓ Using token authentication\n"); + + // Step 3: Configure KMS with Vault backend + println!("3. Configuring KMS with Vault backend..."); + let config = KmsConfig::vault(vault_url, vault_token) + .with_default_key("demo-key-master-1".to_string()) + .with_cache(true); + service_manager.configure(config).await?; + println!(" ✓ KMS configured with Vault backend\n"); + + // Step 4: Start the KMS service + println!("4. Starting KMS service..."); + service_manager.start().await?; + println!(" ✓ KMS service started\n"); + + // Step 5: Get the encryption service + println!("5. Getting encryption service..."); + let encryption_service = rustfs_kms::get_global_encryption_service() + .await + .ok_or("Encryption service not available")?; + println!(" ✓ Encryption service obtained\n"); + + // Step 6: Create a master key (stored in Vault) or use existing one + println!("6. Checking for existing master key in Vault..."); + let master_key_id = "demo-key-master-1".to_string(); + let describe_request = DescribeKeyRequest { + key_id: master_key_id.clone(), + }; + + let master_key_id = match encryption_service.describe_key(describe_request).await { + Ok(describe_response) => { + // Key already exists, use it + println!(" ✓ Master key already exists in Vault:"); + println!(" - Key ID: {}", describe_response.key_metadata.key_id); + println!(" - Key State: {:?}", describe_response.key_metadata.key_state); + println!(" - Key Usage: {:?}", describe_response.key_metadata.key_usage); + println!(" - Created: {}\n", describe_response.key_metadata.creation_date); + describe_response.key_metadata.key_id + } + Err(KmsError::KeyNotFound { .. }) => { + // Key doesn't exist, create it + println!(" Key not found, creating new master key in Vault..."); + let create_request = CreateKeyRequest { + key_name: Some(master_key_id.clone()), + key_usage: KeyUsage::EncryptDecrypt, + description: Some("Demo master key for encryption (stored in Vault)".to_string()), + policy: None, + tags: { + let mut tags = HashMap::new(); + tags.insert("environment".to_string(), "demo".to_string()); + tags.insert("purpose".to_string(), "testing".to_string()); + tags.insert("backend".to_string(), "vault".to_string()); + tags + }, + origin: Some("demo2.rs".to_string()), + }; + + let create_response = encryption_service.create_key(create_request).await?; + println!(" ✓ Master key created in Vault:"); + println!(" - Key ID: {}", create_response.key_id); + println!(" - Key State: {:?}", create_response.key_metadata.key_state); + println!(" - Key Usage: {:?}", create_response.key_metadata.key_usage); + println!(" - Created: {}\n", create_response.key_metadata.creation_date); + create_response.key_id + } + Err(e) => { + // Other error, return it + return Err(Box::new(e) as Box); + } + }; + + // Step 7: Describe the key (retrieved from Vault) + println!("7. Describing the master key (from Vault)..."); + let describe_request = DescribeKeyRequest { + key_id: master_key_id.clone(), + }; + let describe_response = encryption_service.describe_key(describe_request).await?; + let metadata = describe_response.key_metadata; + println!(" ✓ Key details (from Vault):"); + println!(" - Key ID: {}", metadata.key_id); + println!(" - Description: {:?}", metadata.description); + println!(" - Key Usage: {:?}", metadata.key_usage); + println!(" - Key State: {:?}", metadata.key_state); + println!(" - Tags: {:?}\n", metadata.tags); + + // Step 8: Generate a data encryption key (OPTIONAL - for demonstration only) + // NOTE: This step is OPTIONAL and only for educational purposes! + // In real usage, you can skip this step and go directly to Step 9. + // encrypt_object() will automatically generate a data key internally. + println!("8. [OPTIONAL] Generating a data encryption key (for demonstration)..."); + println!(" ⚠️ This step is OPTIONAL - only for understanding the two-layer key architecture:"); + println!(" - Master Key (CMK): Stored in Vault, used to encrypt/decrypt data keys"); + println!(" - Data Key (DEK): Generated per object, encrypted by master key"); + println!(" In production, you can skip this and use encrypt_object() directly!\n"); + + let data_key_request = GenerateDataKeyRequest { + key_id: master_key_id.clone(), + key_spec: KeySpec::Aes256, + encryption_context: { + let mut context = HashMap::new(); + context.insert("bucket".to_string(), "demo-bucket".to_string()); + context.insert("object_key".to_string(), "demo-object.txt".to_string()); + context + }, + }; + + let data_key_response = encryption_service.generate_data_key(data_key_request).await?; + println!(" ✓ Data key generated (for demonstration):"); + println!(" - Master Key ID: {}", data_key_response.key_id); + println!(" - Data Key (plaintext) length: {} bytes", data_key_response.plaintext_key.len()); + println!( + " - Encrypted Data Key (ciphertext blob) length: {} bytes", + data_key_response.ciphertext_blob.len() + ); + println!(" - Note: This data key is NOT used in Step 9 - encrypt_object() generates its own!\n"); + + // Step 9: Encrypt some data using high-level API + // This is the RECOMMENDED way to encrypt data - everything is handled automatically! + println!("9. Encrypting data using object encryption service (RECOMMENDED)..."); + println!(" ✅ This is all you need! encrypt_object() handles everything:"); + println!(" 1. Validates/creates the master key in Vault (if needed)"); + println!(" 2. Generates a NEW data key using the master key from Vault (independent of Step 8)"); + println!(" 3. Uses the data key to encrypt the actual data"); + println!(" 4. Stores the encrypted data key (ciphertext blob) in metadata"); + println!(" You only need to provide the master_key_id - everything else is handled!\n"); + + let plaintext = b"Hello, RustFS KMS with Vault! This is a test message for encryption."; + println!(" Plaintext: {}", String::from_utf8_lossy(plaintext)); + + let reader = Cursor::new(plaintext); + // Just provide the master_key_id - encrypt_object() handles everything internally! + let encryption_result = encryption_service + .encrypt_object( + "demo-bucket", + "demo-object.txt", + reader, + &EncryptionAlgorithm::Aes256, + Some(&master_key_id), // Only need to provide master key ID + None, + ) + .await?; + + println!(" ✓ Data encrypted:"); + println!(" - Encrypted data length: {} bytes", encryption_result.ciphertext.len()); + println!(" - Algorithm: {}", encryption_result.metadata.algorithm); + println!( + " - Master Key ID: {} (stored in Vault, used to encrypt the data key)", + encryption_result.metadata.key_id + ); + println!( + " - Encrypted Data Key length: {} bytes (stored in metadata)", + encryption_result.metadata.encrypted_data_key.len() + ); + println!(" - Original size: {} bytes\n", encryption_result.metadata.original_size); + + // Step 10: Decrypt the data using high-level API + println!("10. Decrypting data..."); + println!(" Note: decrypt_object() has the ENTIRE decryption flow built-in:"); + println!(" 1. Extracts the encrypted data key from metadata"); + println!(" 2. Uses master key from Vault to decrypt the data key"); + println!(" 3. Uses the decrypted data key to decrypt the actual data"); + println!(" You only need to provide the encrypted data and metadata!\n"); + + let mut decrypted_reader = encryption_service + .decrypt_object( + "demo-bucket", + "demo-object.txt", + encryption_result.ciphertext.clone(), + &encryption_result.metadata, // Contains everything needed for decryption + None, + ) + .await?; + + let mut decrypted_data = Vec::new(); + decrypted_reader.read_to_end(&mut decrypted_data).await?; + + println!(" ✓ Data decrypted:"); + println!(" - Decrypted text: {}\n", String::from_utf8_lossy(&decrypted_data)); + + // Verify decryption + assert_eq!(plaintext, decrypted_data.as_slice()); + println!(" ✓ Decryption verified: plaintext matches original\n"); + + // Step 11: List all keys (from Vault) + println!("11. Listing all keys (from Vault)..."); + let list_request = ListKeysRequest { + limit: Some(10), + marker: None, + usage_filter: None, + status_filter: None, + }; + let list_response = encryption_service.list_keys(list_request).await?; + println!(" ✓ Keys found in Vault: {}", list_response.keys.len()); + for (idx, key_info) in list_response.keys.iter().enumerate() { + println!(" {}. {} ({:?})", idx + 1, key_info.key_id, key_info.status); + } + println!(); + + // Step 12: Check cache statistics + println!("12. Checking cache statistics..."); + if let Some((hits, misses)) = encryption_service.cache_stats().await { + println!(" ✓ Cache statistics:"); + println!(" - Cache hits: {}", hits); + println!(" - Cache misses: {}\n", misses); + } else { + println!(" - Cache is disabled\n"); + } + + // Step 13: Health check (verifies Vault connectivity) + println!("13. Performing health check (Vault connectivity)..."); + let is_healthy = encryption_service.health_check().await?; + println!(" ✓ KMS backend (Vault) is healthy: {}\n", is_healthy); + + // Step 14: Stop the service + println!("14. Stopping KMS service..."); + service_manager.stop().await?; + println!(" ✓ KMS service stopped\n"); + + println!("=== Demo 2 (Vault Backend) completed successfully! ==="); + println!("\n💡 Tips:"); + println!(" - Keys are now stored in Vault at: {}/v1/secret/data/rustfs/kms/keys/", vault_address); + println!(" - You can verify keys in Vault using: vault kv list secret/rustfs/kms/keys/"); + println!(" - For production, use proper Vault authentication (AppRole, etc.)"); + println!(" - See examples/VAULT_SETUP.md for detailed Vault configuration guide"); + + Ok(()) +} diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index fffe0434..087d2608 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -271,7 +271,7 @@ impl ConfigureVaultKmsRequest { KmsConfig { backend: KmsBackend::Vault, default_key_id: self.default_key_id.clone(), - backend_config: BackendConfig::Vault(VaultConfig { + backend_config: BackendConfig::Vault(Box::new(VaultConfig { address: self.address.clone(), auth_method: self.auth_method.clone(), namespace: self.namespace.clone(), @@ -288,7 +288,7 @@ impl ConfigureVaultKmsRequest { } else { None }, - }), + })), timeout: Duration::from_secs(self.timeout_seconds.unwrap_or(30)), retry_attempts: self.retry_attempts.unwrap_or(3), enable_cache: self.enable_cache.unwrap_or(true), diff --git a/crates/kms/src/backends/local.rs b/crates/kms/src/backends/local.rs index f9655300..faf5ef18 100644 --- a/crates/kms/src/backends/local.rs +++ b/crates/kms/src/backends/local.rs @@ -17,6 +17,7 @@ use crate::backends::{BackendInfo, KmsBackend, KmsClient}; use crate::config::KmsConfig; use crate::config::LocalConfig; +use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material}; use crate::error::{KmsError, Result}; use crate::types::*; use aes_gcm::{ @@ -24,11 +25,13 @@ use aes_gcm::{ aead::{Aead, KeyInit}, }; use async_trait::async_trait; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use jiff::Zoned; use rand::Rng; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; +use std::time::Duration; use tokio::fs; use tokio::sync::RwLock; use tracing::{debug, info, warn}; @@ -37,9 +40,11 @@ use tracing::{debug, info, warn}; pub struct LocalKmsClient { config: LocalConfig, /// In-memory cache of loaded keys for performance - key_cache: RwLock>, + key_cache: RwLock>, /// Master encryption key for encrypting stored keys master_cipher: Option, + /// DEK encryption implementation + dek_crypto: AesDekCrypto, } /// Serializable representation of a master key stored on disk @@ -55,24 +60,12 @@ struct StoredMasterKey { created_at: Zoned, rotated_at: Option, created_by: Option, - /// Encrypted key material (32 bytes for AES-256) - encrypted_key_material: Vec, + /// Encrypted key material (32 bytes encoded in base64 for AES-256) + encrypted_key_material: String, /// Nonce used for encryption nonce: Vec, } -/// Data key envelope stored with each data key generation -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DataKeyEnvelope { - key_id: String, - master_key_id: String, - key_spec: String, - encrypted_key: Vec, - nonce: Vec, - encryption_context: HashMap, - created_at: Zoned, -} - impl LocalKmsClient { /// Create a new local KMS client pub async fn new(config: LocalConfig) -> Result { @@ -95,6 +88,7 @@ impl LocalKmsClient { config, key_cache: RwLock::new(HashMap::new()), master_cipher, + dek_crypto: AesDekCrypto::new(), }) } @@ -116,8 +110,8 @@ impl LocalKmsClient { self.config.key_dir.join(format!("{key_id}.key")) } - /// Load a master key from disk - async fn load_master_key(&self, key_id: &str) -> Result { + /// Decode and decrypt a stored key file, returning both the metadata and decrypted key material + async fn decode_stored_key(&self, key_id: &str) -> Result<(StoredMasterKey, Vec)> { let key_path = self.master_key_path(key_id); if !key_path.exists() { return Err(KmsError::key_not_found(key_id)); @@ -127,7 +121,7 @@ impl LocalKmsClient { let stored_key: StoredMasterKey = serde_json::from_slice(&content)?; // Decrypt key material if master cipher is available - let _key_material = if let Some(ref cipher) = self.master_cipher { + let key_material = if let Some(ref cipher) = self.master_cipher { if stored_key.nonce.len() != 12 { return Err(KmsError::cryptographic_error("nonce", "Invalid nonce length")); } @@ -136,14 +130,29 @@ impl LocalKmsClient { nonce_array.copy_from_slice(&stored_key.nonce); let nonce = Nonce::from(nonce_array); + // Decode base64 string to bytes + let encrypted_bytes = BASE64 + .decode(&stored_key.encrypted_key_material) + .map_err(|e| KmsError::cryptographic_error("base64_decode", e.to_string()))?; + cipher - .decrypt(&nonce, stored_key.encrypted_key_material.as_ref()) + .decrypt(&nonce, encrypted_bytes.as_ref()) .map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))? } else { - stored_key.encrypted_key_material + // Decode base64 string to bytes when no encryption + BASE64 + .decode(&stored_key.encrypted_key_material) + .map_err(|e| KmsError::cryptographic_error("base64_decode", e.to_string()))? }; - Ok(MasterKey { + Ok((stored_key, key_material)) + } + + /// Load a master key from disk + async fn load_master_key(&self, key_id: &str) -> Result { + let (stored_key, _key_material) = self.decode_stored_key(key_id).await?; + + Ok(MasterKeyInfo { key_id: stored_key.key_id, version: stored_key.version, algorithm: stored_key.algorithm, @@ -158,7 +167,7 @@ impl LocalKmsClient { } /// Save a master key to disk - async fn save_master_key(&self, master_key: &MasterKey, key_material: &[u8]) -> Result<()> { + async fn save_master_key(&self, master_key: &MasterKeyInfo, key_material: &[u8]) -> Result<()> { let key_path = self.master_key_path(&master_key.key_id); // Encrypt key material if master cipher is available @@ -170,9 +179,11 @@ impl LocalKmsClient { let encrypted = cipher .encrypt(&nonce, key_material) .map_err(|e| KmsError::cryptographic_error("encrypt", e.to_string()))?; - (encrypted, nonce.to_vec()) + // Encode encrypted bytes to base64 string + (BASE64.encode(&encrypted), nonce.to_vec()) } else { - (key_material.to_vec(), Vec::new()) + // Encode key material to base64 string when no encryption + (BASE64.encode(key_material), Vec::new()) }; let stored_key = StoredMasterKey { @@ -210,39 +221,9 @@ impl LocalKmsClient { Ok(()) } - /// Generate a random 256-bit key - fn generate_key_material() -> Vec { - let mut key_material = vec![0u8; 32]; // 256 bits - rand::rng().fill(&mut key_material[..]); - key_material - } - /// Get the actual key material for a master key async fn get_key_material(&self, key_id: &str) -> Result> { - let key_path = self.master_key_path(key_id); - - if !key_path.exists() { - return Err(KmsError::key_not_found(key_id)); - } - - let content = fs::read(&key_path).await?; - let stored_key: StoredMasterKey = serde_json::from_slice(&content)?; - - // Decrypt key material if master cipher is available - let key_material = if let Some(ref cipher) = self.master_cipher { - if stored_key.nonce.len() != 12 { - return Err(KmsError::cryptographic_error("nonce", "Invalid nonce length")); - } - let mut nonce_array = [0u8; 12]; - nonce_array.copy_from_slice(&stored_key.nonce); - let nonce = Nonce::from(nonce_array); - cipher - .decrypt(&nonce, stored_key.encrypted_key_material.as_ref()) - .map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))? - } else { - stored_key.encrypted_key_material - }; - + let (_stored_key, key_material) = self.decode_stored_key(key_id).await?; Ok(key_material) } @@ -250,53 +231,22 @@ impl LocalKmsClient { async fn encrypt_with_master_key(&self, key_id: &str, plaintext: &[u8]) -> Result<(Vec, Vec)> { // Load the actual master key material let key_material = self.get_key_material(key_id).await?; - let key = Key::::try_from(key_material.as_slice()) - .map_err(|_| KmsError::cryptographic_error("key", "Invalid key length"))?; - let cipher = Aes256Gcm::new(&key); - - let mut nonce_bytes = [0u8; 12]; - rand::rng().fill(&mut nonce_bytes[..]); - - let nonce = Nonce::from(nonce_bytes); - - let ciphertext = cipher - .encrypt(&nonce, plaintext) - .map_err(|e| KmsError::cryptographic_error("encrypt", e.to_string()))?; - - Ok((ciphertext, nonce_bytes.to_vec())) + self.dek_crypto.encrypt(&key_material, plaintext).await } /// Decrypt data using a master key async fn decrypt_with_master_key(&self, key_id: &str, ciphertext: &[u8], nonce: &[u8]) -> Result> { - if nonce.len() != 12 { - return Err(KmsError::cryptographic_error("nonce", "Invalid nonce length")); - } // Load the actual master key material let key_material = self.get_key_material(key_id).await?; - let key = Key::::try_from(key_material.as_slice()) - .map_err(|_| KmsError::cryptographic_error("key", "Invalid key length"))?; - let cipher = Aes256Gcm::new(&key); - - let mut nonce_array = [0u8; 12]; - nonce_array.copy_from_slice(nonce); - let nonce_ref = Nonce::from(nonce_array); - - let plaintext = cipher - .decrypt(&nonce_ref, ciphertext) - .map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))?; - - Ok(plaintext) + self.dek_crypto.decrypt(&key_material, ciphertext, nonce).await } } #[async_trait] impl KmsClient for LocalKmsClient { - async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result { + async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result { debug!("Generating data key for master key: {}", request.master_key_id); - // Verify master key exists - let _master_key = self.describe_key(&request.master_key_id, context).await?; - // Generate random data key material let key_length = match request.key_spec.as_str() { "AES_256" => 32, @@ -310,7 +260,7 @@ impl KmsClient for LocalKmsClient { // Encrypt the data key with the master key let (encrypted_key, nonce) = self.encrypt_with_master_key(&request.master_key_id, &plaintext_key).await?; - // Create data key envelope + // Create data key envelope with master key version for rotation support let envelope = DataKeyEnvelope { key_id: uuid::Uuid::new_v4().to_string(), master_key_id: request.master_key_id.clone(), @@ -324,7 +274,7 @@ impl KmsClient for LocalKmsClient { // Serialize the envelope as the ciphertext let ciphertext = serde_json::to_vec(&envelope)?; - let data_key = DataKey::new(envelope.key_id, 1, Some(plaintext_key), ciphertext, request.key_spec.clone()); + let data_key = DataKeyInfo::new(envelope.key_id, 1, Some(plaintext_key), ciphertext, request.key_spec.clone()); info!("Generated data key for master key: {}", request.master_key_id); Ok(data_key) @@ -359,15 +309,19 @@ impl KmsClient for LocalKmsClient { let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)?; // Verify encryption context matches - if !request.encryption_context.is_empty() { - for (key, expected_value) in &request.encryption_context { - if let Some(actual_value) = envelope.encryption_context.get(key) { - if actual_value != expected_value { - return Err(KmsError::context_mismatch(format!( - "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" - ))); - } - } else { + // Check that all keys in envelope.encryption_context are present in request.encryption_context + // and their values match. This ensures the context used for decryption matches what was used for encryption. + for (key, expected_value) in &envelope.encryption_context { + if let Some(actual_value) = request.encryption_context.get(key) { + if actual_value != expected_value { + return Err(KmsError::context_mismatch(format!( + "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" + ))); + } + } else { + // If request.encryption_context is empty, allow decryption (backward compatibility) + // Otherwise, require all envelope context keys to be present + if !request.encryption_context.is_empty() { return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))); } } @@ -382,7 +336,7 @@ impl KmsClient for LocalKmsClient { Ok(plaintext) } - async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result { + async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result { debug!("Creating master key: {}", key_id); // Check if key already exists @@ -396,13 +350,13 @@ impl KmsClient for LocalKmsClient { } // Generate key material - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(algorithm)?; let created_by = context .map(|ctx| ctx.principal.clone()) .unwrap_or_else(|| "local-kms".to_string()); - let master_key = MasterKey::new_with_description(key_id.to_string(), algorithm.to_string(), Some(created_by), None); + let master_key = MasterKeyInfo::new_with_description(key_id.to_string(), algorithm.to_string(), Some(created_by), None); // Save to disk self.save_master_key(&master_key, &key_material).await?; @@ -490,7 +444,7 @@ impl KmsClient for LocalKmsClient { // For simplicity, we'll regenerate key material // In a real implementation, we'd preserve the original key material - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(&master_key.algorithm)?; self.save_master_key(&master_key, &key_material).await?; // Update cache @@ -507,7 +461,7 @@ impl KmsClient for LocalKmsClient { let mut master_key = self.load_master_key(key_id).await?; master_key.status = KeyStatus::Disabled; - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(&master_key.algorithm)?; self.save_master_key(&master_key, &key_material).await?; // Update cache @@ -529,7 +483,7 @@ impl KmsClient for LocalKmsClient { let mut master_key = self.load_master_key(key_id).await?; master_key.status = KeyStatus::PendingDeletion; - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(&master_key.algorithm)?; self.save_master_key(&master_key, &key_material).await?; // Update cache @@ -546,7 +500,7 @@ impl KmsClient for LocalKmsClient { let mut master_key = self.load_master_key(key_id).await?; master_key.status = KeyStatus::Active; - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(&master_key.algorithm)?; self.save_master_key(&master_key, &key_material).await?; // Update cache @@ -557,7 +511,7 @@ impl KmsClient for LocalKmsClient { Ok(()) } - async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { + async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { debug!("Rotating key: {}", key_id); let mut master_key = self.load_master_key(key_id).await?; @@ -565,7 +519,7 @@ impl KmsClient for LocalKmsClient { master_key.rotated_at = Some(Zoned::now()); // Generate new key material - let key_material = Self::generate_key_material(); + let key_material = generate_key_material(&master_key.algorithm)?; self.save_master_key(&master_key, &key_material).await?; // Update cache @@ -625,12 +579,13 @@ impl KmsBackend for LocalKmsBackend { // Create master key with description directly let _master_key = { + let algorithm = "AES_256"; // Generate key material - let key_material = LocalKmsClient::generate_key_material(); + let key_material = generate_key_material(algorithm)?; - let master_key = MasterKey::new_with_description( + let master_key = MasterKeyInfo::new_with_description( key_id.clone(), - "AES_256".to_string(), + algorithm.to_string(), Some("local-kms".to_string()), request.description.clone(), ); @@ -787,35 +742,19 @@ impl KmsBackend for LocalKmsBackend { return Err(KmsError::invalid_parameter("pending_window_in_days must be between 7 and 30".to_string())); } - let deletion_date = Zoned::now() + jiff::Span::new().days(days as i64); + let deletion_date = Zoned::now() + Duration::from_secs(days as u64 * 86400); master_key.status = KeyStatus::PendingDeletion; (Some(deletion_date.to_string()), Some(deletion_date)) }; // Save the updated key to disk - preserve existing key material! - // Load the stored key from disk to get the existing key material - let key_path = self.client.master_key_path(key_id); - let content = tokio::fs::read(&key_path) + // Load and decode the stored key to get the existing key material + let (_stored_key, existing_key_material) = self + .client + .decode_stored_key(key_id) .await - .map_err(|e| KmsError::internal_error(format!("Failed to read key file: {e}")))?; - let stored_key: StoredMasterKey = - serde_json::from_slice(&content).map_err(|e| KmsError::internal_error(format!("Failed to parse stored key: {e}")))?; - - // Decrypt the existing key material to preserve it - let existing_key_material = if let Some(ref cipher) = self.client.master_cipher { - if stored_key.nonce.len() != 12 { - return Err(KmsError::cryptographic_error("nonce", "Invalid nonce length")); - } - let mut nonce_array = [0u8; 12]; - nonce_array.copy_from_slice(&stored_key.nonce); - let nonce = Nonce::from(nonce_array); - cipher - .decrypt(&nonce, stored_key.encrypted_key_material.as_ref()) - .map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))? - } else { - stored_key.encrypted_key_material - }; + .map_err(|e| KmsError::internal_error(format!("Failed to decode key: {e}")))?; self.client.save_master_key(&master_key, &existing_key_material).await?; @@ -861,8 +800,14 @@ impl KmsBackend for LocalKmsBackend { master_key.status = KeyStatus::Active; // Save the updated key to disk - this is the missing critical step! - let key_material = LocalKmsClient::generate_key_material(); - self.client.save_master_key(&master_key, &key_material).await?; + // Preserve existing key material instead of generating new one + let (_stored_key, existing_key_material) = self + .client + .decode_stored_key(key_id) + .await + .map_err(|e| KmsError::internal_error(format!("Failed to decode key: {e}")))?; + + self.client.save_master_key(&master_key, &existing_key_material).await?; // Update cache let mut cache = self.client.key_cache.write().await; diff --git a/crates/kms/src/backends/mod.rs b/crates/kms/src/backends/mod.rs index 9d4550d1..1add6fe6 100644 --- a/crates/kms/src/backends/mod.rs +++ b/crates/kms/src/backends/mod.rs @@ -36,7 +36,7 @@ pub trait KmsClient: Send + Sync { /// /// # Returns /// Returns a DataKey containing both plaintext and encrypted key material - async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result; + async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result; /// Encrypt data directly using a master key /// @@ -67,7 +67,7 @@ pub trait KmsClient: Send + Sync { /// * `key_id` - Unique identifier for the new key /// * `algorithm` - Key algorithm (e.g., "AES_256") /// * `context` - Optional operation context for auditing - async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result; + async fn create_key(&self, key_id: &str, algorithm: &str, context: Option<&OperationContext>) -> Result; /// Get information about a specific key /// @@ -139,7 +139,7 @@ pub trait KmsClient: Send + Sync { /// # Arguments /// * `key_id` - The key identifier /// * `context` - Optional operation context for auditing - async fn rotate_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result; + async fn rotate_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result; /// Health check /// diff --git a/crates/kms/src/backends/vault.rs b/crates/kms/src/backends/vault.rs index 9ccd233e..971eb2de 100644 --- a/crates/kms/src/backends/vault.rs +++ b/crates/kms/src/backends/vault.rs @@ -16,14 +16,15 @@ use crate::backends::{BackendInfo, KmsBackend, KmsClient}; use crate::config::{KmsConfig, VaultConfig}; +use crate::encryption::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material}; use crate::error::{KmsError, Result}; use crate::types::*; use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose}; use jiff::Zoned; -use rand::RngCore; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::time::Duration; use tracing::{debug, info, warn}; use vaultrs::{ client::{VaultClient, VaultClientSettingsBuilder}, @@ -38,6 +39,8 @@ pub struct VaultKmsClient { kv_mount: String, /// Path prefix for storing keys key_path_prefix: String, + /// DEK encryption implementation + dek_crypto: AesDekCrypto, } /// Key data stored in Vault @@ -102,6 +105,7 @@ impl VaultKmsClient { kv_mount: config.kv_mount.clone(), key_path_prefix: config.key_path_prefix.clone(), config, + dek_crypto: AesDekCrypto::new(), }) } @@ -110,19 +114,6 @@ impl VaultKmsClient { format!("{}/{}", self.key_path_prefix, key_id) } - /// Generate key material for the given algorithm - fn generate_key_material(algorithm: &str) -> Result> { - 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) - } - /// Encrypt key material using Vault's transit engine async fn encrypt_key_material(&self, key_material: &[u8]) -> Result { // For simplicity, we'll base64 encode the key material @@ -139,6 +130,64 @@ impl VaultKmsClient { .map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string())) } + /// Get the actual key material for a master key + async fn get_key_material(&self, key_id: &str) -> Result> { + let mut key_data = self.get_key_data(key_id).await?; + + // If encrypted_key_material is empty, generate and store it (fix for old keys) + if key_data.encrypted_key_material.is_empty() { + warn!("Key {} has empty encrypted_key_material, generating and storing new key material", key_id); + let key_material = generate_key_material(&key_data.algorithm)?; + key_data.encrypted_key_material = self.encrypt_key_material(&key_material).await?; + // Store the updated key data back to Vault + self.store_key_data(key_id, &key_data).await?; + return Ok(key_material); + } + + let key_material = match self.decrypt_key_material(&key_data.encrypted_key_material).await { + Ok(km) => km, + Err(e) => { + warn!("Failed to decrypt key material for key {}: {}, generating new key material", key_id, e); + let new_key_material = generate_key_material(&key_data.algorithm)?; + key_data.encrypted_key_material = self.encrypt_key_material(&new_key_material).await?; + // Store the updated key data back to Vault + self.store_key_data(key_id, &key_data).await?; + return Ok(new_key_material); + } + }; + + // Validate key material length (should be 32 bytes for AES-256) + if key_material.len() != 32 { + // Try to fix: generate new key material if length is wrong + warn!( + "Key {} has invalid key material length ({} bytes), generating new key material", + key_id, + key_material.len() + ); + let new_key_material = generate_key_material(&key_data.algorithm)?; + key_data.encrypted_key_material = self.encrypt_key_material(&new_key_material).await?; + // Store the updated key data back to Vault + self.store_key_data(key_id, &key_data).await?; + return Ok(new_key_material); + } + + Ok(key_material) + } + + /// Encrypt data using a master key + async fn encrypt_with_master_key(&self, key_id: &str, plaintext: &[u8]) -> Result<(Vec, Vec)> { + // Load the actual master key material + let key_material = self.get_key_material(key_id).await?; + self.dek_crypto.encrypt(&key_material, plaintext).await + } + + /// Decrypt data using a master key + async fn decrypt_with_master_key(&self, key_id: &str, ciphertext: &[u8], nonce: &[u8]) -> Result> { + // Load the actual master key material + let key_material = self.get_key_material(key_id).await?; + self.dek_crypto.decrypt(&key_material, ciphertext, nonce).await + } + /// Store key data in Vault async fn store_key_data(&self, key_id: &str, key_data: &VaultKeyData) -> Result<()> { let path = self.key_path(key_id); @@ -154,19 +203,36 @@ impl VaultKmsClient { async fn store_key_metadata(&self, key_id: &str, request: &CreateKeyRequest) -> Result<()> { debug!("Storing key metadata for {}, input tags: {:?}", key_id, request.tags); + // Get existing key data to preserve encrypted_key_material and other fields + // This is called after create_key, so the key should already exist + let mut existing_key_data = self.get_key_data(key_id).await?; + + // If encrypted_key_material is empty, generate it (this handles the case where + // an old key was created without proper key material) + if existing_key_data.encrypted_key_material.is_empty() { + warn!("Key {} has empty encrypted_key_material, generating new key material", key_id); + let key_material = generate_key_material(&existing_key_data.algorithm)?; + existing_key_data.encrypted_key_material = self.encrypt_key_material(&key_material).await?; + } + + // Update only the metadata fields, preserving the encrypted_key_material let key_data = VaultKeyData { - algorithm: "AES_256".to_string(), + algorithm: existing_key_data.algorithm.clone(), usage: request.key_usage.clone(), - created_at: Zoned::now(), - status: KeyStatus::Active, - version: 1, + created_at: existing_key_data.created_at, + status: existing_key_data.status, + version: existing_key_data.version, description: request.description.clone(), - metadata: HashMap::new(), + metadata: existing_key_data.metadata.clone(), tags: request.tags.clone(), - encrypted_key_material: String::new(), // Not used for transit keys + encrypted_key_material: existing_key_data.encrypted_key_material.clone(), // Preserve the key material }; - debug!("VaultKeyData tags before storage: {:?}", key_data.tags); + debug!( + "VaultKeyData tags before storage: {:?}, encrypted_key_material length: {}", + key_data.tags, + key_data.encrypted_key_material.len() + ); self.store_key_data(key_id, &key_data).await } @@ -225,36 +291,33 @@ impl VaultKmsClient { #[async_trait] impl KmsClient for VaultKmsClient { - async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result { + async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result { debug!("Generating data key for master key: {}", request.master_key_id); - // Verify master key exists - let _master_key = self.describe_key(&request.master_key_id, context).await?; - - // Generate data key material - let key_length = match request.key_spec.as_str() { - "AES_256" => 32, - "AES_128" => 16, - _ => return Err(KmsError::unsupported_algorithm(&request.key_spec)), - }; - - let mut plaintext_key = vec![0u8; key_length]; - rand::rng().fill_bytes(&mut plaintext_key); + // Generate random data key material using the existing method + let plaintext_key = generate_key_material(&request.key_spec)?; // Encrypt the data key with the master key - let encrypted_key = self.encrypt_key_material(&plaintext_key).await?; + let (encrypted_key, nonce) = self.encrypt_with_master_key(&request.master_key_id, &plaintext_key).await?; - Ok(DataKey { - key_id: request.master_key_id.clone(), - version: 1, - plaintext: Some(plaintext_key), - ciphertext: general_purpose::STANDARD - .decode(&encrypted_key) - .map_err(|e| KmsError::cryptographic_error("decode", e.to_string()))?, + // Create data key envelope with master key version for rotation support + let envelope = DataKeyEnvelope { + key_id: uuid::Uuid::new_v4().to_string(), + master_key_id: request.master_key_id.clone(), key_spec: request.key_spec.clone(), - metadata: request.encryption_context.clone(), + encrypted_key: encrypted_key.clone(), + nonce, + encryption_context: request.encryption_context.clone(), created_at: Zoned::now(), - }) + }; + + // Serialize the envelope as the ciphertext + let ciphertext = serde_json::to_vec(&envelope)?; + + let data_key = DataKeyInfo::new(envelope.key_id, 1, Some(plaintext_key), ciphertext, request.key_spec.clone()); + + info!("Generated data key for master key: {}", request.master_key_id); + Ok(data_key) } async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result { @@ -279,15 +342,42 @@ impl KmsClient for VaultKmsClient { }) } - async fn decrypt(&self, _request: &DecryptRequest, _context: Option<&OperationContext>) -> Result> { + async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result> { debug!("Decrypting data"); - // For this simple implementation, we assume the key ID is embedded in the ciphertext metadata - // In practice, you'd extract this from the ciphertext envelope - Err(KmsError::invalid_operation("Decrypt not fully implemented for Vault backend")) + // Parse the data key envelope from ciphertext + let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) + .map_err(|e| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {e}")))?; + + // Verify encryption context matches + // Check that all keys in envelope.encryption_context are present in request.encryption_context + // and their values match. This ensures the context used for decryption matches what was used for encryption. + for (key, expected_value) in &envelope.encryption_context { + if let Some(actual_value) = request.encryption_context.get(key) { + if actual_value != expected_value { + return Err(KmsError::context_mismatch(format!( + "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" + ))); + } + } else { + // If request.encryption_context is empty, allow decryption (backward compatibility) + // Otherwise, require all envelope context keys to be present + if !request.encryption_context.is_empty() { + return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))); + } + } + } + + // Decrypt the data key + let plaintext = self + .decrypt_with_master_key(&envelope.master_key_id, &envelope.encrypted_key, &envelope.nonce) + .await?; + + info!("Successfully decrypted data"); + Ok(plaintext) } - async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result { + async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result { debug!("Creating master key: {} with algorithm: {}", key_id, algorithm); // Check if key already exists @@ -296,7 +386,7 @@ impl KmsClient for VaultKmsClient { } // Generate key material - let key_material = Self::generate_key_material(algorithm)?; + let key_material = generate_key_material(algorithm)?; let encrypted_material = self.encrypt_key_material(&key_material).await?; // Create key data @@ -315,7 +405,7 @@ impl KmsClient for VaultKmsClient { // Store in Vault self.store_key_data(key_id, &key_data).await?; - let master_key = MasterKey { + let master_key = MasterKeyInfo { key_id: key_id.to_string(), version: key_data.version, algorithm: key_data.algorithm.clone(), @@ -438,19 +528,19 @@ impl KmsClient for VaultKmsClient { Ok(()) } - async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { + async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { debug!("Rotating key: {}", key_id); let mut key_data = self.get_key_data(key_id).await?; key_data.version += 1; // Generate new key material - let key_material = Self::generate_key_material(&key_data.algorithm)?; + let key_material = generate_key_material(&key_data.algorithm)?; key_data.encrypted_key_material = self.encrypt_key_material(&key_material).await?; self.store_key_data(key_id, &key_data).await?; - let master_key = MasterKey { + let master_key = MasterKeyInfo { key_id: key_id.to_string(), version: key_data.version, algorithm: key_data.algorithm, @@ -506,7 +596,7 @@ impl VaultKmsBackend { /// Create a new VaultKmsBackend pub async fn new(config: KmsConfig) -> Result { let vault_config = match &config.backend_config { - crate::config::BackendConfig::Vault(vault_config) => vault_config.clone(), + crate::config::BackendConfig::Vault(vault_config) => (**vault_config).clone(), _ => return Err(KmsError::configuration_error("Expected Vault backend configuration")), }; @@ -681,7 +771,7 @@ impl KmsBackend for VaultKmsBackend { )); } - let deletion_date = Zoned::now() + jiff::Span::new().days(days as i64); + let deletion_date = Zoned::now() + Duration::from_secs(days as u64 * 86400); key_metadata.key_state = KeyState::PendingDeletion; key_metadata.deletion_date = Some(deletion_date.clone()); diff --git a/crates/kms/src/config.rs b/crates/kms/src/config.rs index c963074b..c7b28416 100644 --- a/crates/kms/src/config.rs +++ b/crates/kms/src/config.rs @@ -69,7 +69,7 @@ pub enum BackendConfig { /// Local backend configuration Local(LocalConfig), /// Vault backend configuration - Vault(VaultConfig), + Vault(Box), } impl Default for BackendConfig { @@ -194,11 +194,11 @@ impl KmsConfig { pub fn vault(address: Url, token: String) -> Self { Self { backend: KmsBackend::Vault, - backend_config: BackendConfig::Vault(VaultConfig { + backend_config: BackendConfig::Vault(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::Token { token }, ..Default::default() - }), + })), ..Default::default() } } @@ -207,11 +207,11 @@ impl KmsConfig { pub fn vault_approle(address: Url, role_id: String, secret_id: String) -> Self { Self { backend: KmsBackend::Vault, - backend_config: BackendConfig::Vault(VaultConfig { + backend_config: BackendConfig::Vault(Box::new(VaultConfig { address: address.to_string(), auth_method: VaultAuthMethod::AppRole { role_id, secret_id }, ..Default::default() - }), + })), ..Default::default() } } @@ -353,7 +353,7 @@ impl KmsConfig { let address = std::env::var("RUSTFS_KMS_VAULT_ADDRESS").unwrap_or_else(|_| "http://localhost:8200".to_string()); let token = std::env::var("RUSTFS_KMS_VAULT_TOKEN").unwrap_or_else(|_| "dev-token".to_string()); - config.backend_config = BackendConfig::Vault(VaultConfig { + config.backend_config = BackendConfig::Vault(Box::new(VaultConfig { address, auth_method: VaultAuthMethod::Token { token }, namespace: std::env::var("RUSTFS_KMS_VAULT_NAMESPACE").ok(), @@ -362,7 +362,7 @@ impl KmsConfig { key_path_prefix: std::env::var("RUSTFS_KMS_VAULT_KEY_PREFIX") .unwrap_or_else(|_| "rustfs/kms/keys".to_string()), tls: None, - }); + })); } } diff --git a/crates/kms/src/encryption/dek.rs b/crates/kms/src/encryption/dek.rs new file mode 100644 index 00000000..d8725bf3 --- /dev/null +++ b/crates/kms/src/encryption/dek.rs @@ -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, + pub nonce: Vec, + pub encryption_context: HashMap, + 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, Vec)>; + + /// 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>; + + /// 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, Vec)> { + 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::::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> { + 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::::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> { + 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"); + } +} diff --git a/crates/kms/src/encryption/mod.rs b/crates/kms/src/encryption/mod.rs index b2dd155a..3c1ee871 100644 --- a/crates/kms/src/encryption/mod.rs +++ b/crates/kms/src/encryption/mod.rs @@ -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}; diff --git a/crates/kms/src/lib.rs b/crates/kms/src/lib.rs index 7299766b..b882c35f 100644 --- a/crates/kms/src/lib.rs +++ b/crates/kms/src/lib.rs @@ -63,6 +63,7 @@ pub mod config; mod encryption; mod error; pub mod manager; +pub mod service; pub mod service_manager; pub mod types; @@ -73,10 +74,9 @@ pub use api_types::{ UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse, }; pub use config::*; -pub use encryption::ObjectEncryptionService; -pub use encryption::service::DataKey; pub use error::{KmsError, Result}; pub use manager::KmsManager; +pub use service::{DataKey, ObjectEncryptionService}; pub use service_manager::{ KmsServiceManager, KmsServiceStatus, get_global_encryption_service, get_global_kms_service_manager, init_global_kms_service_manager, @@ -112,6 +112,7 @@ pub fn shutdown_global_services() { #[cfg(test)] mod tests { use super::*; + use std::sync::Arc; use tempfile::TempDir; #[tokio::test] @@ -139,4 +140,91 @@ mod tests { // Test stop manager.stop().await.expect("Stop should succeed"); } + + #[tokio::test] + async fn test_versioned_service_reconfiguration() { + // Test versioned service reconfiguration for zero-downtime + let manager = KmsServiceManager::new(); + + // Initial state: no version + assert!(manager.get_service_version().await.is_none()); + + // Start first service + let temp_dir1 = TempDir::new().expect("Failed to create temp dir"); + let config1 = KmsConfig::local(temp_dir1.path().to_path_buf()); + manager + .configure(config1.clone()) + .await + .expect("Configuration should succeed"); + manager.start().await.expect("Start should succeed"); + + // Verify version 1 + let version1 = manager.get_service_version().await.expect("Service should have version"); + assert_eq!(version1, 1); + + // Get service reference (simulating ongoing operation) + let service1 = manager.get_encryption_service().await.expect("Service should be available"); + + // Reconfigure to new service (zero-downtime) + let temp_dir2 = TempDir::new().expect("Failed to create temp dir"); + let config2 = KmsConfig::local(temp_dir2.path().to_path_buf()); + manager.reconfigure(config2).await.expect("Reconfiguration should succeed"); + + // Verify version 2 + let version2 = manager.get_service_version().await.expect("Service should have version"); + assert_eq!(version2, 2); + + // Old service reference should still be valid (Arc keeps it alive) + // New requests should get version 2 + let service2 = manager.get_encryption_service().await.expect("Service should be available"); + + // Verify they are different instances + assert!(!Arc::ptr_eq(&service1, &service2)); + + // Old service should still work (simulating long-running operation) + // This demonstrates zero-downtime: old operations continue, new operations use new service + assert!(service1.health_check().await.is_ok()); + assert!(service2.health_check().await.is_ok()); + } + + #[tokio::test] + async fn test_concurrent_reconfiguration() { + // Test that concurrent reconfiguration requests are serialized + let manager = Arc::new(KmsServiceManager::new()); + + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path().to_path_buf(); + + // Initial configuration + let config1 = KmsConfig::local(base_path.clone()); + manager.configure(config1).await.expect("Configuration should succeed"); + manager.start().await.expect("Start should succeed"); + + // Spawn multiple concurrent reconfiguration requests + let mut handles = Vec::new(); + for _i in 0..5 { + let manager_clone = manager.clone(); + let path = base_path.clone(); + let handle = tokio::spawn(async move { + let config = KmsConfig::local(path); + manager_clone.reconfigure(config).await + }); + handles.push(handle); + } + + // Wait for all reconfigurations to complete + let mut results = Vec::new(); + for handle in handles { + results.push(handle.await); + } + + // All should succeed (serialized by mutex) + for result in results { + assert!(result.expect("Task should complete").is_ok()); + } + + // Final version should be 6 (1 initial + 5 reconfigurations) + let final_version = manager.get_service_version().await.expect("Service should have version"); + assert_eq!(final_version, 6); + } } diff --git a/crates/kms/src/encryption/service.rs b/crates/kms/src/service.rs similarity index 99% rename from crates/kms/src/encryption/service.rs rename to crates/kms/src/service.rs index ef361bc1..2a6ef720 100644 --- a/crates/kms/src/encryption/service.rs +++ b/crates/kms/src/service.rs @@ -274,6 +274,8 @@ impl ObjectEncryptionService { // Build encryption context let mut context = encryption_context.cloned().unwrap_or_default(); context.insert("bucket".to_string(), bucket.to_string()); + context.insert("object_key".to_string(), object_key.to_string()); + // Backward compatibility: also include legacy "object" context key context.insert("object".to_string(), object_key.to_string()); context.insert("algorithm".to_string(), algorithm.as_str().to_string()); diff --git a/crates/kms/src/service_manager.rs b/crates/kms/src/service_manager.rs index c5d40fce..f9cb2f67 100644 --- a/crates/kms/src/service_manager.rs +++ b/crates/kms/src/service_manager.rs @@ -16,11 +16,15 @@ use crate::backends::{KmsBackend, local::LocalKmsBackend}; use crate::config::{BackendConfig, KmsConfig}; -use crate::encryption::service::ObjectEncryptionService; use crate::error::{KmsError, Result}; use crate::manager::KmsManager; -use std::sync::{Arc, OnceLock}; -use tokio::sync::RwLock; +use crate::service::ObjectEncryptionService; +use arc_swap::ArcSwap; +use std::sync::{ + Arc, OnceLock, + atomic::{AtomicU64, Ordering}, +}; +use tokio::sync::{Mutex, RwLock}; use tracing::{error, info, warn}; /// KMS service status @@ -36,26 +40,43 @@ pub enum KmsServiceStatus { Error(String), } -/// Dynamic KMS service manager +/// Service version information for zero-downtime reconfiguration +#[derive(Clone)] +struct ServiceVersion { + /// Service version number (monotonically increasing) + version: u64, + /// The encryption service instance + service: Arc, + /// The KMS manager instance + manager: Arc, +} + +/// Dynamic KMS service manager with versioned services for zero-downtime reconfiguration pub struct KmsServiceManager { - /// Current KMS manager (if running) - manager: Arc>>>, - /// Current encryption service (if running) - encryption_service: Arc>>>, + /// Current service version (if running) + /// Uses ArcSwap for atomic, lock-free service switching + /// This allows instant atomic updates without blocking readers + current_service: ArcSwap>, /// Current configuration config: Arc>>, /// Current status status: Arc>, + /// Version counter (monotonically increasing) + version_counter: Arc, + /// Mutex to protect lifecycle operations (start, stop, reconfigure) + /// This ensures only one lifecycle operation happens at a time + lifecycle_mutex: Arc>, } impl KmsServiceManager { /// Create a new KMS service manager (not configured) pub fn new() -> Self { Self { - manager: Arc::new(RwLock::new(None)), - encryption_service: Arc::new(RwLock::new(None)), + current_service: ArcSwap::from_pointee(None), config: Arc::new(RwLock::new(None)), status: Arc::new(RwLock::new(KmsServiceStatus::NotConfigured)), + version_counter: Arc::new(AtomicU64::new(0)), + lifecycle_mutex: Arc::new(Mutex::new(())), } } @@ -89,6 +110,12 @@ impl KmsServiceManager { /// Start KMS service with current configuration pub async fn start(&self) -> Result<()> { + let _guard = self.lifecycle_mutex.lock().await; + self.start_internal().await + } + + /// Internal start implementation (called within lifecycle mutex) + async fn start_internal(&self) -> Result<()> { let config = { let config_guard = self.config.read().await; match config_guard.as_ref() { @@ -105,23 +132,11 @@ impl KmsServiceManager { info!("Starting KMS service with backend: {:?}", config.backend); - match self.create_backend(&config).await { - Ok(backend) => { - // Create KMS manager - let kms_manager = Arc::new(KmsManager::new(backend, config)); - - // Create encryption service - let encryption_service = Arc::new(ObjectEncryptionService::new((*kms_manager).clone())); - - // Update manager and service - { - let mut manager = self.manager.write().await; - *manager = Some(kms_manager); - } - { - let mut service = self.encryption_service.write().await; - *service = Some(encryption_service); - } + match self.create_service_version(&config).await { + Ok(service_version) => { + // Atomically update to new service version (lock-free, instant) + // ArcSwap::store() is a true atomic operation using CAS + self.current_service.store(Arc::new(Some(service_version))); // Update status { @@ -143,18 +158,21 @@ impl KmsServiceManager { } /// Stop KMS service + /// + /// Note: This stops accepting new operations, but existing operations using + /// the service will continue until they complete (due to Arc reference counting). pub async fn stop(&self) -> Result<()> { + let _guard = self.lifecycle_mutex.lock().await; + self.stop_internal().await + } + + /// Internal stop implementation (called within lifecycle mutex) + async fn stop_internal(&self) -> Result<()> { info!("Stopping KMS service"); - // Clear manager and service - { - let mut manager = self.manager.write().await; - *manager = None; - } - { - let mut service = self.encryption_service.write().await; - *service = None; - } + // Atomically clear current service version (lock-free, instant) + // Note: Existing Arc references will keep the service alive until operations complete + self.current_service.store(Arc::new(None)); // Update status (keep configuration) { @@ -164,37 +182,96 @@ impl KmsServiceManager { } } - info!("KMS service stopped successfully"); + info!("KMS service stopped successfully (existing operations may continue)"); Ok(()) } - /// Reconfigure and restart KMS service + /// Reconfigure and restart KMS service with zero-downtime + /// + /// This method implements versioned service switching: + /// 1. Creates a new service version without stopping the old one + /// 2. Atomically switches to the new version + /// 3. Old operations continue using the old service (via Arc reference counting) + /// 4. New operations automatically use the new service + /// + /// This ensures zero downtime during reconfiguration, even for long-running + /// operations like encrypting large files. pub async fn reconfigure(&self, new_config: KmsConfig) -> Result<()> { - info!("Reconfiguring KMS service"); + let _guard = self.lifecycle_mutex.lock().await; - // Stop current service if running - if matches!(self.get_status().await, KmsServiceStatus::Running) { - self.stop().await?; - } + info!("Reconfiguring KMS service (zero-downtime)"); // Configure with new config - self.configure(new_config).await?; + { + let mut config = self.config.write().await; + *config = Some(new_config.clone()); + } - // Start with new configuration - self.start().await?; + // Create new service version without stopping old one + // This allows existing operations to continue while new operations use new service + match self.create_service_version(&new_config).await { + Ok(new_service_version) => { + // Get old version for logging (lock-free read) + let old_version = self.current_service.load().as_ref().as_ref().map(|sv| sv.version); - info!("KMS service reconfigured successfully"); - Ok(()) + // Atomically switch to new service version (lock-free, instant CAS operation) + // This is a true atomic operation - no waiting for locks, instant switch + // Old service will be dropped when no more Arc references exist + self.current_service.store(Arc::new(Some(new_service_version.clone()))); + + // Update status + { + let mut status = self.status.write().await; + *status = KmsServiceStatus::Running; + } + + if let Some(old_ver) = old_version { + info!( + "KMS service reconfigured successfully: version {} -> {} (old service will be cleaned up when operations complete)", + old_ver, new_service_version.version + ); + } else { + info!( + "KMS service reconfigured successfully: version {} (service started)", + new_service_version.version + ); + } + Ok(()) + } + Err(e) => { + let err_msg = format!("Failed to reconfigure KMS: {e}"); + error!("{}", err_msg); + let mut status = self.status.write().await; + *status = KmsServiceStatus::Error(err_msg.clone()); + Err(KmsError::backend_error(&err_msg)) + } + } } /// Get KMS manager (if running) + /// + /// Returns the manager from the current service version. + /// Uses lock-free atomic load for optimal performance. pub async fn get_manager(&self) -> Option> { - self.manager.read().await.clone() + self.current_service.load().as_ref().as_ref().map(|sv| sv.manager.clone()) } - /// Get encryption service (if running) + /// Get encryption service (if running) + /// + /// Returns the service from the current service version. + /// Uses lock-free atomic load - no blocking, instant access. + /// This ensures new operations always use the latest service version, + /// while existing operations continue using their Arc references. pub async fn get_encryption_service(&self) -> Option> { - self.encryption_service.read().await.clone() + self.current_service.load().as_ref().as_ref().map(|sv| sv.service.clone()) + } + + /// Get current service version number + /// + /// Useful for monitoring and debugging. + /// Uses lock-free atomic load. + pub async fn get_service_version(&self) -> Option { + self.current_service.load().as_ref().as_ref().map(|sv| sv.version) } /// Health check for the KMS service @@ -226,20 +303,40 @@ impl KmsServiceManager { } } - /// Create backend from configuration - async fn create_backend(&self, config: &KmsConfig) -> Result> { - match &config.backend_config { + /// Create a new service version from configuration + /// + /// This creates a new backend, manager, and service, and assigns it a new version number. + async fn create_service_version(&self, config: &KmsConfig) -> Result { + // Increment version counter + let version = self.version_counter.fetch_add(1, Ordering::Relaxed) + 1; + + info!("Creating KMS service version {} with backend: {:?}", version, config.backend); + + // Create backend + let backend = match &config.backend_config { BackendConfig::Local(_) => { - info!("Creating Local KMS backend"); + info!("Creating Local KMS backend for version {}", version); let backend = LocalKmsBackend::new(config.clone()).await?; - Ok(Arc::new(backend)) + Arc::new(backend) as Arc } BackendConfig::Vault(_) => { - info!("Creating Vault KMS backend"); + info!("Creating Vault KMS backend for version {}", version); let backend = crate::backends::vault::VaultKmsBackend::new(config.clone()).await?; - Ok(Arc::new(backend)) + Arc::new(backend) as Arc } - } + }; + + // Create KMS manager + let kms_manager = Arc::new(KmsManager::new(backend, config.clone())); + + // Create encryption service + let encryption_service = Arc::new(ObjectEncryptionService::new((*kms_manager).clone())); + + Ok(ServiceVersion { + version, + service: encryption_service, + manager: kms_manager, + }) } } diff --git a/crates/kms/src/types.rs b/crates/kms/src/types.rs index d04c4b0b..63e548ed 100644 --- a/crates/kms/src/types.rs +++ b/crates/kms/src/types.rs @@ -22,7 +22,7 @@ use zeroize::Zeroize; /// Data encryption key (DEK) used for encrypting object data #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DataKey { +pub struct DataKeyInfo { /// Key identifier pub key_id: String, /// Key version @@ -40,7 +40,7 @@ pub struct DataKey { pub created_at: Zoned, } -impl DataKey { +impl DataKeyInfo { /// Create a new data key /// /// # Arguments @@ -96,7 +96,7 @@ impl DataKey { /// Master key stored in KMS backend #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MasterKey { +pub struct MasterKeyInfo { /// Unique key identifier pub key_id: String, /// Key version @@ -119,7 +119,7 @@ pub struct MasterKey { pub created_by: Option, } -impl MasterKey { +impl MasterKeyInfo { /// Create a new master key /// /// # Arguments @@ -226,8 +226,8 @@ pub struct KeyInfo { pub created_by: Option, } -impl From for KeyInfo { - fn from(master_key: MasterKey) -> Self { +impl From for KeyInfo { + fn from(master_key: MasterKeyInfo) -> Self { Self { key_id: master_key.key_id, description: master_key.description, @@ -913,7 +913,7 @@ pub struct CancelKeyDeletionResponse { } // SECURITY: Implement Drop to automatically zero sensitive data when DataKey is dropped -impl Drop for DataKey { +impl Drop for DataKeyInfo { fn drop(&mut self) { self.clear_plaintext(); } diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index b133e761..18761d63 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -126,6 +126,8 @@ urlencoding = { workspace = true } uuid = { workspace = true } zip = { workspace = true } libc = { workspace = true } +rand = { workspace = true } +aes-gcm = { workspace = true } # Observability and Metrics metrics = { workspace = true } diff --git a/rustfs/src/init.rs b/rustfs/src/init.rs index 35cc2045..1c7c284e 100644 --- a/rustfs/src/init.rs +++ b/rustfs/src/init.rs @@ -209,7 +209,7 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { rustfs_kms::config::KmsConfig { backend: rustfs_kms::config::KmsBackend::Vault, - backend_config: rustfs_kms::config::BackendConfig::Vault(rustfs_kms::config::VaultConfig { + backend_config: rustfs_kms::config::BackendConfig::Vault(Box::new(rustfs_kms::config::VaultConfig { address: vault_address.clone(), auth_method: rustfs_kms::config::VaultAuthMethod::Token { token: vault_token.clone(), @@ -219,7 +219,7 @@ pub(crate) async fn init_kms_system(opt: &config::Opt) -> std::io::Result<()> { kv_mount: "secret".to_string(), key_path_prefix: "rustfs/kms/keys".to_string(), tls: None, - }), + })), default_key_id: opt.kms_default_key_id.clone(), timeout: std::time::Duration::from_secs(30), retry_attempts: 3, diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index 123ce57f..1acb436f 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -22,6 +22,10 @@ use crate::storage::concurrency::{ use crate::storage::head_prefix::{head_prefix_not_found_message, probe_prefix_has_children}; use crate::storage::helper::OperationHelper; use crate::storage::options::{filter_object_metadata, get_content_sha256}; +use crate::storage::sse::{ + DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, check_encryption_metadata, sse_decryption, sse_encryption, + sse_prepare_encryption, strip_managed_encryption_metadata, +}; use crate::storage::{ access::{ReqInfo, authorize_request, has_bypass_governance_header}, ecfs_extend::RFC1123, @@ -31,15 +35,13 @@ use crate::storage::{ }, }; use crate::storage::{ - apply_lock_retention, check_preconditions, create_managed_encryption_material, decrypt_managed_encryption_key, - decrypt_multipart_managed_stream, derive_part_nonce, get_buffer_size_opt_in, get_validated_store, has_replication_rules, - is_managed_sse, parse_object_lock_legal_hold, parse_object_lock_retention, process_lambda_configurations, - process_queue_configurations, process_topic_configurations, strip_managed_encryption_metadata, - validate_bucket_object_lock_enabled, validate_list_object_unordered_with_delimiter, validate_object_key, - wrap_response_with_cors, + apply_lock_retention, check_preconditions, get_buffer_size_opt_in, get_validated_store, has_replication_rules, + parse_object_lock_legal_hold, parse_object_lock_retention, process_lambda_configurations, process_queue_configurations, + process_topic_configurations, validate_bucket_object_lock_enabled, validate_list_object_unordered_with_delimiter, + validate_object_key, wrap_response_with_cors, }; use crate::storage::{entity, parse_part_number_i32_to_usize}; -use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD}; +// base64 imports moved to sse module use bytes::Bytes; use datafusion::arrow::{ csv::WriterBuilder as CsvWriterBuilder, json::WriterBuilder as JsonWriterBuilder, json::writer::JsonArray, @@ -97,13 +99,13 @@ use rustfs_ecstore::{ use rustfs_filemeta::REPLICATE_INCOMING_DELETE; use rustfs_filemeta::{ReplicationStatusType, ReplicationType, VersionPurgeStatusType}; use rustfs_filemeta::{RestoreStatusOps, parse_restore_obj_status}; -use rustfs_kms::DataKey; +// KMS imports moved to sse module use rustfs_notify::{EventArgsBuilder, notifier_global}; use rustfs_policy::policy::{ action::{Action, S3Action}, {BucketPolicy, BucketPolicyArgs, Validator}, }; -use rustfs_rio::{CompressReader, DecryptReader, EncryptReader, EtagReader, HardLimitReader, HashReader, Reader, WarpReader}; +use rustfs_rio::{CompressReader, EtagReader, HashReader, Reader, WarpReader}; use rustfs_s3select_api::{ object_store::bytes_stream, query::{Context, Query}, @@ -174,12 +176,6 @@ pub struct FS { // pub store: ECStore, } -pub(crate) struct ManagedEncryptionMaterial { - pub(crate) data_key: DataKey, - pub(crate) headers: HashMap, - pub(crate) kms_key_id: String, -} - #[derive(Debug, Default, serde::Deserialize)] pub(crate) struct ListObjectUnorderedQuery { #[serde(rename = "allow-unordered")] @@ -713,11 +709,6 @@ impl S3 for FS { .await; }); - info!( - "TDD: Creating output with SSE: {:?}, KMS Key: {:?}", - server_side_encryption, ssekms_key_id - ); - let mut checksum_crc32 = input.checksum_crc32; let mut checksum_crc32c = input.checksum_crc32c; let mut checksum_sha1 = input.checksum_sha1; @@ -883,30 +874,6 @@ impl S3 for FS { return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); }; - let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok(); - let effective_sse = requested_sse.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _)| { - config.rules.first().and_then(|rule| { - rule.apply_server_side_encryption_by_default - .as_ref() - .and_then(|sse| match sse.sse_algorithm.as_str() { - "AES256" => Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)), - "aws:kms" => Some(ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS)), - _ => None, - }) - }) - }) - }); - let mut effective_kms_key_id = requested_kms_key_id.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _)| { - config.rules.first().and_then(|rule| { - rule.apply_server_side_encryption_by_default - .as_ref() - .and_then(|sse| sse.kms_master_key_id.clone()) - }) - }) - }); - let h = HeaderMap::new(); let gr = store @@ -946,11 +913,23 @@ impl S3 for FS { let mut reader: Box = Box::new(WarpReader::new(gr.stream)); - if let Some((key_bytes, nonce, original_size_opt)) = - decrypt_managed_encryption_key(&src_bucket, &src_key, &src_info.user_defined).await? - { - reader = Box::new(DecryptReader::new(reader, key_bytes, nonce)); - if let Some(original) = original_size_opt { + // Apply unified SSE decryption for source object if encrypted + // Note: SSE-C for copy source is handled via copy_source_sse_customer_* headers + let copy_source_sse_customer_key = req.input.copy_source_sse_customer_key.as_ref(); + let copy_source_sse_customer_key_md5 = req.input.copy_source_sse_customer_key_md5.as_ref(); + let decryption_request = DecryptionRequest { + bucket: &src_bucket, + key: &src_key, + metadata: &src_info.user_defined, + sse_customer_key: copy_source_sse_customer_key, + sse_customer_key_md5: copy_source_sse_customer_key_md5, + part_number: None, + parts: &src_info.parts, + }; + + if let Some(material) = sse_decryption(decryption_request).await? { + reader = material.wrap_single_reader(reader); + if let Some(original) = material.original_size { src_info.actual_size = original; } } @@ -1013,64 +992,38 @@ impl S3 for FS { let mut reader = HashReader::new(reader, length, actual_size, None, None, false).map_err(ApiError::from)?; - if let Some(ref sse_alg) = effective_sse - && is_managed_sse(sse_alg) - { - let material = - create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size).await?; + // Apply unified SSE encryption for destination object + let encryption_request = EncryptionRequest { + bucket: &bucket, + key: &key, + server_side_encryption: requested_sse, + ssekms_key_id: requested_kms_key_id, + sse_customer_algorithm: sse_customer_algorithm.clone(), + sse_customer_key, + sse_customer_key_md5: sse_customer_key_md5.clone(), + content_size: actual_size, + part_number: None, + part_key: None, + part_nonce: None, + }; - let ManagedEncryptionMaterial { - data_key, - headers, - kms_key_id: kms_key_used, - } = material; + let (requested_sse, requested_kms_key_id) = match sse_encryption(encryption_request).await? { + Some(material) => { + let requested_sse = Some(material.server_side_encryption.clone()); + let requested_kms_key_id = material.kms_key_id.clone(); - let key_bytes = data_key.plaintext_key; - let nonce = data_key.nonce; + // Apply encryption wrapper + let encrypted_reader = material.wrap_reader(reader); + reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false) + .map_err(ApiError::from)?; - src_info.user_defined.extend(headers.into_iter()); - effective_kms_key_id = Some(kms_key_used.clone()); + // Merge encryption metadata + src_info.user_defined.extend(material.metadata); - let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce); - reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?; - } - - // Apply SSE-C encryption if customer-provided key is specified - if let (Some(sse_alg), Some(sse_key), Some(sse_md5)) = (&sse_customer_algorithm, &sse_customer_key, &sse_customer_key_md5) - && sse_alg.as_str() == "AES256" - { - let key_bytes = BASE64_STANDARD.decode(sse_key.as_str()).map_err(|e| { - error!("Failed to decode SSE-C key: {}", e); - ApiError::from(StorageError::other("Invalid SSE-C key")) - })?; - - if key_bytes.len() != 32 { - return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into()); + (requested_sse, requested_kms_key_id) } - - let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0); - if computed_md5 != sse_md5.as_str() { - return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into()); - } - - // Store original size before encryption - src_info - .user_defined - .insert("x-amz-server-side-encryption-customer-original-size".to_string(), actual_size.to_string()); - - // SAFETY: The length of `key_bytes` is checked to be 32 bytes above, - // so this conversion cannot fail. - let key_array: [u8; 32] = key_bytes.try_into().expect("key length already checked"); - // Generate deterministic nonce from bucket-key - let nonce_source = format!("{bucket}-{key}"); - let nonce_hash = md5::compute(nonce_source.as_bytes()); - let nonce: [u8; 12] = nonce_hash.0[..12] - .try_into() - .expect("MD5 hash is always 16 bytes; taking first 12 bytes for nonce is safe"); - - let encrypt_reader = EncryptReader::new(reader, key_array, nonce); - reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?; - } + None => (None, None), + }; src_info.put_object_reader = Some(PutObjReader::new(reader)); @@ -1080,19 +1033,6 @@ impl S3 for FS { src_info.user_defined.insert(k, v); } - // Store SSE-C metadata for GET responses - if let Some(ref sse_alg) = sse_customer_algorithm { - src_info.user_defined.insert( - "x-amz-server-side-encryption-customer-algorithm".to_string(), - sse_alg.as_str().to_string(), - ); - } - if let Some(ref sse_md5) = sse_customer_key_md5 { - src_info - .user_defined - .insert("x-amz-server-side-encryption-customer-key-md5".to_string(), sse_md5.clone()); - } - // check quota for copy operation if let Some(metadata_sys) = rustfs_ecstore::bucket::metadata_sys::GLOBAL_BucketMetadataSys.get() { let quota_checker = QuotaChecker::new(metadata_sys.clone()); @@ -1151,8 +1091,8 @@ impl S3 for FS { let output = CopyObjectOutput { copy_object_result: Some(copy_object_result), - server_side_encryption: effective_sse, - ssekms_key_id: effective_kms_key_id, + server_side_encryption: requested_sse, + ssekms_key_id: requested_kms_key_id, sse_customer_algorithm, sse_customer_key_md5, version_id: dest_version, @@ -1245,72 +1185,29 @@ impl S3 for FS { metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags); } - // TDD: Get bucket SSE configuration for multipart upload - let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok(); - debug!("TDD: Got bucket SSE config for multipart: {:?}", bucket_sse_config); + // Prepare SSE configuration for multipart upload + // Apply encryption using unified SSE API + let encryption_request = PrepareEncryptionRequest { + bucket: &bucket, + key: &key, + server_side_encryption, + ssekms_key_id, + sse_customer_algorithm: sse_customer_algorithm.clone(), + sse_customer_key_md5: sse_customer_key_md5.clone(), + }; - // TDD: Determine effective encryption (request parameters override bucket defaults) - let original_sse = server_side_encryption.clone(); - let effective_sse = server_side_encryption.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _timestamp)| { - debug!("TDD: Processing bucket SSE config for multipart: {:?}", config); - config.rules.first().and_then(|rule| { - debug!("TDD: Processing SSE rule for multipart: {:?}", rule); - rule.apply_server_side_encryption_by_default.as_ref().map(|sse| { - debug!("TDD: Found SSE default for multipart: {:?}", sse); - match sse.sse_algorithm.as_str() { - "AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256), - "aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS), - _ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), // fallback to AES256 - } - }) - }) - }) - }); - debug!("TDD: effective_sse for multipart={:?} (original={:?})", effective_sse, original_sse); + let (effective_sse, effective_kms_key_id) = match sse_prepare_encryption(encryption_request).await? { + Some(material) => { + let server_side_encryption = Some(material.server_side_encryption.clone()); + let ssekms_key_id = material.kms_key_id.clone(); - let _original_kms_key_id = ssekms_key_id.clone(); - let mut effective_kms_key_id = ssekms_key_id.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _timestamp)| { - config.rules.first().and_then(|rule| { - rule.apply_server_side_encryption_by_default - .as_ref() - .and_then(|sse| sse.kms_master_key_id.clone()) - }) - }) - }); + // Merge encryption metadata + metadata.extend(material.metadata); - // Store effective SSE information in metadata for multipart upload - if let Some(sse_alg) = &sse_customer_algorithm { - metadata.insert( - "x-amz-server-side-encryption-customer-algorithm".to_string(), - sse_alg.as_str().to_string(), - ); - } - if let Some(sse_md5) = &sse_customer_key_md5 { - metadata.insert("x-amz-server-side-encryption-customer-key-md5".to_string(), sse_md5.clone()); - } - - if let Some(sse) = &effective_sse { - if is_managed_sse(sse) { - let material = create_managed_encryption_material(&bucket, &key, sse, effective_kms_key_id.clone(), 0).await?; - - let ManagedEncryptionMaterial { - data_key: _, - headers, - kms_key_id: kms_key_used, - } = material; - - metadata.extend(headers.into_iter()); - effective_kms_key_id = Some(kms_key_used.clone()); - } else { - metadata.insert("x-amz-server-side-encryption".to_string(), sse.as_str().to_string()); + (server_side_encryption, ssekms_key_id) } - } - - if let Some(kms_key_id) = &effective_kms_key_id { - metadata.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), kms_key_id.clone()); - } + None => (None, None), + }; if is_compressible(&req.headers, &key) { metadata.insert( @@ -2717,151 +2614,52 @@ impl S3 for FS { None }; - // Apply SSE-C decryption if customer provided key and object was encrypted with SSE-C + // ============================================ + // Apply Unified SSE Decryption + // ============================================ + // Apply decryption if object is encrypted + + // Apply unified SSE decryption using apply_decryption API let mut final_stream = reader.stream; - let stored_sse_algorithm = info.user_defined.get("x-amz-server-side-encryption-customer-algorithm"); - let stored_sse_key_md5 = info.user_defined.get("x-amz-server-side-encryption-customer-key-md5"); - let mut managed_encryption_applied = false; - let mut managed_original_size: Option = None; + let mut response_content_length = content_length; debug!( - "GET object metadata check: stored_sse_algorithm={:?}, stored_sse_key_md5={:?}, provided_sse_key={:?}", - stored_sse_algorithm, - stored_sse_key_md5, + "GET object metadata check: parts={}, provided_sse_key={:?}", + info.parts.len(), req.input.sse_customer_key.is_some() ); - if stored_sse_algorithm.is_some() { - // Object was encrypted with SSE-C, so customer must provide matching key - if let (Some(sse_key), Some(sse_key_md5_provided)) = (&req.input.sse_customer_key, &req.input.sse_customer_key_md5) { - // For true multipart objects (more than 1 part), SSE-C decryption is currently not fully implemented - // Each part needs to be decrypted individually, which requires storage layer changes - // Note: Single part objects also have info.parts.len() == 1, but they are not true multipart uploads - if info.parts.len() > 1 { - warn!( - "SSE-C multipart object detected with {} parts. Currently, multipart SSE-C upload parts are not encrypted during upload_part, so no decryption is needed during GET.", - info.parts.len() - ); - - // Verify that the provided key MD5 matches the stored MD5 for security - if let Some(stored_md5) = stored_sse_key_md5 { - debug!("SSE-C MD5 comparison: provided='{}', stored='{}'", sse_key_md5_provided, stored_md5); - if sse_key_md5_provided != stored_md5 { - error!("SSE-C key MD5 mismatch: provided='{}', stored='{}'", sse_key_md5_provided, stored_md5); - return Err( - ApiError::from(StorageError::other("SSE-C key does not match object encryption key")).into() - ); - } - } else { - return Err(ApiError::from(StorageError::other( - "Object encrypted with SSE-C but stored key MD5 not found", - )) - .into()); - } - - // Since upload_part currently doesn't encrypt the data (SSE-C code is commented out), - // we don't need to decrypt it either. Just return the data as-is. - // TODO: Implement proper multipart SSE-C encryption/decryption - } else { - // Verify that the provided key MD5 matches the stored MD5 - if let Some(stored_md5) = stored_sse_key_md5 { - debug!("SSE-C MD5 comparison: provided='{}', stored='{}'", sse_key_md5_provided, stored_md5); - if sse_key_md5_provided != stored_md5 { - error!("SSE-C key MD5 mismatch: provided='{}', stored='{}'", sse_key_md5_provided, stored_md5); - return Err( - ApiError::from(StorageError::other("SSE-C key does not match object encryption key")).into() - ); - } - } else { - return Err(ApiError::from(StorageError::other( - "Object encrypted with SSE-C but stored key MD5 not found", - )) - .into()); - } - - // Decode the base64 key - let key_bytes = BASE64_STANDARD - .decode(sse_key) - .map_err(|e| ApiError::from(StorageError::other(format!("Invalid SSE-C key: {e}"))))?; - - // Verify key length (should be 32 bytes for AES-256) - if key_bytes.len() != 32 { - return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into()); - } - - // Convert Vec to [u8; 32] - let mut key_array = [0u8; 32]; - key_array.copy_from_slice(&key_bytes[..32]); - - // Verify MD5 hash of the key matches what the client claims - let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0); - if computed_md5 != *sse_key_md5_provided { - return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into()); - } - - // Generate the same deterministic nonce from object key - let mut nonce = [0u8; 12]; - let nonce_source = format!("{bucket}-{key}"); - let nonce_hash = md5::compute(nonce_source.as_bytes()); - nonce.copy_from_slice(&nonce_hash.0[..12]); - - // Apply decryption - // We need to wrap the stream in a Reader first since DecryptReader expects a Reader - let warp_reader = WarpReader::new(final_stream); - let decrypt_reader = DecryptReader::new(warp_reader, key_array, nonce); - final_stream = Box::new(decrypt_reader); - } - } else { - return Err( - ApiError::from(StorageError::other("Object encrypted with SSE-C but no customer key provided")).into(), - ); - } - } - - if stored_sse_algorithm.is_none() - && let Some((key_bytes, nonce, original_size)) = - decrypt_managed_encryption_key(&bucket, &key, &info.user_defined).await? - { - if info.parts.len() > 1 { - let (reader, plain_size) = decrypt_multipart_managed_stream(final_stream, &info.parts, key_bytes, nonce) - .await - .map_err(ApiError::from)?; - final_stream = reader; - managed_original_size = Some(plain_size); - } else { - let warp_reader = WarpReader::new(final_stream); - let decrypt_reader = DecryptReader::new(warp_reader, key_bytes, nonce); - final_stream = Box::new(decrypt_reader); - managed_original_size = original_size; - } - managed_encryption_applied = true; - } - - // For SSE-C encrypted objects, use the original size instead of encrypted size - let response_content_length = if stored_sse_algorithm.is_some() { - if let Some(original_size_str) = info.user_defined.get("x-amz-server-side-encryption-customer-original-size") { - let original_size = original_size_str.parse::().unwrap_or(content_length); - info!( - "SSE-C decryption: using original size {} instead of encrypted size {}", - original_size, content_length - ); - original_size - } else { - debug!("SSE-C decryption: no original size found, using content_length {}", content_length); - content_length - } - } else if managed_encryption_applied { - managed_original_size.unwrap_or(content_length) - } else { - content_length + let decryption_request = DecryptionRequest { + bucket: &bucket, + key: &key, + metadata: &info.user_defined, + sse_customer_key: req.input.sse_customer_key.as_ref(), + sse_customer_key_md5: req.input.sse_customer_key_md5.as_ref(), + part_number: None, + parts: &info.parts, }; - info!("Final response_content_length: {}", response_content_length); + let (server_side_encryption, sse_customer_algorithm, sse_customer_key_md5, ssekms_key_id, encryption_applied) = + match sse_decryption(decryption_request).await? { + Some(material) => { + let server_side_encryption = Some(material.server_side_encryption.clone()); + let sse_customer_algorithm = Some(material.algorithm.clone()); + let sse_customer_key_md5 = material.customer_key_md5.clone(); + let ssekms_key_id = material.kms_key_id.clone(); - if stored_sse_algorithm.is_some() || managed_encryption_applied { - let limit_reader = HardLimitReader::new(Box::new(WarpReader::new(final_stream)), response_content_length); - final_stream = Box::new(limit_reader); - } + // Apply unified SSE decryption (handles single-part, multipart, and hard limit) + let (decrypted_stream, plaintext_size) = material + .wrap_reader(final_stream, content_length) + .await + .map_err(ApiError::from)?; + + final_stream = decrypted_stream; + response_content_length = plaintext_size; + + (server_side_encryption, sse_customer_algorithm, sse_customer_key_md5, ssekms_key_id, true) + } + None => (None, None, None, None, false), + }; // Calculate concurrency-aware buffer size for optimal performance // This adapts based on the number of concurrent GetObject requests @@ -2891,8 +2689,7 @@ impl S3 for FS { && io_strategy.cache_writeback_enabled && part_number.is_none() && rs.is_none() - && !managed_encryption_applied - && stored_sse_algorithm.is_none() + && !encryption_applied && response_content_length > 0 && (response_content_length as usize) <= manager.max_object_size(); @@ -2953,11 +2750,11 @@ impl S3 for FS { ReaderStream::with_capacity(Box::new(mem_reader), optimal_buffer_size), response_content_length as usize, ))) - } else if stored_sse_algorithm.is_some() || managed_encryption_applied { - // For SSE-C encrypted objects, don't use bytes_stream to limit the stream + } else if encryption_applied { + // For encrypted objects, don't use bytes_stream to limit the stream // because DecryptReader needs to read all encrypted data to produce decrypted output info!( - "Managed SSE: Using unlimited stream for decryption with buffer size {}", + "SSE decryption: Using unlimited stream for decryption with buffer size {}", optimal_buffer_size ); Some(StreamingBlob::wrap(ReaderStream::with_capacity(final_stream, optimal_buffer_size))) @@ -3013,21 +2810,6 @@ impl S3 for FS { } }; - // Extract SSE information from metadata for response - let server_side_encryption = info - .user_defined - .get("x-amz-server-side-encryption") - .map(|v| ServerSideEncryption::from(v.clone())); - let sse_customer_algorithm = info - .user_defined - .get("x-amz-server-side-encryption-customer-algorithm") - .map(|v| SSECustomerAlgorithm::from(v.clone())); - let sse_customer_key_md5 = info - .user_defined - .get("x-amz-server-side-encryption-customer-key-md5") - .cloned(); - let ssekms_key_id = info.user_defined.get("x-amz-server-side-encryption-aws-kms-key-id").cloned(); - let mut checksum_crc32 = None; let mut checksum_crc32c = None; let mut checksum_sha1 = None; @@ -4562,12 +4344,54 @@ impl S3 for FS { sse_customer_key_md5, ssekms_key_id, content_md5, + if_match, + if_none_match, .. } = input; // Validate object key validate_object_key(&key, "PUT")?; + if if_match.is_some() || if_none_match.is_some() { + let Some(store) = new_object_layer_fn() else { + return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string())); + }; + + match store.get_object_info(&bucket, &key, &ObjectOptions::default()).await { + Ok(info) => { + if !info.delete_marker { + if let Some(ifmatch) = if_match + && let Some(strong_etag) = ifmatch.into_etag() + && info + .etag + .as_ref() + .is_some_and(|etag| ETag::Strong(etag.clone()) != strong_etag) + { + return Err(s3_error!(PreconditionFailed)); + } + if let Some(ifnonematch) = if_none_match + && let Some(strong_etag) = ifnonematch.into_etag() + && info + .etag + .as_ref() + .is_some_and(|etag| ETag::Strong(etag.clone()) == strong_etag) + { + return Err(s3_error!(PreconditionFailed)); + } + } + } + Err(err) => { + if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) { + return Err(ApiError::from(err).into()); + } + + if if_match.is_some() && (is_err_object_not_found(&err) || is_err_version_not_found(&err)) { + return Err(ApiError::from(err).into()); + } + } + } + } + // check quota for put operation if let Some(size) = content_length && let Some(metadata_sys) = rustfs_ecstore::bucket::metadata_sys::GLOBAL_BucketMetadataSys.get() @@ -4631,40 +4455,6 @@ impl S3 for FS { let store = get_validated_store(&bucket).await?; - // TDD: Get bucket default encryption configuration - let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok(); - debug!("TDD: bucket_sse_config={:?}", bucket_sse_config); - - // TDD: Determine effective encryption configuration (request overrides bucket default) - let original_sse = server_side_encryption.clone(); - let effective_sse = server_side_encryption.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _timestamp)| { - debug!("TDD: Processing bucket SSE config: {:?}", config); - config.rules.first().and_then(|rule| { - debug!("TDD: Processing SSE rule: {:?}", rule); - rule.apply_server_side_encryption_by_default.as_ref().map(|sse| { - debug!("TDD: Found SSE default: {:?}", sse); - match sse.sse_algorithm.as_str() { - "AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256), - "aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS), - _ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), // fallback to AES256 - } - }) - }) - }) - }); - debug!("TDD: effective_sse={:?} (original={:?})", effective_sse, original_sse); - - let mut effective_kms_key_id = ssekms_key_id.or_else(|| { - bucket_sse_config.as_ref().and_then(|(config, _timestamp)| { - config.rules.first().and_then(|rule| { - rule.apply_server_side_encryption_by_default - .as_ref() - .and_then(|sse| sse.kms_master_key_id.clone()) - }) - }) - }); - let mut metadata = metadata.unwrap_or_default(); let object_lock_configuration = match metadata_sys::get_object_lock_config(&bucket).await { @@ -4694,24 +4484,6 @@ impl S3 for FS { metadata.insert(AMZ_OBJECT_TAGGING.to_owned(), tags.to_string()); } - // TDD: Store effective SSE information in metadata for GET responses - if let Some(sse_alg) = &sse_customer_algorithm { - metadata.insert( - "x-amz-server-side-encryption-customer-algorithm".to_string(), - sse_alg.as_str().to_string(), - ); - } - if let Some(sse_md5) = &sse_customer_key_md5 { - metadata.insert("x-amz-server-side-encryption-customer-key-md5".to_string(), sse_md5.clone()); - } - if let Some(sse) = &effective_sse { - metadata.insert("x-amz-server-side-encryption".to_string(), sse.as_str().to_string()); - } - - if let Some(kms_key_id) = &effective_kms_key_id { - metadata.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), kms_key_id.clone()); - } - let mut opts: ObjectOptions = put_opts(&bucket, &key, version_id.clone(), &req.headers, metadata.clone()) .await .map_err(ApiError::from)?; @@ -4765,75 +4537,43 @@ impl S3 for FS { opts.want_checksum = reader.checksum(); } - // Apply SSE-C encryption if customer provided key - if let (Some(_), Some(sse_key), Some(sse_key_md5_provided)) = - (&sse_customer_algorithm, &sse_customer_key, &sse_customer_key_md5) - { - // Decode the base64 key - let key_bytes = BASE64_STANDARD - .decode(sse_key) - .map_err(|e| ApiError::from(StorageError::other(format!("Invalid SSE-C key: {e}"))))?; + // Apply encryption using unified SSE API + let encryption_request = EncryptionRequest { + bucket: &bucket, + key: &key, + server_side_encryption, + ssekms_key_id, + sse_customer_algorithm: sse_customer_algorithm.clone(), + sse_customer_key, + sse_customer_key_md5: sse_customer_key_md5.clone(), + content_size: actual_size, + part_number: None, + part_key: None, + part_nonce: None, + }; - // Verify key length (should be 32 bytes for AES-256) - if key_bytes.len() != 32 { - return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into()); + let (effective_sse, effective_kms_key_id) = match sse_encryption(encryption_request).await? { + Some(material) => { + let server_side_encryption = Some(material.server_side_encryption.clone()); + let ssekms_key_id = material.kms_key_id.clone(); + + // Apply encryption wrapper + let encrypted_reader = material.wrap_reader(reader); + reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false) + .map_err(ApiError::from)?; + + // Merge encryption metadata + metadata.extend(material.metadata); + + (server_side_encryption, ssekms_key_id) } - - // Convert Vec to [u8; 32] - let mut key_array = [0u8; 32]; - key_array.copy_from_slice(&key_bytes[..32]); - - // Verify MD5 hash of the key matches what the client claims - let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0); - if computed_md5 != *sse_key_md5_provided { - return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into()); - } - - // Store original size for later retrieval during decryption - let original_size = if size >= 0 { size } else { actual_size }; - metadata.insert( - "x-amz-server-side-encryption-customer-original-size".to_string(), - original_size.to_string(), - ); - - // Generate a deterministic nonce from object key for consistency - let mut nonce = [0u8; 12]; - let nonce_source = format!("{bucket}-{key}"); - let nonce_hash = md5::compute(nonce_source.as_bytes()); - nonce.copy_from_slice(&nonce_hash.0[..12]); - - // Apply encryption - let encrypt_reader = EncryptReader::new(reader, key_array, nonce); - reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?; - } - - // Apply managed SSE (SSE-S3 or SSE-KMS) when requested - if sse_customer_algorithm.is_none() - && let Some(sse_alg) = &effective_sse - && is_managed_sse(sse_alg) - { - let material = - create_managed_encryption_material(&bucket, &key, sse_alg, effective_kms_key_id.clone(), actual_size).await?; - - let ManagedEncryptionMaterial { - data_key, - headers, - kms_key_id: kms_key_used, - } = material; - - let key_bytes = data_key.plaintext_key; - let nonce = data_key.nonce; - - metadata.extend(headers); - effective_kms_key_id = Some(kms_key_used.clone()); - - let encrypt_reader = EncryptReader::new(reader, key_bytes, nonce); - reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?; - } + None => (None, None), + }; let mut reader = PutObjReader::new(reader); let mt2 = metadata.clone(); + opts.user_defined.extend(metadata); let repoptions = get_must_replicate_options(&mt2, "".to_string(), ReplicationStatusType::Empty, ReplicationType::Object, opts.clone()); @@ -4923,8 +4663,8 @@ impl S3 for FS { let output = PutObjectOutput { e_tag, server_side_encryption: effective_sse, // TDD: Return effective encryption config - sse_customer_algorithm: sse_customer_algorithm.clone(), - sse_customer_key_md5: sse_customer_key_md5.clone(), + sse_customer_algorithm, + sse_customer_key_md5, ssekms_key_id: effective_kms_key_id, // TDD: Return effective KMS key ID checksum_crc32, checksum_crc32c, @@ -5551,9 +5291,9 @@ impl S3 for FS { upload_id, part_number, content_length, - sse_customer_algorithm: _sse_customer_algorithm, - sse_customer_key: _sse_customer_key, - sse_customer_key_md5: _sse_customer_key_md5, + sse_customer_algorithm, + sse_customer_key, + sse_customer_key_md5, // content_md5, .. } = input; @@ -5598,15 +5338,13 @@ impl S3 for FS { }; let opts = ObjectOptions::default(); - let fi = store + let mut fi = store .get_multipart_info(&bucket, &key, &upload_id, &opts) .await .map_err(ApiError::from)?; // Check if managed encryption will be applied - let will_apply_managed_encryption = decrypt_managed_encryption_key(&bucket, &key, &fi.user_defined) - .await? - .is_some(); + let will_apply_managed_encryption = check_encryption_metadata(&fi.user_defined); // If managed encryption will be applied, and we have Content-Length, buffer the entire body // This is necessary because encryption changes the data size, which causes Content-Length mismatches @@ -5650,46 +5388,6 @@ impl S3 for FS { let actual_size = size; - // TODO: Apply SSE-C encryption for upload_part if needed - // Temporarily commented out to debug multipart issues - /* - // Apply SSE-C encryption if customer provided key before any other processing - if let (Some(_), Some(sse_key), Some(sse_key_md5_provided)) = - (&_sse_customer_algorithm, &_sse_customer_key, &_sse_customer_key_md5) { - - // Decode the base64 key - let key_bytes = BASE64_STANDARD.decode(sse_key) - .map_err(|e| ApiError::from(StorageError::other(format!("Invalid SSE-C key: {}", e))))?; - - // Verify key length (should be 32 bytes for AES-256) - if key_bytes.len() != 32 { - return Err(ApiError::from(StorageError::other("SSE-C key must be 32 bytes")).into()); - } - - // Convert Vec to [u8; 32] - let mut key_array = [0u8; 32]; - key_array.copy_from_slice(&key_bytes[..32]); - - // Verify MD5 hash of the key matches what the client claims - let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0); - if computed_md5 != *sse_key_md5_provided { - return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch")).into()); - } - - // Generate a deterministic nonce from object key for consistency - let mut nonce = [0u8; 12]; - let nonce_source = format!("{}-{}", bucket, key); - let nonce_hash = md5::compute(nonce_source.as_bytes()); - nonce.copy_from_slice(&nonce_hash.0[..12]); - - // Apply encryption - this will change the size so we need to handle it - let encrypt_reader = EncryptReader::new(reader, key_array, nonce); - reader = Box::new(encrypt_reader); - // When encrypting, size becomes unknown since encryption adds authentication tags - size = -1; - } - */ - let mut md5hex = if let Some(base64_md5) = input.content_md5 { let md5 = base64_simd::STANDARD .decode_to_vec(base64_md5.as_bytes()) @@ -5721,12 +5419,56 @@ impl S3 for FS { return Err(ApiError::from(StorageError::other(format!("add_checksum error={err:?}"))).into()); } - if let Some((key_bytes, base_nonce, _)) = decrypt_managed_encryption_key(&bucket, &key, &fi.user_defined).await? { - let part_nonce = derive_part_nonce(base_nonce, part_id); - let encrypt_reader = EncryptReader::new(reader, key_bytes, part_nonce); - reader = HashReader::new(Box::new(encrypt_reader), HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false) - .map_err(ApiError::from)?; - } + // Apply unified SSE encryption for upload_part + // Note: For SSE-C, the key is provided with each part upload + // For managed SSE, the encryption material was generated in create_multipart_upload + let server_side_encryption = fi + .user_defined + .get("x-amz-server-side-encryption") + .map(|s| { + ServerSideEncryption::from_str(s) + .map_err(|e| ApiError::from(StorageError::other(format!("Invalid server-side encryption: {e}")))) + }) + .transpose()?; + let ssekms_key_id = fi + .user_defined + .get("x-amz-server-side-encryption-aws-kms-key-id") + .map(|s| s.to_string()); + let part_key = fi.user_defined.get("x-rustfs-encryption-key").cloned(); + let part_nonce = fi.user_defined.get("x-rustfs-encryption-iv").cloned(); + let encryption_request = EncryptionRequest { + bucket: &bucket, + key: &key, + server_side_encryption, // Managed SSE handled below + ssekms_key_id, + sse_customer_algorithm: sse_customer_algorithm.clone(), + sse_customer_key, + sse_customer_key_md5: sse_customer_key_md5.clone(), + content_size: actual_size, + part_number: Some(part_id), + part_key, + part_nonce, + }; + + encryption_request.check_upload_part_customer_key_md5(&fi.user_defined, sse_customer_key_md5.clone())?; + + let (requested_sse, requested_kms_key_id) = match sse_encryption(encryption_request).await? { + Some(material) => { + let requested_sse = Some(material.server_side_encryption.clone()); + let requested_kms_key_id = material.kms_key_id.clone(); + + // Apply encryption wrapper + let encrypted_reader = material.wrap_reader(reader); + reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false) + .map_err(ApiError::from)?; + + // Merge encryption metadata + fi.user_defined.extend(material.metadata); + + (requested_sse, requested_kms_key_id) + } + None => (None, None), + }; let mut reader = PutObjReader::new(reader); @@ -5769,6 +5511,10 @@ impl S3 for FS { } let output = UploadPartOutput { + server_side_encryption: requested_sse, + ssekms_key_id: requested_kms_key_id, + sse_customer_algorithm, + sse_customer_key_md5, checksum_crc32, checksum_crc32c, checksum_sha1, @@ -5792,6 +5538,9 @@ impl S3 for FS { upload_id, copy_source_if_match, copy_source_if_none_match, + sse_customer_algorithm, + sse_customer_key, + sse_customer_key_md5, .. } = req.input; @@ -5821,7 +5570,7 @@ impl S3 for FS { }; // Check if multipart upload exists and get its info - let mp_info = store + let mut mp_info = store .get_multipart_info(&bucket, &key, &upload_id, &ObjectOptions::default()) .await .map_err(ApiError::from)?; @@ -5924,11 +5673,23 @@ impl S3 for FS { let mut reader: Box = Box::new(WarpReader::new(src_stream)); - if let Some((key_bytes, nonce, original_size_opt)) = - decrypt_managed_encryption_key(&src_bucket, &src_key, &src_info.user_defined).await? - { - reader = Box::new(DecryptReader::new(reader, key_bytes, nonce)); - if let Some(original) = original_size_opt { + // Apply unified SSE decryption for source object if encrypted + // Note: SSE-C for copy source is handled via copy_source_sse_customer_* headers + let copy_source_sse_customer_key = req.input.copy_source_sse_customer_key.as_ref(); + let copy_source_sse_customer_key_md5 = req.input.copy_source_sse_customer_key_md5.as_ref(); + let src_decryption_request = DecryptionRequest { + bucket: &src_bucket, + key: &src_key, + metadata: &src_info.user_defined, + sse_customer_key: copy_source_sse_customer_key, + sse_customer_key_md5: copy_source_sse_customer_key_md5, + part_number: None, + parts: &src_info.parts, + }; + + if let Some(material) = sse_decryption(src_decryption_request).await? { + reader = material.wrap_single_reader(reader); + if let Some(original) = material.original_size { src_info.actual_size = original; } } @@ -5944,11 +5705,56 @@ impl S3 for FS { let mut reader = HashReader::new(reader, size, actual_size, None, None, false).map_err(ApiError::from)?; - if let Some((key_bytes, base_nonce, _)) = decrypt_managed_encryption_key(&bucket, &key, &mp_info.user_defined).await? { - let part_nonce = derive_part_nonce(base_nonce, part_id); - let encrypt_reader = EncryptReader::new(reader, key_bytes, part_nonce); - reader = HashReader::new(Box::new(encrypt_reader), -1, actual_size, None, None, false).map_err(ApiError::from)?; - } + // Apply unified SSE encryption for upload_part + // Note: For SSE-C, the key is provided with each part upload + // For managed SSE, the encryption material was generated in create_multipart_upload + let server_side_encryption = mp_info + .user_defined + .get("x-amz-server-side-encryption") + .map(|s| { + ServerSideEncryption::from_str(s) + .map_err(|e| ApiError::from(StorageError::other(format!("Invalid server-side encryption: {e}")))) + }) + .transpose()?; + let ssekms_key_id = mp_info + .user_defined + .get("x-amz-server-side-encryption-aws-kms-key-id") + .map(|s| s.to_string()); + let part_key = mp_info.user_defined.get("x-rustfs-encryption-key").cloned(); + let part_nonce = mp_info.user_defined.get("x-rustfs-encryption-iv").cloned(); + let encryption_request = EncryptionRequest { + bucket: &bucket, + key: &key, + server_side_encryption, // Managed SSE handled below + ssekms_key_id, + sse_customer_algorithm: sse_customer_algorithm.clone(), + sse_customer_key, + sse_customer_key_md5: sse_customer_key_md5.clone(), + content_size: actual_size, + part_number: Some(part_id), + part_key, + part_nonce, + }; + + encryption_request.check_upload_part_customer_key_md5(&mp_info.user_defined, sse_customer_key_md5.clone())?; + + let (requested_sse, requested_kms_key_id) = match sse_encryption(encryption_request).await? { + Some(material) => { + let requested_sse = Some(material.server_side_encryption.clone()); + let requested_kms_key_id = material.kms_key_id.clone(); + + // Apply encryption wrapper + let encrypted_reader = material.wrap_reader(reader); + reader = HashReader::new(encrypted_reader, HashReader::SIZE_PRESERVE_LAYER, actual_size, None, None, false) + .map_err(ApiError::from)?; + + // Merge encryption metadata + mp_info.user_defined.extend(material.metadata); + + (requested_sse, requested_kms_key_id) + } + None => (None, None), + }; let mut reader = PutObjReader::new(reader); @@ -5974,6 +5780,10 @@ impl S3 for FS { let output = UploadPartCopyOutput { copy_part_result: Some(copy_part_result), copy_source_version_id: src_version_id, + server_side_encryption: requested_sse, + ssekms_key_id: requested_kms_key_id, + sse_customer_algorithm, + sse_customer_key_md5, ..Default::default() }; diff --git a/rustfs/src/storage/ecfs_extend.rs b/rustfs/src/storage/ecfs_extend.rs index e0fb9f3f..da5bf0ad 100644 --- a/rustfs/src/storage/ecfs_extend.rs +++ b/rustfs/src/storage/ecfs_extend.rs @@ -17,7 +17,7 @@ use crate::config::workload_profiles::{ }; use crate::error::ApiError; use crate::server::cors; -use crate::storage::ecfs::{InMemoryAsyncReader, ListObjectUnorderedQuery}; +use crate::storage::ecfs::ListObjectUnorderedQuery; use axum::body::Body; use http::{HeaderMap, HeaderValue, StatusCode}; use metrics::counter; @@ -28,9 +28,6 @@ use rustfs_ecstore::bucket::replication::ReplicationConfigurationExt; use rustfs_ecstore::error::StorageError; use rustfs_ecstore::store_api::{BucketOptions, ObjectInfo, ObjectToDelete}; use rustfs_ecstore::{StorageAPI, new_object_layer_fn}; -use rustfs_filemeta::ObjectPartInfo; -use rustfs_kms::{EncryptionMetadata, ObjectEncryptionContext, get_global_encryption_service}; -use rustfs_rio::{DecryptReader, Reader, WarpReader}; use rustfs_targets::EventName; use rustfs_targets::arn::{TargetID, TargetIDError}; use rustfs_utils::http::{ @@ -40,7 +37,7 @@ use rustfs_utils::http::{ use s3s::dto::{ Delimiter, LambdaFunctionConfiguration, NotificationConfigurationFilter, ObjectLockConfiguration, ObjectLockEnabled, ObjectLockLegalHold, ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, QueueConfiguration, - ServerSideEncryption, TopicConfiguration, + TopicConfiguration, }; use s3s::{S3Error, S3ErrorCode, S3Response, S3Result}; use serde_urlencoded::from_bytes; @@ -50,7 +47,6 @@ use std::sync::Arc; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; use time::{format_description::FormatItem, macros::format_description}; -use tokio::io::AsyncRead; use tracing::{debug, warn}; pub const RFC1123: &[FormatItem<'_>] = @@ -326,180 +322,6 @@ pub(crate) fn get_buffer_size_opt_in(file_size: i64) -> usize { buffer_size } -pub(crate) async fn create_managed_encryption_material( - bucket: &str, - key: &str, - algorithm: &ServerSideEncryption, - kms_key_id: Option, - original_size: i64, -) -> Result { - let Some(service) = get_global_encryption_service().await else { - return Err(ApiError::from(StorageError::other("KMS encryption service is not initialized"))); - }; - - if !is_managed_sse(algorithm) { - return Err(ApiError::from(StorageError::other(format!( - "Unsupported server-side encryption algorithm: {}", - algorithm.as_str() - )))); - } - - let algorithm_str = algorithm.as_str(); - - let mut context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string()); - if original_size >= 0 { - context = context.with_size(original_size as u64); - } - - let mut kms_key_candidate = kms_key_id; - if kms_key_candidate.is_none() { - kms_key_candidate = service.get_default_key_id().cloned(); - } - - let kms_key_to_use = kms_key_candidate - .clone() - .ok_or_else(|| ApiError::from(StorageError::other("No KMS key available for managed server-side encryption")))?; - - let (data_key, encrypted_data_key) = service - .create_data_key(&kms_key_candidate, &context) - .await - .map_err(|e| ApiError::from(StorageError::other(format!("Failed to create data key: {e}"))))?; - - let metadata = EncryptionMetadata { - algorithm: algorithm_str.to_string(), - key_id: kms_key_to_use.clone(), - key_version: 1, - iv: data_key.nonce.to_vec(), - tag: None, - encryption_context: context.encryption_context.clone(), - encrypted_at: jiff::Zoned::now(), - original_size: if original_size >= 0 { original_size as u64 } else { 0 }, - encrypted_data_key, - }; - - let mut headers = service.metadata_to_headers(&metadata); - headers.insert("x-rustfs-encryption-original-size".to_string(), metadata.original_size.to_string()); - - Ok(crate::storage::ecfs::ManagedEncryptionMaterial { - data_key, - headers, - kms_key_id: kms_key_to_use, - }) -} - -pub(crate) async fn decrypt_managed_encryption_key( - bucket: &str, - key: &str, - metadata: &HashMap, -) -> Result)>, ApiError> { - if !metadata.contains_key("x-rustfs-encryption-key") { - return Ok(None); - } - - let Some(service) = get_global_encryption_service().await else { - return Err(ApiError::from(StorageError::other("KMS encryption service is not initialized"))); - }; - - let parsed = service - .headers_to_metadata(metadata) - .map_err(|e| ApiError::from(StorageError::other(format!("Failed to parse encryption metadata: {e}"))))?; - - if parsed.iv.len() != 12 { - return Err(ApiError::from(StorageError::other("Invalid encryption nonce length; expected 12 bytes"))); - } - - let context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string()); - let data_key = service - .decrypt_data_key(&parsed.encrypted_data_key, &context) - .await - .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decrypt data key: {e}"))))?; - - let key_bytes = data_key.plaintext_key; - let mut nonce = [0u8; 12]; - nonce.copy_from_slice(&parsed.iv[..12]); - - let original_size = metadata - .get("x-rustfs-encryption-original-size") - .and_then(|s| s.parse::().ok()); - - Ok(Some((key_bytes, nonce, original_size))) -} - -pub(crate) fn derive_part_nonce(base: [u8; 12], part_number: usize) -> [u8; 12] { - let mut nonce = base; - let current = u32::from_be_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]); - let incremented = current.wrapping_add(part_number as u32); - nonce[8..12].copy_from_slice(&incremented.to_be_bytes()); - nonce -} - -pub(crate) async fn decrypt_multipart_managed_stream( - mut encrypted_stream: Box, - parts: &[ObjectPartInfo], - key_bytes: [u8; 32], - base_nonce: [u8; 12], -) -> Result<(Box, i64), StorageError> { - let total_plain_capacity: usize = parts.iter().map(|part| part.actual_size.max(0) as usize).sum(); - - let mut plaintext = Vec::with_capacity(total_plain_capacity); - - for part in parts { - if part.size == 0 { - continue; - } - - let mut encrypted_part = vec![0u8; part.size]; - tokio::io::AsyncReadExt::read_exact(&mut encrypted_stream, &mut encrypted_part) - .await - .map_err(|e| StorageError::other(format!("failed to read encrypted multipart segment {}: {}", part.number, e)))?; - - let part_nonce = derive_part_nonce(base_nonce, part.number); - let cursor = std::io::Cursor::new(encrypted_part); - let mut decrypt_reader = DecryptReader::new(WarpReader::new(cursor), key_bytes, part_nonce); - - tokio::io::AsyncReadExt::read_to_end(&mut decrypt_reader, &mut plaintext) - .await - .map_err(|e| StorageError::other(format!("failed to decrypt multipart segment {}: {}", part.number, e)))?; - } - - let total_plain_size = plaintext.len() as i64; - let reader = Box::new(WarpReader::new(InMemoryAsyncReader::new(plaintext))) as Box; - - Ok((reader, total_plain_size)) -} - -pub(crate) fn strip_managed_encryption_metadata(metadata: &mut HashMap) { - const KEYS: [&str; 7] = [ - "x-amz-server-side-encryption", - "x-amz-server-side-encryption-aws-kms-key-id", - "x-rustfs-encryption-iv", - "x-rustfs-encryption-tag", - "x-rustfs-encryption-key", - "x-rustfs-encryption-context", - "x-rustfs-encryption-original-size", - ]; - - for key in KEYS.iter() { - metadata.remove(*key); - } -} - -/// Check if the given server-side encryption algorithm is a managed SSE type -/// -/// This function checks if the provided ServerSideEncryption algorithm -/// corresponds to a managed server-side encryption method, specifically -/// "AES256" or "aws:kms". -/// -/// # Arguments -/// * `algorithm` - A reference to the ServerSideEncryption enum to check. -/// -/// # Returns -/// * `true` if the algorithm is "AES256" or "aws:kms", otherwise `false`. -/// -pub(crate) fn is_managed_sse(algorithm: &ServerSideEncryption) -> bool { - matches!(algorithm.as_str(), "AES256" | "aws:kms") -} - /// Validate object key for control characters and log special characters /// /// This function: diff --git a/rustfs/src/storage/mod.rs b/rustfs/src/storage/mod.rs index 571e344e..8739be93 100644 --- a/rustfs/src/storage/mod.rs +++ b/rustfs/src/storage/mod.rs @@ -26,5 +26,8 @@ mod ecfs_extend; #[cfg(test)] mod ecfs_test; pub(crate) mod head_prefix; +mod sse; +#[cfg(test)] +mod sse_test; pub(crate) use ecfs_extend::*; diff --git a/rustfs/src/storage/sse.rs b/rustfs/src/storage/sse.rs new file mode 100644 index 00000000..6a7cdb45 --- /dev/null +++ b/rustfs/src/storage/sse.rs @@ -0,0 +1,1890 @@ +// 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 (SSE) utilities +//! +//! This module provides reusable components for handling S3 Server-Side Encryption: +//! - SSE-S3 (AES256): Server-managed encryption with S3-managed keys +//! - SSE-KMS (aws:kms): Server-managed encryption with KMS-managed keys +//! - SSE-C (AES256): Customer-provided encryption keys +//! +//! ## Architecture +//! +//! ### Unified API +//! The module provides two core functions that automatically route to the correct encryption method: +//! - `apply_encryption()` - Unified encryption entry point +//! - `apply_decryption()` - Unified decryption entry point +//! +//! ### Managed SSE (SSE-S3 / SSE-KMS) +//! - Keys are managed by the server-side KMS service +//! - Data keys are generated and encrypted by KMS +//! - Encryption metadata is stored in object metadata +//! +//! ### Customer-Provided Keys (SSE-C) +//! - Keys are provided by the client on every request +//! - Server validates key using MD5 hash +//! - Keys are NEVER stored on the server +//! +//! ## Usage Example +//! +//! ```rust,ignore +//! // Unified encryption API +//! let request = EncryptionRequest { +//! bucket: &bucket, +//! key: &key, +//! server_side_encryption: effective_sse.as_ref(), +//! ssekms_key_id: effective_kms_key_id.as_deref(), +//! sse_customer_algorithm: sse_customer_algorithm.as_ref(), +//! sse_customer_key: sse_customer_key.as_deref(), +//! sse_customer_key_md5: sse_customer_key_md5.as_deref(), +//! content_size: actual_size, +//! part_number: None, +//! }; +//! +//! if let Some(material) = apply_encryption(request).await? { +//! reader = material.wrap_reader(reader)?; +//! metadata.extend(material.metadata); +//! } +//! +//! // Unified decryption API +//! let request = DecryptionRequest { +//! bucket: &bucket, +//! key: &key, +//! metadata: &metadata, +//! sse_customer_key: sse_customer_key.as_deref(), +//! sse_customer_key_md5: sse_customer_key_md5.as_deref(), +//! part_number: None, +//! }; +//! +//! if let Some(material) = apply_decryption(request).await? { +//! reader = material.wrap_reader(reader)?; +//! } +//! ``` + +use aes_gcm::{ + Aes256Gcm, Key, Nonce, + aead::{Aead, KeyInit}, +}; +use async_trait::async_trait; +use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use rand::RngCore; +use rustfs_ecstore::error::StorageError; +use rustfs_filemeta::ObjectPartInfo; +use rustfs_kms::{ + DataKey, + service_manager::get_global_encryption_service, + types::{EncryptionMetadata, ObjectEncryptionContext}, +}; +use rustfs_rio::{DecryptReader, EncryptReader, HardLimitReader, Reader, WarpReader}; +use s3s::dto::ServerSideEncryption; +use std::collections::HashMap; +use std::sync::{Arc, OnceLock}; +use tokio::io::AsyncRead; +use tracing::{debug, error}; + +use crate::error::ApiError; +use crate::storage::ecfs::InMemoryAsyncReader; +use rustfs_ecstore::bucket::metadata_sys; +use rustfs_ecstore::error::Error; +use s3s::dto::{SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5, SSEKMSKeyId}; + +// ============================================================================ +// High-Level SSE Configuration +// ============================================================================ + +const DEFAULT_SSE_ALGORITHM: &str = "AES256"; + +const SUPPORT_SSE_ALGORITHMS: &[&str] = &[DEFAULT_SSE_ALGORITHM]; + +// check sse type +#[allow(unused)] +pub fn get_sse_type( + server_side_encryption: Option<&ServerSideEncryption>, + customer_algorithm: Option<&SSECustomerAlgorithm>, + customer_key: Option<&SSECustomerKey>, + customer_key_md5: Option<&SSECustomerKeyMD5>, +) -> Option { + if customer_algorithm.is_some() && customer_key.is_some() && customer_key_md5.is_some() { + return Some(SSEType::SseC); + } + + let sse = server_side_encryption?; + match sse.as_str() { + ServerSideEncryption::AES256 => Some(SSEType::SseS3), + ServerSideEncryption::AWS_KMS => Some(SSEType::SseKms), + _ => None, + } +} + +/// SSE configuration resolved from request and bucket defaults +#[derive(Debug)] +pub struct SseConfiguration { + /// Effective server-side encryption algorithm (after considering bucket defaults) + pub effective_sse: ServerSideEncryption, + /// Effective KMS key ID (after considering bucket defaults) + pub effective_kms_key_id: Option, +} + +/// Prepare SSE configuration by resolving request parameters with bucket defaults +/// +/// This function: +/// 1. Queries bucket default encryption configuration +/// 2. Resolves effective encryption (request overrides bucket default) +/// 3. Prepares metadata headers for managed SSE +/// +/// # Arguments +/// * `bucket` - Bucket name +/// * `server_side_encryption` - SSE algorithm from request (SSE-S3 or SSE-KMS) +/// * `ssekms_key_id` - KMS key ID from request +/// * `sse_customer_algorithm` - SSE-C algorithm from request +/// +/// # Returns +/// `SseConfiguration` with resolved encryption parameters and metadata headers +async fn prepare_sse_configuration( + bucket: &str, + server_side_encryption: Option, + ssekms_key_id: Option, +) -> Result, ApiError> { + if let Some(server_side_encryption) = server_side_encryption.clone() + && let Some(ssekms_key_id) = ssekms_key_id + { + return Ok(Some(SseConfiguration { + effective_sse: server_side_encryption, + effective_kms_key_id: Some(ssekms_key_id), + })); + } + + // Get bucket default encryption configuration + let bucket_sse_config_result = metadata_sys::get_sse_config(bucket).await; + debug!("bucket_sse_config_result={:?}", bucket_sse_config_result); + + if let Ok((bucket_sse_config, _timestamp)) = bucket_sse_config_result { + let effective_sse = server_side_encryption + .clone() + .or_else(|| { + bucket_sse_config.rules.first().and_then(|rule| { + debug!("Processing SSE rule: {:?}", rule); + rule.apply_server_side_encryption_by_default.as_ref().map(|sse| { + debug!("Found SSE default: {:?}", sse); + match sse.sse_algorithm.as_str() { + "AES256" => ServerSideEncryption::from_static(ServerSideEncryption::AES256), + "aws:kms" => ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS), + _ => ServerSideEncryption::from_static(ServerSideEncryption::AES256), // fallback to AES256 + } + }) + }) + }) + .unwrap_or_else(|| ServerSideEncryption::from_static(ServerSideEncryption::AES256)); + debug!("effective_sse={:?} (original={:?})", effective_sse, server_side_encryption); + + let effective_kms_key_id = ssekms_key_id.or_else(|| { + bucket_sse_config.rules.first().and_then(|rule| { + rule.apply_server_side_encryption_by_default + .as_ref() + .and_then(|sse| sse.kms_master_key_id.clone()) + }) + }); + + Ok(Some(SseConfiguration { + effective_sse, + effective_kms_key_id, + })) + } else if let Err(e) = bucket_sse_config_result { + match e { + Error::ConfigNotFound => Ok(None), + _ => Err(ApiError::from(e)), + } + } else { + Ok(None) + } +} + +#[derive(Debug, Clone)] +pub enum SseTypeV2 { + SseS3(ServerSideEncryption), + SseKms(ServerSideEncryption, Option), + SseC(SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5), +} + +impl SseTypeV2 { + #[allow(unused)] + pub fn to_metadata(&self) -> HashMap { + sse_configuration_to_metadata(self) + } +} + +pub async fn prepare_sse_configuration_v2( + bucket: &str, + server_side_encryption: Option, + customer_algorithm: Option, + customer_key: Option, + customer_key_md5: Option, + ssekms_key_id: Option, +) -> Result, ApiError> { + if let Some(customer_algorithm) = customer_algorithm + && let Some(customer_key_md5) = customer_key_md5 + { + // if create_multipart_upload request, customer_key is not provided + let customer_key = customer_key.unwrap_or_default(); + + return Ok(Some(SseTypeV2::SseC(customer_algorithm, customer_key, customer_key_md5))); + } + + let sse_config = prepare_sse_configuration(bucket, server_side_encryption, ssekms_key_id).await?; + + if let Some(sse_config) = sse_config { + return match sse_config.effective_sse.as_str() { + ServerSideEncryption::AES256 => Ok(Some(SseTypeV2::SseS3(sse_config.effective_sse))), + ServerSideEncryption::AWS_KMS => { + Ok(Some(SseTypeV2::SseKms(sse_config.effective_sse.clone(), sse_config.effective_kms_key_id))) + } + _ => Ok(None), + }; + } + + Ok(None) +} + +#[allow(unused)] +pub fn sse_configuration_to_metadata(sse_configuration: &SseTypeV2) -> HashMap { + let mut metadata = HashMap::new(); + match sse_configuration { + SseTypeV2::SseS3(sse) => { + metadata.insert("x-amz-server-side-encryption".to_string(), sse.as_str().to_string()); + } + SseTypeV2::SseKms(sse, kms_key_id) => { + metadata.insert("x-amz-server-side-encryption".to_string(), sse.as_str().to_string()); + if let Some(kms_key_id) = kms_key_id { + metadata.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), kms_key_id.to_string()); + } + } + SseTypeV2::SseC(algorithm, _key, key_md5) => { + metadata.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string()); + metadata.insert( + "x-amz-server-side-encryption-customer-algorithm".to_string(), + algorithm.as_str().to_string(), + ); + metadata.insert("x-amz-server-side-encryption-customer-key-md5".to_string(), key_md5.to_string()); + } + } + + metadata +} + +// ============================================================================ +// Core Types - Unified Encryption/Decryption API +// ============================================================================ + +/// Request parameters for unified encryption +#[derive(Debug, Clone)] +pub struct EncryptionRequest<'a> { + /// Bucket name + pub bucket: &'a str, + /// Object key + pub key: &'a str, + /// Server-side encryption algorithm (SSE-S3 or SSE-KMS) + pub server_side_encryption: Option, + /// KMS key ID (for SSE-KMS) + pub ssekms_key_id: Option, + /// SSE-C algorithm (customer-provided key) + pub sse_customer_algorithm: Option, + /// SSE-C key (Base64-encoded) + pub sse_customer_key: Option, + /// SSE-C key MD5 (Base64-encoded) + pub sse_customer_key_md5: Option, + /// Content size (for metadata) + pub content_size: i64, + + /// Part number (for multipart upload, None for single-part) + pub part_number: Option, + pub part_key: Option, + pub part_nonce: Option, +} + +impl EncryptionRequest<'_> { + pub fn check_upload_part_customer_key_md5( + &self, + user_defined: &HashMap, + customer_key_md5: Option, + ) -> Result<(), ApiError> { + if let Some(customer_key_md5) = customer_key_md5 { + // if customer_key_md5 is provided, check if it matches the metadata + let customer_key_md5_from_metadata = user_defined.get("x-amz-server-side-encryption-customer-key-md5"); + if let Some(customer_key_md5_from_metadata) = customer_key_md5_from_metadata + && !customer_key_md5_from_metadata.eq_ignore_ascii_case(customer_key_md5.as_str()) + { + return Err(ApiError::from(StorageError::other("Customer key MD5 mismatch"))); + } + } + + Ok(()) + } +} + +/// Request parameters for unified decryption +#[derive(Debug)] +pub struct DecryptionRequest<'a> { + /// Bucket name + pub bucket: &'a str, + /// Object key + pub key: &'a str, + /// Object metadata containing encryption headers + pub metadata: &'a HashMap, + /// SSE-C key (Base64-encoded) - required if object was encrypted with SSE-C + pub sse_customer_key: Option<&'a SSECustomerKey>, + /// SSE-C key MD5 (Base64-encoded) - required if object was encrypted with SSE-C + pub sse_customer_key_md5: Option<&'a SSECustomerKeyMD5>, + /// Part number (for multipart upload, None for single-part) + pub part_number: Option, + /// Parts information for multipart objects + pub parts: &'a [ObjectPartInfo], +} + +/// Unified encryption material returned by `apply_encryption()` +#[derive(Debug)] +pub struct EncryptionMaterial { + #[allow(unused)] + pub sse_type: SSEType, + pub server_side_encryption: ServerSideEncryption, + pub kms_key_id: Option, + + #[allow(unused)] + pub algorithm: SSECustomerAlgorithm, + + /// Encryption key bytes + pub key_bytes: [u8; 32], + /// Nonce/IV for encryption + pub nonce: [u8; 12], + /// Metadata to store with the object + pub metadata: HashMap, +} + +/// Unified decryption material returned by `apply_decryption()` +#[derive(Debug)] +pub struct DecryptionMaterial { + #[allow(unused)] + pub sse_type: SSEType, + pub server_side_encryption: ServerSideEncryption, + pub kms_key_id: Option, + pub algorithm: SSECustomerAlgorithm, + pub customer_key_md5: Option, // if use SSE-C, check key md5 + + /// Decryption key bytes + pub key_bytes: [u8; 32], + /// Nonce/IV for decryption + pub nonce: [u8; 12], + /// Original unencrypted size (if available) + pub original_size: Option, + + /// Whether this is a multipart object + pub is_multipart: bool, + /// Part information for multipart objects + pub parts: Vec, +} + +/// Type of encryption used +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SSEType { + /// SSE-S3 (AES256) + SseS3, + /// SSE-KMS (aws:kms) + SseKms, + /// SSE-C (customer-provided key) + SseC, +} + +impl EncryptionMaterial { + /// Wrap a reader with encryption + pub fn wrap_reader(&self, reader: R) -> Box> + where + R: Reader + 'static, + { + Box::new(EncryptReader::new(reader, self.key_bytes, self.nonce)) + } +} + +impl DecryptionMaterial { + /// Wrap a reader with decryption + /// For multipart objects, use `wrap_multipart_stream` instead + pub fn wrap_single_reader(&self, reader: R) -> Box> + where + R: Reader + 'static, + { + Box::new(DecryptReader::new(reader, self.key_bytes, self.nonce)) + } + + /// Wrap a stream with multipart decryption + /// Returns the decrypted reader and the total plaintext size + pub async fn wrap_multipart_stream( + &self, + encrypted_stream: Box, + ) -> Result<(Box, i64), StorageError> { + decrypt_multipart_managed_stream(encrypted_stream, &self.parts, self.key_bytes, self.nonce).await + } + + /// Unified method to wrap stream with decryption and hard limit + /// Handles both single-part and multipart objects, applies decryption and size limiting + /// Accepts AsyncRead stream (from object storage) and returns (decrypted_reader, plaintext_size) + pub async fn wrap_reader( + self, + stream: Box, + actual_size: i64, + ) -> Result<(Box, i64), StorageError> { + let (mut final_stream, response_content_length): (Box, i64) = if self.is_multipart { + // Multipart decryption + let (decrypted_reader, plain_size) = self.wrap_multipart_stream(stream).await?; + (decrypted_reader, plain_size) + } else { + // Single-part decryption - wrap AsyncRead into Reader first + let warp_reader = WarpReader::new(stream); + let decrypt_reader = self.wrap_single_reader(warp_reader); + let plain_size = self.original_size.unwrap_or(actual_size); + (decrypt_reader, plain_size) + }; + + // Add hard limit reader to prevent over-reading + // final_stream is already Box, no need to wrap with WarpReader + let limit_reader = HardLimitReader::new(final_stream, response_content_length); + final_stream = Box::new(limit_reader); + + debug!( + "{:?} decryption applied: plaintext_size={}, encrypted_size={}", + self.sse_type, response_content_length, actual_size + ); + + Ok((final_stream, response_content_length)) + } +} + +// ============================================================================ +// Core API - Unified Encryption/Decryption Entry Points +// ============================================================================ + +/// **Core API**: Apply encryption based on request parameters +/// +/// This function automatically routes to the appropriate encryption method: +/// - SSE-C if customer key is provided +/// - SSE-S3/SSE-KMS if server-side encryption is requested +/// - None if no encryption is requested +/// +/// # Arguments +/// * `request` - Encryption request with all possible encryption parameters +/// +/// # Returns +/// * `Ok(Some(material))` - Encryption should be applied with the returned material +/// * `Ok(None)` - No encryption requested +/// * `Err` - Encryption configuration error +/// +/// # Example +/// ```rust,ignore +/// let request = EncryptionRequest { +/// bucket: &bucket, +/// key: &key, +/// server_side_encryption: effective_sse.as_ref(), +/// ssekms_key_id: effective_kms_key_id.as_deref(), +/// sse_customer_algorithm: sse_customer_algorithm.as_ref(), +/// sse_customer_key: sse_customer_key.as_deref(), +/// sse_customer_key_md5: sse_customer_key_md5.as_deref(), +/// content_size: actual_size, +/// part_number: None, +/// }; +/// +/// if let Some(material) = apply_encryption(request).await? { +/// reader = material.wrap_reader(reader)?; +/// metadata.extend(material.metadata); +/// } +/// ``` +pub async fn sse_encryption(request: EncryptionRequest<'_>) -> Result, ApiError> { + // Priority 1: SSE-C (customer-provided key) + if let (Some(algorithm), Some(key), Some(key_md5)) = + (request.sse_customer_algorithm, request.sse_customer_key, request.sse_customer_key_md5) + { + return apply_ssec_encryption_material( + request.bucket, + request.key, + algorithm, + key, + key_md5, + request.content_size, + request.part_number, + ) + .await + .map(Some); + } + + // Priority 2: Managed SSE (SSE-S3 or SSE-KMS) + let sse_config = prepare_sse_configuration(request.bucket, request.server_side_encryption, request.ssekms_key_id).await?; + + if let Some(sse_config) = sse_config + && is_managed_sse(&sse_config.effective_sse) + { + return apply_managed_encryption_material( + request.bucket, + request.key, + sse_config.effective_sse, + sse_config.effective_kms_key_id, + request.content_size, + request.part_number, + request.part_key, + request.part_nonce, + ) + .await + .map(Some); + } + + // No encryption requested + Ok(None) +} + +/// **Core API**: Apply encryption based on request parameters +/// +/// sse_prepare_encryption, support SSE-C, SSE-S3, SSE-KMS +pub struct PrepareEncryptionRequest<'a> { + /// Bucket name + pub bucket: &'a str, + /// Object key + pub key: &'a str, + /// Server-side encryption algorithm (SSE-S3 or SSE-KMS) + pub server_side_encryption: Option, + /// KMS key ID (for SSE-KMS) + pub ssekms_key_id: Option, + /// SSE-C algorithm (customer-provided key) + pub sse_customer_algorithm: Option, + /// SSE-C key MD5 (Base64-encoded) + pub sse_customer_key_md5: Option, +} + +pub async fn sse_prepare_encryption(request: PrepareEncryptionRequest<'_>) -> Result, ApiError> { + let sse_type = prepare_sse_configuration_v2( + request.bucket, + request.server_side_encryption, + request.sse_customer_algorithm, + None, + request.sse_customer_key_md5, + request.ssekms_key_id, + ) + .await?; + + // apply encryption material + let material = match sse_type { + Some(SseTypeV2::SseS3(sse)) => { + apply_managed_encryption_material(request.bucket, request.key, sse, None, 0, None, None, None).await? + } + Some(SseTypeV2::SseKms(sse, kms_key_id)) => { + apply_managed_encryption_material(request.bucket, request.key, sse, kms_key_id, 0, None, None, None).await? + } + Some(SseTypeV2::SseC(algorithm, _, key_md5)) => apply_ssec_prepare_encryption_material(algorithm, key_md5).await?, + None => return Ok(None), + }; + + Ok(Some(material)) +} + +/// **Core API**: Apply decryption based on stored metadata +/// +/// This function automatically detects the encryption type from metadata: +/// - SSE-C if customer key is provided +/// - SSE-S3/SSE-KMS if managed encryption metadata is found +/// - None if object is not encrypted +/// +/// # Arguments +/// * `request` - Decryption request with metadata and optional customer key +/// +/// # Returns +/// * `Ok(Some(material))` - Decryption should be applied with the returned material +/// * `Ok(None)` - Object is not encrypted +/// * `Err` - Decryption configuration error or key mismatch +/// +/// # Example +/// ```rust,ignore +/// let request = DecryptionRequest { +/// bucket: &bucket, +/// key: &key, +/// metadata: &metadata, +/// sse_customer_key: sse_customer_key.as_deref(), +/// sse_customer_key_md5: sse_customer_key_md5.as_deref(), +/// part_number: None, +/// }; +/// +/// if let Some(material) = apply_decryption(request).await? { +/// reader = material.wrap_reader(reader)?; +/// } +/// ``` +pub async fn sse_decryption(request: DecryptionRequest<'_>) -> Result, ApiError> { + let is_multipart = request.parts.len() > 1; + + // Check for SSE-C encryption + if request + .metadata + .contains_key("x-amz-server-side-encryption-customer-algorithm") + { + let (key, key_md5) = match (request.sse_customer_key, request.sse_customer_key_md5) { + (Some(k), Some(md5)) => (k, md5), + _ => { + return Err(ApiError::from(StorageError::other( + "Object is encrypted with SSE-C but no customer key provided", + ))); + } + }; + + // Verify that the provided key MD5 matches the stored MD5 for security + let stored_md5 = request.metadata.get("x-amz-server-side-encryption-customer-key-md5"); + verify_ssec_key_match(key_md5, stored_md5)?; + + let mut material = + apply_ssec_decryption_material(request.bucket, request.key, request.metadata, key, key_md5, request.part_number) + .await?; + material.is_multipart = is_multipart; + material.parts = request.parts.to_vec(); + material.customer_key_md5 = Some(key_md5.clone()); + + return Ok(Some(material)); + } + + // Check for managed SSE encryption + if request.metadata.contains_key("x-rustfs-encryption-key") { + let mut material_opt = + apply_managed_decryption_material(request.bucket, request.key, request.metadata, request.part_number).await?; + if let Some(ref mut material) = material_opt { + material.is_multipart = is_multipart; + material.parts = request.parts.to_vec(); + } + return Ok(material_opt); + } + + // No encryption detected + Ok(None) +} + +// ============================================================================ +// Internal Implementation - SSE-C +// ============================================================================ + +async fn apply_ssec_prepare_encryption_material( + algorithm: SSECustomerAlgorithm, + sse_key_md5: SSECustomerKeyMD5, +) -> Result { + // Build metadata + let mut metadata = HashMap::new(); + + metadata.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string()); + metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), algorithm.clone()); + metadata.insert("x-amz-server-side-encryption-customer-key-md5".to_string(), sse_key_md5); + + Ok(EncryptionMaterial { + sse_type: SSEType::SseC, + server_side_encryption: ServerSideEncryption::AES256.parse().unwrap(), + kms_key_id: None, + algorithm, + key_bytes: [0; 32], + nonce: [0; 12], + metadata, + }) +} + +async fn apply_ssec_encryption_material( + bucket: &str, + key: &str, + algorithm: SSECustomerAlgorithm, + sse_key: SSECustomerKey, + sse_key_md5: SSECustomerKeyMD5, + content_size: i64, + part_number: Option, +) -> Result { + let params = SsecParams { + algorithm, + key: sse_key.to_string(), + key_md5: sse_key_md5, + }; + + let validated = validate_ssec_params(params)?; + + // Generate nonce (deterministic for SSE-C) + let base_nonce = generate_ssec_nonce(bucket, key); + let nonce = if let Some(part_num) = part_number { + derive_part_nonce(base_nonce, part_num) + } else { + base_nonce + }; + + // Build metadata + let mut metadata = HashMap::new(); + + metadata.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string()); + metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), validated.algorithm.clone()); + metadata.insert("x-amz-server-side-encryption-customer-key-md5".to_string(), validated.key_md5.clone()); + metadata.insert( + "x-amz-server-side-encryption-customer-original-size".to_string(), + content_size.to_string(), + ); + + Ok(EncryptionMaterial { + sse_type: SSEType::SseC, + server_side_encryption: ServerSideEncryption::AES256.parse().unwrap(), + kms_key_id: None, + algorithm: validated.algorithm, + key_bytes: validated.key_bytes, + nonce, + metadata, + }) +} + +async fn apply_ssec_decryption_material( + bucket: &str, + key: &str, + metadata: &HashMap, + sse_key: &str, + sse_key_md5: &str, + part_number: Option, +) -> Result { + // Validate provided key + let algorithm = metadata + .get("x-amz-server-side-encryption-customer-algorithm") + .map(|s| s.as_str()) + .unwrap_or("AES256"); + + let params = SsecParams { + algorithm: algorithm.to_string(), + key: sse_key.to_string(), + key_md5: sse_key_md5.to_string(), + }; + + let validated = validate_ssec_params(params)?; + + // Generate nonce (same as encryption) + let base_nonce = generate_ssec_nonce(bucket, key); + let nonce = if let Some(part_num) = part_number { + derive_part_nonce(base_nonce, part_num) + } else { + base_nonce + }; + + let original_size = metadata + .get("x-amz-server-side-encryption-customer-original-size") + .and_then(|s| s.parse::().ok()); + + Ok(DecryptionMaterial { + sse_type: SSEType::SseC, + server_side_encryption: ServerSideEncryption::AES256.parse().unwrap(), // const + kms_key_id: None, + algorithm: SSECustomerAlgorithm::from(algorithm), + + customer_key_md5: None, + key_bytes: validated.key_bytes, + nonce, + original_size, + + is_multipart: false, + parts: Vec::new(), + }) +} + +// ============================================================================ +// Internal Implementation - Managed SSE (SSE-S3 / SSE-KMS) +// ============================================================================ + +#[allow(clippy::too_many_arguments)] +async fn apply_managed_encryption_material( + bucket: &str, + key: &str, + server_side_encryption: ServerSideEncryption, + kms_key_id: Option, + content_size: i64, + part_number: Option, + part_key: Option, + part_nonce: Option, +) -> Result { + // For multipart, we only generate keys at CompleteMultipartUpload + // During UploadPart, we use the same base nonce with incremented counter + // This is handled externally, so here we just generate the base material + + if !is_managed_sse(&server_side_encryption) { + return Err(ApiError::from(StorageError::other(format!( + "Unsupported server-side encryption: {}", + server_side_encryption.as_str() + )))); + } + + let encryption_type = match server_side_encryption.as_str() { + "AES256" => SSEType::SseS3, + "aws:kms" => SSEType::SseKms, + _ => SSEType::SseS3, + }; + + let mut context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string()); + if content_size >= 0 { + context = context.with_size(content_size as u64); + } + + // Determine KMS key ID to use + let mut kms_key_candidate = kms_key_id.clone().map(|s| s.to_string()); + if kms_key_candidate.is_none() { + // Try to get default key from KMS service (if available) + if let Some(service) = get_global_encryption_service().await { + kms_key_candidate = service.get_default_key_id().cloned(); + } + } + + let kms_key_to_use = kms_key_candidate + .clone() + .ok_or_else(|| ApiError::from(StorageError::other("No KMS key available for managed server-side encryption")))?; + + let provider = get_sse_dek_provider().await?; + + let (data_key, encrypted_data_key) = if let Some(part_number) = part_number + && let Some(part_nonce) = part_nonce + && let Some(part_key) = part_key + && part_number >= 1 + // upload_part mode, dek generate by create_multipart_upload + { + let _base_nonce = BASE64_STANDARD + .decode(part_nonce.as_bytes()) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decode nonce: {e}"))))?; + if _base_nonce.len() != 12 { + return Err(ApiError::from(StorageError::other("Invalid encryption nonce length; expected 12 bytes"))); + } + let mut base_nonce_array = [0u8; 12]; + base_nonce_array.copy_from_slice(&_base_nonce[..12]); + let encrypted_data_key = BASE64_STANDARD + .decode(part_key.as_bytes()) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decode data key: {e}"))))?; + let _data_key = provider + .decrypt_sse_dek(encrypted_data_key.as_slice(), &kms_key_to_use) + .await?; + let data_key = DataKey { + plaintext_key: _data_key, + nonce: derive_part_nonce(base_nonce_array, part_number), + }; + + // load original data key from metadata + (data_key, encrypted_data_key) + } else { + // Use factory pattern to get provider (test or production mode) + let (data_key, encrypted_data_key) = provider + .generate_sse_dek(bucket, key, &kms_key_to_use) + .await + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to create data key: {e}"))))?; + (data_key, encrypted_data_key) + }; + + let algorithm = DEFAULT_SSE_ALGORITHM.to_string(); + + let encryption_metadata = EncryptionMetadata { + algorithm: algorithm.clone(), + key_id: kms_key_to_use.clone(), + key_version: 1, + iv: data_key.nonce.to_vec(), + tag: None, + encryption_context: context.encryption_context.clone(), + encrypted_at: jiff::Zoned::now(), + original_size: if content_size >= 0 { content_size as u64 } else { 0 }, + encrypted_data_key, + }; + + // Build metadata headers + let mut metadata = HashMap::new(); + + // Try to use service for metadata formatting if available, otherwise build manually + if let Some(service) = get_global_encryption_service().await { + metadata = service.metadata_to_headers(&encryption_metadata); + } else { + // Manual metadata building for test mode + metadata.insert( + "x-rustfs-encryption-key".to_string(), + BASE64_STANDARD.encode(&encryption_metadata.encrypted_data_key), + ); + metadata.insert("x-rustfs-encryption-iv".to_string(), BASE64_STANDARD.encode(&encryption_metadata.iv)); + metadata.insert("x-rustfs-encryption-algorithm".to_string(), encryption_metadata.algorithm.clone()); + metadata.insert("x-amz-server-side-encryption".to_string(), server_side_encryption.as_str().to_string()); + + // if kms_key is changed, we need to update the metadata + if kms_key_id.is_none() { + metadata.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), kms_key_to_use.clone()); + } + } + + metadata.insert( + "x-rustfs-encryption-original-size".to_string(), + encryption_metadata.original_size.to_string(), + ); + + Ok(EncryptionMaterial { + sse_type: encryption_type, + server_side_encryption, + kms_key_id: Some(kms_key_to_use), + algorithm, + + key_bytes: data_key.plaintext_key, + nonce: data_key.nonce, + metadata, + }) +} + +async fn apply_managed_decryption_material( + _bucket: &str, + _key: &str, + metadata: &HashMap, + part_number: Option, +) -> Result, ApiError> { + if !metadata.contains_key("x-rustfs-encryption-key") || !metadata.contains_key("x-amz-server-side-encryption") { + return Ok(None); + } + + let server_side_encryption = metadata.get("x-amz-server-side-encryption").unwrap().clone(); + + // Parse metadata - try using service if available, otherwise parse manually + let (encrypted_data_key, iv, algorithm) = if let Some(service) = get_global_encryption_service().await { + // Production mode: use service for metadata parsing + let parsed = service + .headers_to_metadata(metadata) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to parse encryption metadata: {e}"))))?; + + if parsed.iv.len() != 12 { + return Err(ApiError::from(StorageError::other("Invalid encryption nonce length; expected 12 bytes"))); + } + + (parsed.encrypted_data_key, parsed.iv, parsed.algorithm) + } else { + // Test mode: parse metadata manually + let encrypted_key_b64 = metadata + .get("x-rustfs-encryption-key") + .ok_or_else(|| ApiError::from(StorageError::other("Missing encrypted key in metadata")))?; + let encrypted_data_key = BASE64_STANDARD + .decode(encrypted_key_b64) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decode encrypted key: {e}"))))?; + + let iv_b64 = metadata + .get("x-rustfs-encryption-iv") + .ok_or_else(|| ApiError::from(StorageError::other("Missing IV in metadata")))?; + let iv = BASE64_STANDARD + .decode(iv_b64) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decode IV: {e}"))))?; + + if iv.len() != 12 { + return Err(ApiError::from(StorageError::other("Invalid encryption nonce length; expected 12 bytes"))); + } + + let algorithm = metadata + .get("x-rustfs-encryption-algorithm") + .cloned() + .unwrap_or_else(|| "AES256".to_string()); + + (encrypted_data_key, iv, algorithm) + }; + + // Extract KMS key ID from metadata (optional, used for provider context) + let kms_key_id = metadata + .get("x-amz-server-side-encryption-aws-kms-key-id") + .cloned() + .unwrap_or_else(|| "default".to_string()); + + // Use factory pattern to get provider (test or production mode) + let provider = get_sse_dek_provider().await?; + let key_bytes = provider + .decrypt_sse_dek(&encrypted_data_key, &kms_key_id) + .await + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decrypt data key: {e}"))))?; + + let mut base_nonce = [0u8; 12]; + base_nonce.copy_from_slice(&iv[..12]); + let nonce = if let Some(part_num) = part_number { + derive_part_nonce(base_nonce, part_num) + } else { + base_nonce + }; + + let original_size = metadata + .get("x-rustfs-encryption-original-size") + .and_then(|s| s.parse::().ok()); + + let encryption_type = match server_side_encryption.as_str() { + ServerSideEncryption::AES256 => SSEType::SseS3, + ServerSideEncryption::AWS_KMS => SSEType::SseKms, + _ => SSEType::SseS3, + }; + + Ok(Some(DecryptionMaterial { + sse_type: encryption_type, + server_side_encryption: ServerSideEncryption::from(server_side_encryption), + kms_key_id: Some(SSEKMSKeyId::from(kms_key_id)), + algorithm, + customer_key_md5: None, + + key_bytes, + nonce, + original_size, + + is_multipart: false, + parts: Vec::new(), + })) +} + +// ============================================================================ +// Legacy Types (for backward compatibility) +// ============================================================================ + +/// Validated SSE-C parameters +#[derive(Debug, Clone)] +pub struct ValidatedSsecParams { + /// Encryption algorithm (always "AES256" for SSE-C) + pub algorithm: SSECustomerAlgorithm, + /// Decoded encryption key bytes (32 bytes for AES-256) + pub key_bytes: [u8; 32], + /// Base64-encoded MD5 of the key + pub key_md5: SSECustomerKeyMD5, +} + +/// SSE-C parameters from client request +#[derive(Debug, Clone)] +pub struct SsecParams { + /// Encryption algorithm + pub algorithm: SSECustomerAlgorithm, + /// Base64-encoded encryption key + pub key: SSECustomerKey, + /// Base64-encoded MD5 of the key + pub key_md5: SSECustomerKeyMD5, +} + +// ============================================================================ +// SSE DEK Provider Abstraction (Factory Pattern) +// ============================================================================ + +/// Trait for SSE data encryption key management +/// Abstracts the source of encryption keys (KMS, test provider, etc.) +#[async_trait] +pub trait SseDekProvider: Send + Sync { + /// Generate an SSE data encryption key + async fn generate_sse_dek(&self, bucket: &str, key: &str, kms_key_id: &str) -> Result<(DataKey, Vec), ApiError>; + + /// Decrypt an SSE data encryption key (returns only plaintext key, nonce should be read from metadata) + async fn decrypt_sse_dek(&self, encrypted_dek: &[u8], kms_key_id: &str) -> Result<[u8; 32], ApiError>; +} + +// ============================================================================ +// Production KMS-backed DEK Provider +// ============================================================================ + +/// Production KMS-backed DEK provider +/// Wraps the global ObjectEncryptionService to provide SSE DEK operations +struct KmsSseDekProvider { + service: Arc, +} + +impl KmsSseDekProvider { + /// Create a new KMS-backed provider + pub async fn new() -> Result { + let service = get_global_encryption_service() + .await + .ok_or_else(|| ApiError::from(StorageError::other("KMS encryption service is not initialized")))?; + Ok(Self { service }) + } +} + +#[async_trait] +impl SseDekProvider for KmsSseDekProvider { + async fn generate_sse_dek(&self, bucket: &str, key: &str, kms_key_id: &str) -> Result<(DataKey, Vec), ApiError> { + let context = ObjectEncryptionContext::new(bucket.to_string(), key.to_string()); + + let kms_key_option = Some(kms_key_id.to_string()); + let (data_key, encrypted_data_key) = self + .service + .create_data_key(&kms_key_option, &context) + .await + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to create data key: {}", e))))?; + + Ok((data_key, encrypted_data_key)) + } + + async fn decrypt_sse_dek(&self, encrypted_dek: &[u8], _kms_key_id: &str) -> Result<[u8; 32], ApiError> { + // Create a minimal context for decryption + let context = ObjectEncryptionContext::new("".to_string(), "".to_string()); + let data_key = self + .service + .decrypt_data_key(encrypted_dek, &context) + .await + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decrypt data key: {}", e))))?; + + Ok(data_key.plaintext_key) + } +} + +// ============================================================================ +// Test/Simple DEK Provider +// ============================================================================ + +/// Simple SSE DEK provider for testing purposes +/// +/// This provider reads a single 32-byte customer master key (CMK) from the +/// `__RUSTFS_SSE_SIMPLE_CMK` environment variable. The key must be base64-encoded. +/// +/// # Environment Variable Format +/// +/// ```text +/// __RUSTFS_SSE_SIMPLE_CMK= +/// ``` +/// +/// Example: +/// ```bash +/// export __RUSTFS_SSE_SIMPLE_CMK="AKHul86TBMMJ3+VrGlh9X3dHJsOtSXOXHOODPwmAnOo=" +/// ``` +/// +/// # Key Generation +/// +/// Use the provided script to generate a valid key: +/// ```bash +/// # Windows +/// .\scripts\generate-sse-keys.ps1 +/// +/// # Linux/Unix/macOS +/// ./scripts/generate-sse-keys.sh +/// ``` +pub(crate) struct TestSseDekProvider { + master_key: [u8; 32], +} + +impl TestSseDekProvider { + /// Create a SimpleSseDekProvider with a predefined key (for testing) + #[cfg(test)] + pub fn new_with_key(master_key: [u8; 32]) -> Self { + Self { master_key } + } + + pub fn new() -> Self { + let cmk_value = std::env::var("__RUSTFS_SSE_SIMPLE_CMK").unwrap_or_else(|_| "".to_string()); + + let master_key = if !cmk_value.is_empty() { + match BASE64_STANDARD.decode(cmk_value.trim()) { + Ok(v) => { + let decoded_len = v.len(); + match v.try_into() { + Ok(arr) => { + println!("✓ Successfully loaded master key (32 bytes)"); + arr + } + Err(_) => { + eprintln!("✗ Failed to load master key: decoded key is not 32 bytes (got {} bytes)", decoded_len); + [0u8; 32] + } + } + } + Err(e) => { + eprintln!("✗ Failed to load master key: invalid base64 encoding: {}", e); + [0u8; 32] + } + } + } else { + [0u8; 32] + }; + + if master_key == [0u8; 32] { + eprintln!("✗ Failed to load master key: no valid master key loaded! All encryption operations will fail."); + eprintln!(" Set __RUSTFS_SSE_SIMPLE_CMK environment variable to a base64-encoded 32-byte key."); + std::process::exit(1); + } + + Self { master_key } + } + + // Simple encryption of DEK + pub(crate) fn encrypt_dek(dek: [u8; 32], cmk_value: [u8; 32]) -> Result { + // Use AES-256-GCM to encrypt DEK + let key = Key::::from(cmk_value); + + let cipher = Aes256Gcm::new(&key); + let nonce = Nonce::from([0u8; 12]); + let ciphertext = cipher + .encrypt(&nonce, dek.as_slice()) + .map_err(|_| ApiError::from(StorageError::other("Failed to encrypt DEK")))?; + + // nonce:ciphertext + Ok(format!("{}:{}", BASE64_STANDARD.encode(nonce), BASE64_STANDARD.encode(ciphertext))) + } + + // Simple decryption of DEK + pub(crate) fn decrypt_dek(encrypted_dek: &str, cmk_value: [u8; 32]) -> Result<[u8; 32], ApiError> { + let parts: Vec<&str> = encrypted_dek.split(':').collect(); + if parts.len() != 2 { + return Err(ApiError::from(StorageError::other("Invalid encrypted DEK format"))); + } + + let nonce_vec = BASE64_STANDARD + .decode(parts[0]) + .map_err(|_| ApiError::from(StorageError::other("Invalid nonce format")))?; + let ciphertext = BASE64_STANDARD + .decode(parts[1]) + .map_err(|_| ApiError::from(StorageError::other("Invalid ciphertext format")))?; + + let key = Key::::from(cmk_value); + let cipher = Aes256Gcm::new(&key); + + let nonce_array: [u8; 12] = nonce_vec + .try_into() + .map_err(|_| ApiError::from(StorageError::other("Invalid nonce length")))?; + let nonce = Nonce::from(nonce_array); + + let plaintext = cipher + .decrypt(&nonce, ciphertext.as_slice()) + .map_err(|e| ApiError::from(StorageError::other(format!("Failed to decrypt DEK: {e}"))))?; + + let dek: [u8; 32] = plaintext + .try_into() + .map_err(|_| ApiError::from(StorageError::other("Decrypted DEK has invalid length")))?; + + Ok(dek) + } +} + +#[async_trait] +impl SseDekProvider for TestSseDekProvider { + async fn generate_sse_dek(&self, _bucket: &str, _key: &str, _kms_key_id: &str) -> Result<(DataKey, Vec), ApiError> { + // Generate a 32-byte array as data key + let mut dek = [0u8; 32]; + rand::rng().fill_bytes(&mut dek); + + // Generate a 12-byte array as IV + let mut nonce = [0u8; 12]; + rand::rng().fill_bytes(&mut nonce); + + // Encrypt data key with master key + let encrypted_dek = Self::encrypt_dek(dek, self.master_key)?; + + // Return data key and IV + Ok(( + DataKey { + plaintext_key: dek, + nonce, + }, + encrypted_dek.into_bytes(), + )) + } + + async fn decrypt_sse_dek(&self, encrypted_dek: &[u8], _kms_key_id: &str) -> Result<[u8; 32], ApiError> { + // Decrypt data key with master key + let encrypted_dek_str = std::str::from_utf8(encrypted_dek) + .map_err(|_| ApiError::from(StorageError::other("Invalid UTF-8 in encrypted DEK")))?; + let dek = Self::decrypt_dek(encrypted_dek_str, self.master_key)?; + Ok(dek) + } +} + +// ============================================================================ +// Factory Function for SSE DEK Provider +// ============================================================================ + +/// Global SSE DEK provider cache +static GLOBAL_SSE_DEK_PROVIDER: OnceLock> = OnceLock::new(); + +/// Get or initialize the global SSE DEK provider +/// +/// Factory function that automatically selects the appropriate provider: +/// - If `__RUSTFS_SSE_SIMPLE_CMK` environment variable exists: use SimpleSseDekProvider (test mode) +/// - Otherwise: use KmsSseDekProvider (production mode with real KMS) +/// +/// # Returns +/// Arc to the global SSE DEK provider instance +/// +/// # Example +/// ```rust,ignore +/// let provider = get_sse_dek_provider().await?; +/// let (data_key, encrypted_dek) = provider +/// .generate_sse_dek("bucket", "key", "kms-key-id") +/// .await?; +/// ``` +pub async fn get_sse_dek_provider() -> Result, ApiError> { + // Check if already initialized + if let Some(provider) = GLOBAL_SSE_DEK_PROVIDER.get() { + return Ok(provider.clone()); + } + + // Determine provider based on environment variable + let provider: Arc = if std::env::var("__RUSTFS_SSE_SIMPLE_CMK").is_ok() { + debug!("Using SimpleSseDekProvider (test mode) based on __RUSTFS_SSE_SIMPLE_CMK"); + Arc::new(TestSseDekProvider::new()) + } else { + debug!("Using KmsSseDekProvider (production mode)"); + Arc::new(KmsSseDekProvider::new().await?) + }; + + // Store in global cache + GLOBAL_SSE_DEK_PROVIDER + .set(provider.clone()) + .map_err(|_| ApiError::from(StorageError::other("Failed to initialize global SSE DEK provider (already set)")))?; + + Ok(provider) +} + +// check encryption metadata +pub fn check_encryption_metadata(metadata: &HashMap) -> bool { + if !metadata.contains_key("x-rustfs-encryption-key") && !metadata.contains_key("x-amz-server-side-encryption") { + return false; + } + + true +} + +/// Reset the global SSE DEK provider (for testing only) +/// +/// Note: OnceLock doesn't support reset in stable Rust. +/// Tests should set environment variables before first call to `get_sse_dek_provider()`. +#[cfg(test)] +#[allow(dead_code)] +pub fn reset_sse_dek_provider() { + // OnceLock doesn't support reset - this is a documentation placeholder + // Consider using arc_swap::ArcSwap if runtime reset is needed +} + +// ============================================================================ +// Legacy Functions (SSE-S3 / SSE-KMS) +// ============================================================================ + +/// Check if the server_side_encryption is a managed SSE type (SSE-S3 or SSE-KMS) +#[inline] +pub fn is_managed_sse(server_side_encryption: &ServerSideEncryption) -> bool { + matches!(server_side_encryption.as_str(), "AES256" | "aws:kms") +} + +/// Strip managed encryption metadata from object metadata +/// +/// Removes all managed SSE-related headers before returning object metadata to client. +/// This is necessary because encryption is transparent to S3 clients. +pub fn strip_managed_encryption_metadata(metadata: &mut HashMap) { + const KEYS: [&str; 7] = [ + "x-amz-server-side-encryption", + "x-amz-server-side-encryption-aws-kms-key-id", + "x-rustfs-encryption-iv", + "x-rustfs-encryption-tag", + "x-rustfs-encryption-key", + "x-rustfs-encryption-context", + "x-rustfs-encryption-original-size", + ]; + + for key in KEYS.iter() { + metadata.remove(*key); + } +} + +// ============================================================================ +// Multipart Encryption Support +// ============================================================================ + +/// Derive a unique nonce for each part in a multipart upload +/// +/// Uses the base nonce and increments the counter portion by part number. +/// This ensures each part has a unique nonce while maintaining determinism. +pub fn derive_part_nonce(base: [u8; 12], part_number: usize) -> [u8; 12] { + let mut nonce = base; + let current = u32::from_be_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]); + let incremented = current.wrapping_add(part_number as u32); + nonce[8..12].copy_from_slice(&incremented.to_be_bytes()); + nonce +} + +pub(crate) async fn decrypt_multipart_managed_stream( + mut encrypted_stream: Box, + parts: &[ObjectPartInfo], + key_bytes: [u8; 32], + base_nonce: [u8; 12], +) -> Result<(Box, i64), StorageError> { + let total_plain_capacity: usize = parts.iter().map(|part| part.actual_size.max(0) as usize).sum(); + + let mut plaintext = Vec::with_capacity(total_plain_capacity); + + for part in parts { + if part.size == 0 { + continue; + } + + let mut encrypted_part = vec![0u8; part.size]; + tokio::io::AsyncReadExt::read_exact(&mut encrypted_stream, &mut encrypted_part) + .await + .map_err(|e| StorageError::other(format!("failed to read encrypted multipart segment {}: {}", part.number, e)))?; + + let part_nonce = derive_part_nonce(base_nonce, part.number); + let cursor = std::io::Cursor::new(encrypted_part); + let mut decrypt_reader = DecryptReader::new(WarpReader::new(cursor), key_bytes, part_nonce); + + tokio::io::AsyncReadExt::read_to_end(&mut decrypt_reader, &mut plaintext) + .await + .map_err(|e| StorageError::other(format!("failed to decrypt multipart segment {}: {}", part.number, e)))?; + } + + let total_plain_size = plaintext.len() as i64; + let reader = Box::new(WarpReader::new(InMemoryAsyncReader::new(plaintext))) as Box; + + Ok((reader, total_plain_size)) +} + +// ============================================================================ +// SSE-C Functions +// ============================================================================ + +/// Validate SSE-C parameters from client request +/// +/// Validates: +/// 1. Algorithm is "AES256" +/// 2. Key is valid Base64 and exactly 32 bytes +/// 3. MD5 hash matches the key +/// +/// # Returns +/// `ValidatedSsecParams` with decoded key bytes +pub fn validate_ssec_params(params: SsecParams) -> Result { + // Validate algorithm + if !SUPPORT_SSE_ALGORITHMS.contains(¶ms.algorithm.as_str()) { + return Err(ApiError::from(StorageError::other(format!( + "Unsupported SSE-C algorithm: {}. Only {} is supported", + params.algorithm, DEFAULT_SSE_ALGORITHM + )))); + } + + // Decode Base64 key + let key_bytes = BASE64_STANDARD.decode(¶ms.key).map_err(|e| { + error!("Failed to decode SSE-C key: {}", e); + ApiError::from(StorageError::other("Invalid SSE-C key: not valid Base64")) + })?; + + // Validate key length (must be 32 bytes for AES-256) + if key_bytes.len() != 32 { + return Err(ApiError::from(StorageError::other(format!( + "SSE-C key must be 32 bytes (256 bits), got {} bytes", + key_bytes.len() + )))); + } + + // Verify MD5 hash + let computed_md5 = BASE64_STANDARD.encode(md5::compute(&key_bytes).0); + if computed_md5 != params.key_md5 { + error!("SSE-C key MD5 mismatch: expected '{}', got '{}'", params.key_md5, computed_md5); + return Err(ApiError::from(StorageError::other("SSE-C key MD5 mismatch"))); + } + + // SAFETY: We validated the length is exactly 32 bytes above + let key_array: [u8; 32] = key_bytes.try_into().expect("key length already validated to be 32 bytes"); + + Ok(ValidatedSsecParams { + algorithm: params.algorithm, + key_bytes: key_array, + key_md5: params.key_md5, + }) +} + +/// Generate deterministic nonce for SSE-C encryption +/// +/// The nonce is derived from the bucket and key to ensure: +/// 1. Same object always gets the same nonce (required for SSE-C) +/// 2. Different objects get different nonces +pub fn generate_ssec_nonce(bucket: &str, key: &str) -> [u8; 12] { + let nonce_source = format!("{bucket}-{key}"); + let nonce_hash = md5::compute(nonce_source.as_bytes()); + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&nonce_hash.0[..12]); + nonce +} + +/// Verify SSE-C key matches the stored metadata +/// +/// Used during GetObject to ensure the client provided the correct key. +pub fn verify_ssec_key_match(provided_md5: &str, stored_md5: Option<&String>) -> Result<(), ApiError> { + match stored_md5 { + Some(stored) if stored == provided_md5 => Ok(()), + Some(stored) => Err(ApiError::from(StorageError::other(format!( + "SSE-C key MD5 mismatch: provided '{}' but expected '{}'", + provided_md5, stored + )))), + None => Err(ApiError::from(StorageError::other("Object has no stored SSE-C key MD5"))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_managed_sse() { + assert!(is_managed_sse(&ServerSideEncryption::from_static("AES256"))); + assert!(is_managed_sse(&ServerSideEncryption::from_static("aws:kms"))); + } + + #[test] + fn test_derive_part_nonce() { + let base = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 10]; + let part1 = derive_part_nonce(base, 1); + let part2 = derive_part_nonce(base, 2); + + // First 8 bytes should be unchanged + assert_eq!(&base[..8], &part1[..8]); + assert_eq!(&base[..8], &part2[..8]); + + // Last 4 bytes should be incremented + assert_ne!(&base[8..], &part1[8..]); + assert_ne!(&part1[8..], &part2[8..]); + } + + #[test] + fn test_generate_ssec_nonce() { + let nonce1 = generate_ssec_nonce("bucket1", "key1"); + let nonce2 = generate_ssec_nonce("bucket1", "key1"); + let nonce3 = generate_ssec_nonce("bucket1", "key2"); + + // Same inputs should produce same nonce + assert_eq!(nonce1, nonce2); + + // Different inputs should produce different nonce + assert_ne!(nonce1, nonce3); + + // Nonce should be exactly 12 bytes + assert_eq!(nonce1.len(), 12); + } + + #[test] + fn test_validate_ssec_params_success() { + let key = BASE64_STANDARD.encode([42u8; 32]); + let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + + let params = SsecParams { + algorithm: "AES256".to_string(), + key, + key_md5, + }; + + let result = validate_ssec_params(params); + assert!(result.is_ok()); + let validated = result.unwrap(); + assert_eq!(validated.key_bytes, [42u8; 32]); + } + + #[test] + fn test_validate_ssec_params_wrong_algorithm() { + let key = BASE64_STANDARD.encode([42u8; 32]); + let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + + let params = SsecParams { + algorithm: "AES128".to_string(), // Wrong algorithm + key, + key_md5, + }; + + let result = validate_ssec_params(params); + assert!(result.is_err()); + } + + #[test] + fn test_validate_ssec_params_wrong_key_length() { + let key = BASE64_STANDARD.encode([42u8; 16]); // Only 16 bytes + let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 16]).0); + + let params = SsecParams { + algorithm: "AES256".to_string(), + key, + key_md5, + }; + + let result = validate_ssec_params(params); + assert!(result.is_err()); + } + + #[test] + fn test_validate_ssec_params_wrong_md5() { + let key = BASE64_STANDARD.encode([42u8; 32]); + let key_md5 = BASE64_STANDARD.encode([99u8; 16]); // Wrong MD5 + + let params = SsecParams { + algorithm: "AES256".to_string(), + key, + key_md5, + }; + + let result = validate_ssec_params(params); + assert!(result.is_err()); + } + + #[test] + fn test_strip_managed_encryption_metadata() { + let mut metadata = HashMap::new(); + metadata.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string()); + metadata.insert("x-rustfs-encryption-key".to_string(), "encrypted_key".to_string()); + metadata.insert("content-type".to_string(), "text/plain".to_string()); + + strip_managed_encryption_metadata(&mut metadata); + + assert!(!metadata.contains_key("x-amz-server-side-encryption")); + assert!(!metadata.contains_key("x-rustfs-encryption-key")); + assert!(metadata.contains_key("content-type")); + } + + #[test] + fn test_verify_ssec_key_match_success() { + let md5 = "test_md5".to_string(); + let result = verify_ssec_key_match("test_md5", Some(&md5)); + assert!(result.is_ok()); + } + + #[test] + fn test_verify_ssec_key_match_mismatch() { + let md5 = "stored_md5".to_string(); + let result = verify_ssec_key_match("provided_md5", Some(&md5)); + assert!(result.is_err()); + } + + #[test] + fn test_verify_ssec_key_match_no_stored() { + let result = verify_ssec_key_match("provided_md5", None); + assert!(result.is_err()); + } + + // ============================================================================ + // Integration Tests - Encrypt/Decrypt with SimpleSseDekProvider + // ============================================================================ + + #[tokio::test] + async fn test_simple_sse_dek_provider_encrypt_decrypt() { + use std::io::Cursor; + use tokio::io::AsyncReadExt; + + // 1. Setup: Create SimpleSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([42u8; 32]); + + // 2. Generate a data encryption key + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; // Key ID is ignored in simple provider + + let (data_key, _encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + // 3. Prepare test data (明文) + let plaintext = b"Hello, World! This is a test message for encryption and decryption."; + println!("Original plaintext: {:?}", String::from_utf8_lossy(plaintext)); + println!("Plaintext length: {} bytes", plaintext.len()); + + // 4. Encrypt with EncryptReader (wrap Cursor with WarpReader) + let plaintext_reader = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader = EncryptReader::new(plaintext_reader, data_key.plaintext_key, data_key.nonce); + + // Read encrypted data + let mut encrypted_data = Vec::new(); + encrypt_reader + .read_to_end(&mut encrypted_data) + .await + .expect("Failed to read encrypted data"); + + println!("Encrypted data length: {} bytes", encrypted_data.len()); + println!( + "First 16 bytes of encrypted data: {:02x?}", + &encrypted_data[..16.min(encrypted_data.len())] + ); + + // Verify encrypted data is different from plaintext + assert_ne!( + &encrypted_data[..plaintext.len()], + plaintext, + "Encrypted data should be different from plaintext" + ); + + // 5. Decrypt with DecryptReader (wrap Cursor with WarpReader) + let encrypted_reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new(encrypted_reader, data_key.plaintext_key, data_key.nonce); + + // Read decrypted data + let mut decrypted_data = Vec::new(); + decrypt_reader + .read_to_end(&mut decrypted_data) + .await + .expect("Failed to read decrypted data"); + + println!("Decrypted data: {:?}", String::from_utf8_lossy(&decrypted_data)); + println!("Decrypted length: {} bytes", decrypted_data.len()); + + // 6. Verify decrypted data matches original plaintext + assert_eq!(decrypted_data, plaintext, "Decrypted data should match original plaintext"); + + println!("✅ Encryption/Decryption test passed!"); + } + + #[tokio::test] + async fn test_simple_sse_dek_provider_encrypt_decrypt_large_data() { + use std::io::Cursor; + use tokio::io::AsyncReadExt; + + // 1. Setup: Create SimpleSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key-large"; + let kms_key_id = "default"; + + let (data_key, _encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + // Create 1MB of test data + let plaintext_size = 1024 * 1024; // 1MB + let plaintext: Vec = (0..plaintext_size).map(|i| (i % 256) as u8).collect(); + println!("Testing with {} bytes of data", plaintext.len()); + + // Encrypt (wrap with WarpReader) + let plaintext_reader = WarpReader::new(Cursor::new(plaintext.clone())); + let mut encrypt_reader = EncryptReader::new(plaintext_reader, data_key.plaintext_key, data_key.nonce); + + let mut encrypted_data = Vec::new(); + encrypt_reader + .read_to_end(&mut encrypted_data) + .await + .expect("Failed to encrypt large data"); + + println!("Encrypted {} bytes to {} bytes", plaintext.len(), encrypted_data.len()); + + // Decrypt (wrap with WarpReader) + let encrypted_reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new(encrypted_reader, data_key.plaintext_key, data_key.nonce); + + let mut decrypted_data = Vec::new(); + decrypt_reader + .read_to_end(&mut decrypted_data) + .await + .expect("Failed to decrypt large data"); + + // Verify + assert_eq!(decrypted_data.len(), plaintext.len(), "Decrypted size should match original"); + assert_eq!(decrypted_data, plaintext, "Decrypted data should match original plaintext"); + + println!("✅ Large data encryption/decryption test passed!"); + } + + #[tokio::test] + async fn test_simple_sse_dek_provider_different_nonces() { + use std::io::Cursor; + use tokio::io::AsyncReadExt; + + // 1. Setup: Create SimpleSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; + + // Generate two different keys (with different nonces) + let (data_key1, _) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK 1"); + + let (data_key2, _) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK 2"); + + // Verify nonces are different + assert_ne!(data_key1.nonce, data_key2.nonce, "Different keys should have different nonces"); + + // Same plaintext + let plaintext = b"Same plaintext"; + + // Encrypt with first key (wrap with WarpReader) + let reader1 = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader1 = EncryptReader::new(reader1, data_key1.plaintext_key, data_key1.nonce); + let mut encrypted1 = Vec::new(); + encrypt_reader1.read_to_end(&mut encrypted1).await.unwrap(); + + // Encrypt with second key (wrap with WarpReader) + let reader2 = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader2 = EncryptReader::new(reader2, data_key2.plaintext_key, data_key2.nonce); + let mut encrypted2 = Vec::new(); + encrypt_reader2.read_to_end(&mut encrypted2).await.unwrap(); + + // Verify ciphertexts are different (due to different nonces/keys) + assert_ne!( + encrypted1, encrypted2, + "Same plaintext with different nonces should produce different ciphertext" + ); + + println!("✅ Different nonces produce different ciphertext - test passed!"); + } + + #[tokio::test] + async fn test_simple_sse_dek_provider_decrypt_with_encrypted_dek() { + use std::io::Cursor; + use tokio::io::AsyncReadExt; + + // 1. Setup: Create SimpleSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; + + // 1. Generate DEK and get encrypted DEK + let (data_key, encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + let original_plaintext_key = data_key.plaintext_key; + let original_nonce = data_key.nonce; + + // 2. Simulate storing encrypted_dek and nonce in metadata + // In real scenario, nonce would be stored separately in metadata + + // 3. Later, decrypt the DEK + let decrypted_plaintext_key = provider + .decrypt_sse_dek(&encrypted_dek, kms_key_id) + .await + .expect("Failed to decrypt DEK"); + + // 4. Verify decrypted key matches original + assert_eq!( + decrypted_plaintext_key, original_plaintext_key, + "Decrypted DEK should match original plaintext key" + ); + + // 5. Use decrypted key to encrypt/decrypt data + let plaintext = b"Test data with decrypted DEK"; + + // Encrypt with original key (wrap with WarpReader) + let reader = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader = EncryptReader::new(reader, original_plaintext_key, original_nonce); + let mut encrypted_data = Vec::new(); + encrypt_reader.read_to_end(&mut encrypted_data).await.unwrap(); + + // Decrypt with recovered key (simulating GET operation) (wrap with WarpReader) + let reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new( + reader, + decrypted_plaintext_key, + original_nonce, // In real scenario, read from metadata + ); + let mut decrypted_data = Vec::new(); + decrypt_reader.read_to_end(&mut decrypted_data).await.unwrap(); + + // Verify + assert_eq!(decrypted_data, plaintext, "Data decrypted with recovered key should match original"); + + println!("✅ Full cycle (generate -> encrypt DEK -> decrypt DEK -> decrypt data) test passed!"); + } + + #[test] + fn test_encryption_type_enum() { + // Test EncryptionType enum + assert_eq!(SSEType::SseS3, SSEType::SseS3); + assert_eq!(SSEType::SseKms, SSEType::SseKms); + assert_eq!(SSEType::SseC, SSEType::SseC); + assert_ne!(SSEType::SseS3, SSEType::SseKms); + + // Test Debug format + let debug_str = format!("{:?}", SSEType::SseKms); + assert!(debug_str.contains("SseKms")); + } +} diff --git a/rustfs/src/storage/sse_test.rs b/rustfs/src/storage/sse_test.rs new file mode 100644 index 00000000..bcc059e5 --- /dev/null +++ b/rustfs/src/storage/sse_test.rs @@ -0,0 +1,250 @@ +// 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. + +#[cfg(test)] +mod tests { + use crate::storage::sse::SseDekProvider; + use crate::storage::sse::TestSseDekProvider; + use rustfs_rio::{DecryptReader, EncryptReader, WarpReader}; + use std::io::Cursor; + use tokio::io::AsyncReadExt; + + /// Test EncryptReader encryption and DecryptReader decryption integration without KMS + /// This test verifies the complete encryption/decryption flow: + /// 1. Create TestSseDekProvider with a test master key + /// 2. Generate data encryption key (DEK) using the provider + /// 3. Encrypt data using EncryptReader + /// 4. Decrypt data using DecryptReader + /// 5. Verify decrypted data matches original plaintext + #[tokio::test] + async fn test_encrypt_reader_decrypt_reader_integration_without_kms() { + // Step 1: Create TestSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([0x42u8; 32]); + + // Step 2: Generate a data encryption key + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; // Key ID is ignored in test provider + + let (data_key, _encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + // Verify data key properties + assert_eq!(data_key.plaintext_key.len(), 32); + assert_eq!(data_key.nonce.len(), 12); + + // Step 3: Prepare test data + let plaintext = b"Hello, World! This is a test message for encryption and decryption."; + println!("Original plaintext: {:?}", String::from_utf8_lossy(plaintext)); + println!("Plaintext length: {} bytes", plaintext.len()); + + // Step 4: Encrypt using EncryptReader (wrap Cursor with WarpReader) + let plaintext_reader = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader = EncryptReader::new(plaintext_reader, data_key.plaintext_key, data_key.nonce); + + // Read encrypted data + let mut encrypted_data = Vec::new(); + encrypt_reader + .read_to_end(&mut encrypted_data) + .await + .expect("Failed to read encrypted data"); + + println!("Encrypted data length: {} bytes", encrypted_data.len()); + println!( + "First 16 bytes of encrypted data: {:02x?}", + &encrypted_data[..16.min(encrypted_data.len())] + ); + + // Verify encrypted data is different from plaintext + assert_ne!( + &encrypted_data[..plaintext.len().min(encrypted_data.len())], + plaintext, + "Encrypted data should be different from plaintext" + ); + + // Step 5: Decrypt using DecryptReader (wrap Cursor with WarpReader) + let encrypted_reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new(encrypted_reader, data_key.plaintext_key, data_key.nonce); + + // Read decrypted data + let mut decrypted_data = Vec::new(); + decrypt_reader + .read_to_end(&mut decrypted_data) + .await + .expect("Failed to read decrypted data"); + + println!("Decrypted data: {:?}", String::from_utf8_lossy(&decrypted_data)); + println!("Decrypted length: {} bytes", decrypted_data.len()); + + // Step 6: Verify decrypted data matches original plaintext + assert_eq!(decrypted_data, plaintext, "Decrypted data should match original plaintext"); + + println!("✅ EncryptReader/DecryptReader integration test passed!"); + } + + /// Test EncryptReader with large data (10MB) without KMS + #[tokio::test] + async fn test_encrypt_reader_large_data_without_kms() { + // Create TestSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([0x42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key-large"; + let kms_key_id = "default"; + + let (data_key, _encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + // Create 1MB of test data + let plaintext_size = 1024 * 1024 * 10; + let plaintext: Vec = (0..plaintext_size).map(|i| (i % 256) as u8).collect(); + println!("Testing with {} bytes of data", plaintext.len()); + + // Encrypt (wrap with WarpReader) + let plaintext_reader = WarpReader::new(Cursor::new(plaintext.clone())); + let mut encrypt_reader = EncryptReader::new(plaintext_reader, data_key.plaintext_key, data_key.nonce); + + let mut encrypted_data = Vec::new(); + encrypt_reader + .read_to_end(&mut encrypted_data) + .await + .expect("Failed to encrypt large data"); + + println!("Encrypted {} bytes to {} bytes", plaintext.len(), encrypted_data.len()); + + // Decrypt (wrap with WarpReader) + let encrypted_reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new(encrypted_reader, data_key.plaintext_key, data_key.nonce); + + let mut decrypted_data = Vec::new(); + decrypt_reader + .read_to_end(&mut decrypted_data) + .await + .expect("Failed to decrypt large data"); + + // Verify + assert_eq!(decrypted_data.len(), plaintext.len(), "Decrypted size should match original"); + assert_eq!(decrypted_data, plaintext, "Decrypted data should match original plaintext"); + + println!("✅ Large data encryption/decryption test passed!"); + } + + /// Test EncryptReader with different nonces produce different ciphertexts + #[tokio::test] + async fn test_encrypt_reader_different_nonces_produce_different_ciphertext() { + // Create TestSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([0x42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; + + // Generate two different keys (with different nonces) + let (data_key1, _) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK 1"); + + let (data_key2, _) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK 2"); + + // Verify nonces are different + assert_ne!(data_key1.nonce, data_key2.nonce, "Different keys should have different nonces"); + + // Same plaintext + let plaintext = b"Same plaintext"; + + // Encrypt with first key (wrap with WarpReader) + let reader1 = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader1 = EncryptReader::new(reader1, data_key1.plaintext_key, data_key1.nonce); + let mut encrypted1 = Vec::new(); + encrypt_reader1.read_to_end(&mut encrypted1).await.unwrap(); + + // Encrypt with second key (wrap with WarpReader) + let reader2 = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader2 = EncryptReader::new(reader2, data_key2.plaintext_key, data_key2.nonce); + let mut encrypted2 = Vec::new(); + encrypt_reader2.read_to_end(&mut encrypted2).await.unwrap(); + + // Verify ciphertexts are different (due to different nonces/keys) + assert_ne!( + encrypted1, encrypted2, + "Same plaintext with different nonces should produce different ciphertext" + ); + + println!("✅ Different nonces produce different ciphertext - test passed!"); + } + + /// Test EncryptReader with decrypted DEK (simulating full cycle) + #[tokio::test] + async fn test_encrypt_reader_with_decrypted_dek() { + // Create TestSseDekProvider with test master key + let provider = TestSseDekProvider::new_with_key([0x42u8; 32]); + + let bucket = "test-bucket"; + let key = "test-key"; + let kms_key_id = "default"; + + // Step 1: Generate DEK and get encrypted DEK + let (data_key, encrypted_dek) = provider + .generate_sse_dek(bucket, key, kms_key_id) + .await + .expect("Failed to generate DEK"); + + let original_plaintext_key = data_key.plaintext_key; + let original_nonce = data_key.nonce; + + // Step 2: Later, decrypt the DEK (simulating GET operation) + let decrypted_plaintext_key = provider + .decrypt_sse_dek(&encrypted_dek, kms_key_id) + .await + .expect("Failed to decrypt DEK"); + + // Step 3: Verify decrypted key matches original + assert_eq!( + decrypted_plaintext_key, original_plaintext_key, + "Decrypted DEK should match original plaintext key" + ); + + // Step 4: Use decrypted key to encrypt/decrypt data + let plaintext = b"Test data with decrypted DEK"; + + // Encrypt with original key (wrap with WarpReader) + let reader = WarpReader::new(Cursor::new(plaintext.to_vec())); + let mut encrypt_reader = EncryptReader::new(reader, original_plaintext_key, original_nonce); + let mut encrypted_data = Vec::new(); + encrypt_reader.read_to_end(&mut encrypted_data).await.unwrap(); + + // Decrypt with recovered key (simulating GET operation) (wrap with WarpReader) + let reader = WarpReader::new(Cursor::new(encrypted_data)); + let mut decrypt_reader = DecryptReader::new( + reader, + decrypted_plaintext_key, + original_nonce, // In real scenario, read from metadata + ); + let mut decrypted_data = Vec::new(); + decrypt_reader.read_to_end(&mut decrypted_data).await.unwrap(); + + // Step 5: Verify + assert_eq!(decrypted_data, plaintext, "Data decrypted with recovered key should match original"); + + println!("✅ Full cycle (generate -> encrypt DEK -> decrypt DEK -> decrypt data) test passed!"); + } +}