feature: support kms && encryt (#573)

* feat(kms): implement key management service with local and vault backends

Signed-off-by: junxiang Mu <1948535941@qq.com>

* feat(kms): enhance security with zeroize for sensitive data and improve key management

Signed-off-by: junxiang Mu <1948535941@qq.com>

* remove Hashi word

Signed-off-by: junxiang Mu <1948535941@qq.com>

* refactor: remove unused request structs from kms handlers

Signed-off-by: junxiang Mu <1948535941@qq.com>

---------

Signed-off-by: junxiang Mu <1948535941@qq.com>
This commit is contained in:
guojidan
2025-09-22 17:53:05 +08:00
committed by GitHub
parent f7e188eee7
commit 9ddf6a011d
59 changed files with 18461 additions and 830 deletions
+503
View File
@@ -0,0 +1,503 @@
// 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.
//! API types for KMS dynamic configuration
use crate::config::{KmsBackend, KmsConfig, VaultAuthMethod};
use crate::service_manager::KmsServiceStatus;
use crate::types::{KeyMetadata, KeyUsage};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
/// Request to configure KMS with Local backend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigureLocalKmsRequest {
/// Directory to store key files
pub key_dir: PathBuf,
/// Master key for encrypting stored keys (optional)
pub master_key: Option<String>,
/// File permissions for key files (octal, optional)
pub file_permissions: Option<u32>,
/// Default master key ID for auto-encryption
pub default_key_id: Option<String>,
/// Operation timeout in seconds
pub timeout_seconds: Option<u64>,
/// Number of retry attempts
pub retry_attempts: Option<u32>,
/// Enable caching
pub enable_cache: Option<bool>,
/// Maximum number of keys to cache
pub max_cached_keys: Option<usize>,
/// Cache TTL in seconds
pub cache_ttl_seconds: Option<u64>,
}
/// Request to configure KMS with Vault backend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigureVaultKmsRequest {
/// Vault server URL
pub address: String,
/// Authentication method
pub auth_method: VaultAuthMethod,
/// Vault namespace (Vault Enterprise, optional)
pub namespace: Option<String>,
/// Transit engine mount path
pub mount_path: Option<String>,
/// KV engine mount path for storing keys
pub kv_mount: Option<String>,
/// Path prefix for keys in KV store
pub key_path_prefix: Option<String>,
/// Skip TLS verification (insecure, for development only)
pub skip_tls_verify: Option<bool>,
/// Default master key ID for auto-encryption
pub default_key_id: Option<String>,
/// Operation timeout in seconds
pub timeout_seconds: Option<u64>,
/// Number of retry attempts
pub retry_attempts: Option<u32>,
/// Enable caching
pub enable_cache: Option<bool>,
/// Maximum number of keys to cache
pub max_cached_keys: Option<usize>,
/// Cache TTL in seconds
pub cache_ttl_seconds: Option<u64>,
}
/// Generic KMS configuration request
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "backend_type", rename_all = "lowercase")]
pub enum ConfigureKmsRequest {
/// Configure with Local backend
Local(ConfigureLocalKmsRequest),
/// Configure with Vault backend
Vault(ConfigureVaultKmsRequest),
}
/// KMS configuration response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigureKmsResponse {
/// Whether configuration was successful
pub success: bool,
/// Status message
pub message: String,
/// New service status
pub status: KmsServiceStatus,
}
/// Request to start KMS service
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartKmsRequest {
/// Whether to force start (restart if already running)
pub force: Option<bool>,
}
/// KMS start response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartKmsResponse {
/// Whether start was successful
pub success: bool,
/// Status message
pub message: String,
/// New service status
pub status: KmsServiceStatus,
}
/// KMS stop response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopKmsResponse {
/// Whether stop was successful
pub success: bool,
/// Status message
pub message: String,
/// New service status
pub status: KmsServiceStatus,
}
/// KMS status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmsStatusResponse {
/// Current service status
pub status: KmsServiceStatus,
/// Current backend type (if configured)
pub backend_type: Option<KmsBackend>,
/// Whether KMS is healthy (if running)
pub healthy: Option<bool>,
/// Configuration summary (if configured)
pub config_summary: Option<KmsConfigSummary>,
}
/// Summary of KMS configuration (without sensitive data)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmsConfigSummary {
/// Backend type
pub backend_type: KmsBackend,
/// Default key ID (if configured)
pub default_key_id: Option<String>,
/// Operation timeout in seconds
pub timeout_seconds: u64,
/// Number of retry attempts
pub retry_attempts: u32,
/// Whether caching is enabled
pub enable_cache: bool,
/// Cache configuration summary
pub cache_summary: Option<CacheSummary>,
/// Backend-specific summary
pub backend_summary: BackendSummary,
}
/// Cache configuration summary
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheSummary {
/// Maximum number of keys to cache
pub max_keys: usize,
/// Cache TTL in seconds
pub ttl_seconds: u64,
/// Whether cache metrics are enabled
pub enable_metrics: bool,
}
/// Backend-specific configuration summary
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "backend_type", rename_all = "lowercase")]
pub enum BackendSummary {
/// Local backend summary
Local {
/// Key directory path
key_dir: PathBuf,
/// Whether master key is configured
has_master_key: bool,
/// File permissions (octal)
file_permissions: Option<u32>,
},
/// Vault backend summary
Vault {
/// Vault server address
address: String,
/// Authentication method type
auth_method_type: String,
/// Namespace (if configured)
namespace: Option<String>,
/// Transit engine mount path
mount_path: String,
/// KV engine mount path
kv_mount: String,
/// Key path prefix
key_path_prefix: String,
},
}
impl From<&KmsConfig> for KmsConfigSummary {
fn from(config: &KmsConfig) -> Self {
let cache_summary = if config.enable_cache {
Some(CacheSummary {
max_keys: config.cache_config.max_keys,
ttl_seconds: config.cache_config.ttl.as_secs(),
enable_metrics: config.cache_config.enable_metrics,
})
} else {
None
};
let backend_summary = match &config.backend_config {
crate::config::BackendConfig::Local(local_config) => BackendSummary::Local {
key_dir: local_config.key_dir.clone(),
has_master_key: local_config.master_key.is_some(),
file_permissions: local_config.file_permissions,
},
crate::config::BackendConfig::Vault(vault_config) => BackendSummary::Vault {
address: vault_config.address.clone(),
auth_method_type: match &vault_config.auth_method {
VaultAuthMethod::Token { .. } => "token".to_string(),
VaultAuthMethod::AppRole { .. } => "approle".to_string(),
},
namespace: vault_config.namespace.clone(),
mount_path: vault_config.mount_path.clone(),
kv_mount: vault_config.kv_mount.clone(),
key_path_prefix: vault_config.key_path_prefix.clone(),
},
};
Self {
backend_type: config.backend.clone(),
default_key_id: config.default_key_id.clone(),
timeout_seconds: config.timeout.as_secs(),
retry_attempts: config.retry_attempts,
enable_cache: config.enable_cache,
cache_summary,
backend_summary,
}
}
}
impl ConfigureLocalKmsRequest {
/// Convert to KmsConfig
pub fn to_kms_config(&self) -> KmsConfig {
KmsConfig {
backend: KmsBackend::Local,
default_key_id: self.default_key_id.clone(),
backend_config: crate::config::BackendConfig::Local(crate::config::LocalConfig {
key_dir: self.key_dir.clone(),
master_key: self.master_key.clone(),
file_permissions: self.file_permissions,
}),
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),
cache_config: crate::config::CacheConfig {
max_keys: self.max_cached_keys.unwrap_or(1000),
ttl: Duration::from_secs(self.cache_ttl_seconds.unwrap_or(3600)),
enable_metrics: true,
},
}
}
}
impl ConfigureVaultKmsRequest {
/// Convert to KmsConfig
pub fn to_kms_config(&self) -> KmsConfig {
KmsConfig {
backend: KmsBackend::Vault,
default_key_id: self.default_key_id.clone(),
backend_config: crate::config::BackendConfig::Vault(crate::config::VaultConfig {
address: self.address.clone(),
auth_method: self.auth_method.clone(),
namespace: self.namespace.clone(),
mount_path: self.mount_path.clone().unwrap_or_else(|| "transit".to_string()),
kv_mount: self.kv_mount.clone().unwrap_or_else(|| "secret".to_string()),
key_path_prefix: self.key_path_prefix.clone().unwrap_or_else(|| "rustfs/kms/keys".to_string()),
tls: if self.skip_tls_verify.unwrap_or(false) {
Some(crate::config::TlsConfig {
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
skip_verify: true,
})
} 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),
cache_config: crate::config::CacheConfig {
max_keys: self.max_cached_keys.unwrap_or(1000),
ttl: Duration::from_secs(self.cache_ttl_seconds.unwrap_or(3600)),
enable_metrics: true,
},
}
}
}
impl ConfigureKmsRequest {
/// Convert to KmsConfig
pub fn to_kms_config(&self) -> KmsConfig {
match self {
ConfigureKmsRequest::Local(req) => req.to_kms_config(),
ConfigureKmsRequest::Vault(req) => req.to_kms_config(),
}
}
}
// ========================================
// Key Management API Types
// ========================================
/// Request to create a new key with optional custom name
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateKeyRequest {
/// Custom key name (optional, will auto-generate UUID if not provided)
pub key_name: Option<String>,
/// Key usage type
pub key_usage: KeyUsage,
/// Key description
pub description: Option<String>,
/// Key policy JSON string
pub policy: Option<String>,
/// Tags for the key
pub tags: HashMap<String, String>,
/// Origin of the key
pub origin: Option<String>,
}
impl Default for CreateKeyRequest {
fn default() -> Self {
Self {
key_name: None,
key_usage: KeyUsage::EncryptDecrypt,
description: None,
policy: None,
tags: HashMap::new(),
origin: None,
}
}
}
/// Response from create key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateKeyResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Created key ID (either custom name or auto-generated UUID)
pub key_id: String,
/// Key metadata
pub key_metadata: KeyMetadata,
}
/// Request to delete a key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteKeyRequest {
/// Key ID to delete
pub key_id: String,
/// Number of days to wait before deletion (7-30 days, optional)
pub pending_window_in_days: Option<u32>,
/// Force immediate deletion (for development/testing only)
pub force_immediate: Option<bool>,
}
/// Response from delete key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteKeyResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key ID that was deleted or scheduled for deletion
pub key_id: String,
/// Deletion date (if scheduled)
pub deletion_date: Option<String>,
}
/// Request to list all keys
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListKeysRequest {
/// Maximum number of keys to return (1-1000)
pub limit: Option<u32>,
/// Pagination marker
pub marker: Option<String>,
}
/// Response from list keys operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListKeysResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// List of key IDs
pub keys: Vec<String>,
/// Whether more keys are available
pub truncated: bool,
/// Next marker for pagination
pub next_marker: Option<String>,
}
/// Request to describe a key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribeKeyRequest {
/// Key ID to describe
pub key_id: String,
}
/// Response from describe key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribeKeyResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key metadata
pub key_metadata: Option<KeyMetadata>,
}
/// Request to cancel key deletion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelKeyDeletionRequest {
/// Key ID to cancel deletion for
pub key_id: String,
}
/// Response from cancel key deletion operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelKeyDeletionResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key ID
pub key_id: String,
}
/// Request to update key description
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateKeyDescriptionRequest {
/// Key ID to update
pub key_id: String,
/// New description
pub description: String,
}
/// Response from update key description operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateKeyDescriptionResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key ID
pub key_id: String,
}
/// Request to add/update key tags
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagKeyRequest {
/// Key ID to tag
pub key_id: String,
/// Tags to add/update
pub tags: HashMap<String, String>,
}
/// Response from tag key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagKeyResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key ID
pub key_id: String,
}
/// Request to remove key tags
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UntagKeyRequest {
/// Key ID to untag
pub key_id: String,
/// Tag keys to remove
pub tag_keys: Vec<String>,
}
/// Response from untag key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UntagKeyResponse {
/// Success flag
pub success: bool,
/// Status message
pub message: String,
/// Key ID
pub key_id: String,
}
+974
View File
@@ -0,0 +1,974 @@
// 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.
//! Local file-based KMS backend implementation
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
use crate::config::KmsConfig;
use crate::config::LocalConfig;
use crate::error::{KmsError, Result};
use crate::types::*;
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, AeadCore, KeyInit, OsRng},
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::fs;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
/// Local KMS client that stores keys in local files
pub struct LocalKmsClient {
config: LocalConfig,
/// In-memory cache of loaded keys for performance
key_cache: RwLock<HashMap<String, MasterKey>>,
/// Master encryption key for encrypting stored keys
master_cipher: Option<Aes256Gcm>,
}
/// Serializable representation of a master key stored on disk
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StoredMasterKey {
key_id: String,
version: u32,
algorithm: String,
usage: KeyUsage,
status: KeyStatus,
description: Option<String>,
metadata: HashMap<String, String>,
created_at: chrono::DateTime<chrono::Utc>,
rotated_at: Option<chrono::DateTime<chrono::Utc>>,
created_by: Option<String>,
/// Encrypted key material (32 bytes for AES-256)
encrypted_key_material: Vec<u8>,
/// Nonce used for encryption
nonce: Vec<u8>,
}
/// 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<u8>,
nonce: Vec<u8>,
encryption_context: HashMap<String, String>,
created_at: chrono::DateTime<chrono::Utc>,
}
impl LocalKmsClient {
/// Create a new local KMS client
pub async fn new(config: LocalConfig) -> Result<Self> {
// Create key directory if it doesn't exist
if !config.key_dir.exists() {
fs::create_dir_all(&config.key_dir).await?;
info!("Created KMS key directory: {:?}", config.key_dir);
}
// Initialize master cipher if master key is provided
let master_cipher = if let Some(ref master_key) = config.master_key {
let key = Self::derive_master_key(master_key)?;
Some(Aes256Gcm::new(&key))
} else {
warn!("No master key provided - stored keys will not be encrypted at rest");
None
};
Ok(Self {
config,
key_cache: RwLock::new(HashMap::new()),
master_cipher,
})
}
/// Derive a 256-bit key from the master key string
fn derive_master_key(master_key: &str) -> Result<Key<Aes256Gcm>> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(master_key.as_bytes());
hasher.update(b"rustfs-kms-local"); // Salt to prevent rainbow tables
let hash = hasher.finalize();
Ok(*Key::<Aes256Gcm>::from_slice(&hash))
}
/// Get the file path for a master key
fn master_key_path(&self, key_id: &str) -> PathBuf {
self.config.key_dir.join(format!("{}.key", key_id))
}
/// Load a master key from disk
async fn load_master_key(&self, key_id: &str) -> Result<MasterKey> {
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 {
let nonce = Nonce::from_slice(&stored_key.nonce);
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
};
Ok(MasterKey {
key_id: stored_key.key_id,
version: stored_key.version,
algorithm: stored_key.algorithm,
usage: stored_key.usage,
status: stored_key.status,
description: stored_key.description,
metadata: stored_key.metadata,
created_at: stored_key.created_at,
rotated_at: stored_key.rotated_at,
created_by: stored_key.created_by,
})
}
/// Save a master key to disk
async fn save_master_key(&self, master_key: &MasterKey, key_material: &[u8]) -> Result<()> {
let key_path = self.master_key_path(&master_key.key_id);
// Encrypt key material if master cipher is available
let (encrypted_key_material, nonce) = if let Some(ref cipher) = self.master_cipher {
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let encrypted = cipher
.encrypt(&nonce, key_material)
.map_err(|e| KmsError::cryptographic_error("encrypt", e.to_string()))?;
(encrypted, nonce.to_vec())
} else {
(key_material.to_vec(), Vec::new())
};
let stored_key = StoredMasterKey {
key_id: master_key.key_id.clone(),
version: master_key.version,
algorithm: master_key.algorithm.clone(),
usage: master_key.usage.clone(),
status: master_key.status.clone(),
description: master_key.description.clone(),
metadata: master_key.metadata.clone(),
created_at: master_key.created_at,
rotated_at: master_key.rotated_at,
created_by: master_key.created_by.clone(),
encrypted_key_material,
nonce,
};
let content = serde_json::to_vec_pretty(&stored_key)?;
// Write to temporary file first, then rename for atomicity
let temp_path = key_path.with_extension("tmp");
fs::write(&temp_path, &content).await?;
// Set file permissions if specified
#[cfg(unix)]
if let Some(permissions) = self.config.file_permissions {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(permissions);
std::fs::set_permissions(&temp_path, perms)?;
}
fs::rename(&temp_path, &key_path).await?;
info!("Saved master key {} to {:?}", master_key.key_id, key_path);
Ok(())
}
/// Generate a random 256-bit key
fn generate_key_material() -> Vec<u8> {
let mut key_material = vec![0u8; 32]; // 256 bits
OsRng.fill_bytes(&mut key_material);
key_material
}
/// Get the actual key material for a master key
async fn get_key_material(&self, key_id: &str) -> Result<Vec<u8>> {
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 {
let nonce = Nonce::from_slice(&stored_key.nonce);
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
};
Ok(key_material)
}
/// Encrypt data using a master key
async fn encrypt_with_master_key(&self, key_id: &str, plaintext: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
// Load the actual master key material
let key_material = self.get_key_material(key_id).await?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_material));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, plaintext)
.map_err(|e| KmsError::cryptographic_error("encrypt", e.to_string()))?;
Ok((ciphertext, nonce.to_vec()))
}
/// Decrypt data using a master key
async fn decrypt_with_master_key(&self, key_id: &str, ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>> {
// Load the actual master key material
let key_material = self.get_key_material(key_id).await?;
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_material));
let nonce = Nonce::from_slice(nonce);
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))?;
Ok(plaintext)
}
}
#[async_trait]
impl KmsClient for LocalKmsClient {
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKey> {
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,
"AES_128" => 16,
_ => return Err(KmsError::unsupported_algorithm(&request.key_spec)),
};
let mut plaintext_key = vec![0u8; key_length];
OsRng.fill_bytes(&mut plaintext_key);
// 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
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(),
encrypted_key: encrypted_key.clone(),
nonce,
encryption_context: request.encryption_context.clone(),
created_at: chrono::Utc::now(),
};
// 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());
info!("Generated data key for master key: {}", request.master_key_id);
Ok(data_key)
}
async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse> {
debug!("Encrypting data with key: {}", request.key_id);
// Verify key exists and is active
let key_info = self.describe_key(&request.key_id, context).await?;
if key_info.status != KeyStatus::Active {
return Err(KmsError::invalid_operation(format!(
"Key {} is not active (status: {:?})",
request.key_id, key_info.status
)));
}
let (ciphertext, _nonce) = self.encrypt_with_master_key(&request.key_id, &request.plaintext).await?;
Ok(EncryptResponse {
ciphertext,
key_id: request.key_id.clone(),
key_version: key_info.version,
algorithm: key_info.algorithm,
})
}
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
debug!("Decrypting data");
// Parse the data key envelope from ciphertext
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 '{}': expected '{}', got '{}'",
key, expected_value, actual_value
)));
}
} else {
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<MasterKey> {
debug!("Creating master key: {}", key_id);
// Check if key already exists
if self.master_key_path(key_id).exists() {
return Err(KmsError::key_already_exists(key_id));
}
// Validate algorithm
if algorithm != "AES_256" {
return Err(KmsError::unsupported_algorithm(algorithm));
}
// Generate key material
let key_material = Self::generate_key_material();
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);
// Save to disk
self.save_master_key(&master_key, &key_material).await?;
// Cache the key
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key.clone());
info!("Created master key: {}", key_id);
Ok(master_key)
}
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
debug!("Describing key: {}", key_id);
// Check cache first
{
let cache = self.key_cache.read().await;
if let Some(master_key) = cache.get(key_id) {
return Ok(master_key.clone().into());
}
}
// Load from disk
let master_key = self.load_master_key(key_id).await?;
// Update cache
{
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key.clone());
}
Ok(master_key.into())
}
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
debug!("Listing keys");
let mut keys = Vec::new();
let limit = request.limit.unwrap_or(100) as usize;
let mut count = 0;
let mut entries = fs::read_dir(&self.config.key_dir).await?;
while let Some(entry) = entries.next_entry().await? {
if count >= limit {
break;
}
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "key") {
if let Some(stem) = path.file_stem() {
if let Some(key_id) = stem.to_str() {
if let Ok(key_info) = self.describe_key(key_id, None).await {
// Apply filters
if let Some(ref status_filter) = request.status_filter {
if &key_info.status != status_filter {
continue;
}
}
if let Some(ref usage_filter) = request.usage_filter {
if &key_info.usage != usage_filter {
continue;
}
}
keys.push(key_info);
count += 1;
}
}
}
}
}
Ok(ListKeysResponse {
keys,
next_marker: None, // Simple implementation without pagination
truncated: false,
})
}
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Enabling key: {}", key_id);
let mut master_key = self.load_master_key(key_id).await?;
master_key.status = KeyStatus::Active;
// 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();
self.save_master_key(&master_key, &key_material).await?;
// Update cache
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key);
info!("Enabled key: {}", key_id);
Ok(())
}
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Disabling key: {}", key_id);
let mut master_key = self.load_master_key(key_id).await?;
master_key.status = KeyStatus::Disabled;
let key_material = Self::generate_key_material();
self.save_master_key(&master_key, &key_material).await?;
// Update cache
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key);
info!("Disabled key: {}", key_id);
Ok(())
}
async fn schedule_key_deletion(
&self,
key_id: &str,
_pending_window_days: u32,
_context: Option<&OperationContext>,
) -> Result<()> {
debug!("Scheduling deletion for key: {}", key_id);
let mut master_key = self.load_master_key(key_id).await?;
master_key.status = KeyStatus::PendingDeletion;
let key_material = Self::generate_key_material();
self.save_master_key(&master_key, &key_material).await?;
// Update cache
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key);
warn!("Scheduled key deletion: {}", key_id);
Ok(())
}
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Canceling deletion for key: {}", key_id);
let mut master_key = self.load_master_key(key_id).await?;
master_key.status = KeyStatus::Active;
let key_material = Self::generate_key_material();
self.save_master_key(&master_key, &key_material).await?;
// Update cache
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key);
info!("Canceled deletion for key: {}", key_id);
Ok(())
}
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKey> {
debug!("Rotating key: {}", key_id);
let mut master_key = self.load_master_key(key_id).await?;
master_key.version += 1;
master_key.rotated_at = Some(chrono::Utc::now());
// Generate new key material
let key_material = Self::generate_key_material();
self.save_master_key(&master_key, &key_material).await?;
// Update cache
let mut cache = self.key_cache.write().await;
cache.insert(key_id.to_string(), master_key.clone());
info!("Rotated key: {}", key_id);
Ok(master_key)
}
async fn health_check(&self) -> Result<()> {
// Check if key directory is accessible
if !self.config.key_dir.exists() {
return Err(KmsError::backend_error("Key directory does not exist"));
}
// Try to read the directory
let _ = fs::read_dir(&self.config.key_dir).await?;
Ok(())
}
fn backend_info(&self) -> BackendInfo {
BackendInfo::new(
"local".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
self.config.key_dir.to_string_lossy().to_string(),
true, // We'll assume healthy for now
)
.with_metadata("key_dir".to_string(), self.config.key_dir.to_string_lossy().to_string())
.with_metadata("encrypted_at_rest".to_string(), self.master_cipher.is_some().to_string())
}
}
/// LocalKmsBackend wraps LocalKmsClient and implements the KmsBackend trait
pub struct LocalKmsBackend {
client: LocalKmsClient,
}
impl LocalKmsBackend {
/// Create a new LocalKmsBackend
pub async fn new(config: KmsConfig) -> Result<Self> {
let local_config = match &config.backend_config {
crate::config::BackendConfig::Local(local_config) => local_config.clone(),
_ => return Err(KmsError::configuration_error("Expected Local backend configuration")),
};
let client = LocalKmsClient::new(local_config).await?;
Ok(Self { client })
}
}
#[async_trait]
impl KmsBackend for LocalKmsBackend {
async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
let key_id = request.key_name.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// Create master key with description directly
let _master_key = {
// Generate key material
let key_material = LocalKmsClient::generate_key_material();
let master_key = MasterKey::new_with_description(
key_id.clone(),
"AES_256".to_string(),
Some("local-kms".to_string()),
request.description.clone(),
);
// Save to disk and cache
self.client.save_master_key(&master_key, &key_material).await?;
let mut cache = self.client.key_cache.write().await;
cache.insert(key_id.clone(), master_key.clone());
master_key
};
let metadata = KeyMetadata {
key_id: key_id.clone(),
key_state: KeyState::Enabled,
key_usage: request.key_usage,
description: request.description,
creation_date: chrono::Utc::now(),
deletion_date: None,
origin: "KMS".to_string(),
key_manager: "CUSTOMER".to_string(),
tags: request.tags,
};
Ok(CreateKeyResponse {
key_id,
key_metadata: metadata,
})
}
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
let encrypt_request = crate::types::EncryptRequest {
key_id: request.key_id.clone(),
plaintext: request.plaintext,
encryption_context: request.encryption_context,
grant_tokens: request.grant_tokens,
};
let response = self.client.encrypt(&encrypt_request, None).await?;
Ok(EncryptResponse {
ciphertext: response.ciphertext,
key_id: response.key_id,
key_version: response.key_version,
algorithm: response.algorithm,
})
}
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
let plaintext = self.client.decrypt(&request, None).await?;
// For simplicity, return basic response - in real implementation would extract more info from ciphertext
Ok(DecryptResponse {
plaintext,
key_id: "unknown".to_string(), // Would be extracted from ciphertext metadata
encryption_algorithm: Some("AES-256-GCM".to_string()),
})
}
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
let generate_request = GenerateKeyRequest {
master_key_id: request.key_id.clone(),
key_spec: request.key_spec.as_str().to_string(),
key_length: Some(request.key_spec.key_size() as u32),
encryption_context: request.encryption_context,
grant_tokens: Vec::new(),
};
let data_key = self.client.generate_data_key(&generate_request, None).await?;
Ok(GenerateDataKeyResponse {
key_id: request.key_id,
plaintext_key: data_key.plaintext.clone().unwrap_or_default(),
ciphertext_blob: data_key.ciphertext.clone(),
})
}
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
let key_info = self.client.describe_key(&request.key_id, None).await?;
let metadata = KeyMetadata {
key_id: key_info.key_id,
key_state: match key_info.status {
KeyStatus::Active => KeyState::Enabled,
KeyStatus::Disabled => KeyState::Disabled,
KeyStatus::PendingDeletion => KeyState::PendingDeletion,
KeyStatus::Deleted => KeyState::Unavailable,
},
key_usage: key_info.usage,
description: key_info.description,
creation_date: key_info.created_at,
deletion_date: None,
origin: "KMS".to_string(),
key_manager: "CUSTOMER".to_string(),
tags: key_info.tags,
};
Ok(DescribeKeyResponse { key_metadata: metadata })
}
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
let response = self.client.list_keys(&request, None).await?;
Ok(response)
}
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
// For local backend, we'll implement immediate deletion by default
// unless a pending window is specified
let key_id = &request.key_id;
// First, load the key from disk to get the master key
let mut master_key = self
.client
.load_master_key(key_id)
.await
.map_err(|_| crate::error::KmsError::key_not_found(format!("Key {} not found", key_id)))?;
let (deletion_date_str, deletion_date_dt) = if request.force_immediate.unwrap_or(false) {
// For immediate deletion, actually delete the key from filesystem
let key_path = self.client.master_key_path(key_id);
tokio::fs::remove_file(&key_path)
.await
.map_err(|e| crate::error::KmsError::internal_error(format!("Failed to delete key file: {}", e)))?;
// Remove from cache
let mut cache = self.client.key_cache.write().await;
cache.remove(key_id);
info!("Immediately deleted key: {}", key_id);
// Return success response for immediate deletion
let key_metadata = KeyMetadata {
key_id: master_key.key_id.clone(),
description: master_key.description.clone(),
key_usage: master_key.usage,
key_state: KeyState::PendingDeletion, // AWS KMS compatibility
creation_date: master_key.created_at,
deletion_date: Some(chrono::Utc::now()),
key_manager: "CUSTOMER".to_string(),
origin: "AWS_KMS".to_string(),
tags: master_key.metadata,
};
return Ok(DeleteKeyResponse {
key_id: key_id.clone(),
deletion_date: None, // No deletion date for immediate deletion
key_metadata,
});
} else {
// Schedule for deletion (default 30 days)
let days = request.pending_window_in_days.unwrap_or(30);
if !(7..=30).contains(&days) {
return Err(crate::error::KmsError::invalid_parameter(
"pending_window_in_days must be between 7 and 30".to_string(),
));
}
let deletion_date = chrono::Utc::now() + chrono::Duration::days(days as i64);
master_key.status = KeyStatus::PendingDeletion;
(Some(deletion_date.to_rfc3339()), 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)
.await
.map_err(|e| crate::error::KmsError::internal_error(format!("Failed to read key file: {}", e)))?;
let stored_key: crate::backends::local::StoredMasterKey = serde_json::from_slice(&content)
.map_err(|e| crate::error::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 {
let nonce = aes_gcm::Nonce::from_slice(&stored_key.nonce);
cipher
.decrypt(nonce, stored_key.encrypted_key_material.as_ref())
.map_err(|e| crate::error::KmsError::cryptographic_error("decrypt", e.to_string()))?
} else {
stored_key.encrypted_key_material
};
self.client.save_master_key(&master_key, &existing_key_material).await?;
// Update cache
let mut cache = self.client.key_cache.write().await;
cache.insert(key_id.to_string(), master_key.clone());
// Convert master_key to KeyMetadata for response
let key_metadata = KeyMetadata {
key_id: master_key.key_id.clone(),
description: master_key.description.clone(),
key_usage: master_key.usage,
key_state: KeyState::PendingDeletion,
creation_date: master_key.created_at,
deletion_date: deletion_date_dt,
key_manager: "CUSTOMER".to_string(),
origin: "AWS_KMS".to_string(),
tags: master_key.metadata,
};
Ok(DeleteKeyResponse {
key_id: key_id.clone(),
deletion_date: deletion_date_str,
key_metadata,
})
}
async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result<CancelKeyDeletionResponse> {
let key_id = &request.key_id;
// Load the key from disk to get the master key
let mut master_key = self
.client
.load_master_key(key_id)
.await
.map_err(|_| crate::error::KmsError::key_not_found(format!("Key {} not found", key_id)))?;
if master_key.status != KeyStatus::PendingDeletion {
return Err(crate::error::KmsError::invalid_key_state(format!(
"Key {} is not pending deletion",
key_id
)));
}
// Cancel the deletion by resetting the state
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?;
// Update cache
let mut cache = self.client.key_cache.write().await;
cache.insert(key_id.to_string(), master_key.clone());
// Convert master_key to KeyMetadata for response
let key_metadata = KeyMetadata {
key_id: master_key.key_id.clone(),
description: master_key.description.clone(),
key_usage: master_key.usage,
key_state: KeyState::Enabled,
creation_date: master_key.created_at,
deletion_date: None,
key_manager: "CUSTOMER".to_string(),
origin: "AWS_KMS".to_string(),
tags: master_key.metadata,
};
Ok(CancelKeyDeletionResponse {
key_id: key_id.clone(),
key_metadata,
})
}
async fn health_check(&self) -> Result<bool> {
self.client.health_check().await.map(|_| true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
async fn create_test_client() -> (LocalKmsClient, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = LocalConfig {
key_dir: temp_dir.path().to_path_buf(),
master_key: Some("test-master-key".to_string()),
file_permissions: Some(0o600),
};
let client = LocalKmsClient::new(config).await.expect("Failed to create client");
(client, temp_dir)
}
#[tokio::test]
async fn test_key_lifecycle() {
let (client, _temp_dir) = create_test_client().await;
let key_id = "test-key";
let algorithm = "AES_256";
// Create key
let master_key = client
.create_key(key_id, algorithm, None)
.await
.expect("Failed to create key");
assert_eq!(master_key.key_id, key_id);
assert_eq!(master_key.algorithm, algorithm);
assert_eq!(master_key.status, KeyStatus::Active);
// Describe key
let key_info = client.describe_key(key_id, None).await.expect("Failed to describe key");
assert_eq!(key_info.key_id, key_id);
assert_eq!(key_info.status, KeyStatus::Active);
// List keys
let list_response = client
.list_keys(&ListKeysRequest::default(), None)
.await
.expect("Failed to list keys");
assert_eq!(list_response.keys.len(), 1);
assert_eq!(list_response.keys[0].key_id, key_id);
// Disable key
client.disable_key(key_id, None).await.expect("Failed to disable key");
let key_info = client.describe_key(key_id, None).await.expect("Failed to describe key");
assert_eq!(key_info.status, KeyStatus::Disabled);
// Enable key
client.enable_key(key_id, None).await.expect("Failed to enable key");
let key_info = client.describe_key(key_id, None).await.expect("Failed to describe key");
assert_eq!(key_info.status, KeyStatus::Active);
}
#[tokio::test]
async fn test_data_key_operations() {
let (client, _temp_dir) = create_test_client().await;
let key_id = "test-key";
client
.create_key(key_id, "AES_256", None)
.await
.expect("Failed to create key");
// Generate data key
let request = GenerateKeyRequest::new(key_id.to_string(), "AES_256".to_string())
.with_context("bucket".to_string(), "test-bucket".to_string());
let data_key = client
.generate_data_key(&request, None)
.await
.expect("Failed to generate data key");
assert!(data_key.plaintext.is_some());
assert!(!data_key.ciphertext.is_empty());
// Decrypt data key
let decrypt_request =
DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string());
let decrypted = client.decrypt(&decrypt_request, None).await.expect("Failed to decrypt");
assert_eq!(decrypted, data_key.plaintext.clone().expect("No plaintext"));
}
#[tokio::test]
async fn test_encryption_operations() {
let (client, _temp_dir) = create_test_client().await;
let key_id = "test-key";
client
.create_key(key_id, "AES_256", None)
.await
.expect("Failed to create key");
let plaintext = b"Hello, World!";
let encrypt_request = EncryptRequest::new(key_id.to_string(), plaintext.to_vec());
// Encrypt
let encrypt_response = client.encrypt(&encrypt_request, None).await.expect("Failed to encrypt");
assert!(!encrypt_response.ciphertext.is_empty());
assert_eq!(encrypt_response.key_id, key_id);
// Note: Direct decryption of encrypt() results is not implemented in this simple version
// In a real implementation, encrypt() would create a different envelope format
}
}
+219
View File
@@ -0,0 +1,219 @@
// 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 backend implementations
use crate::error::Result;
use crate::types::*;
use async_trait::async_trait;
use std::collections::HashMap;
pub mod local;
pub mod vault;
/// Abstract KMS client interface that all backends must implement
#[async_trait]
pub trait KmsClient: Send + Sync {
/// Generate a new data encryption key (DEK)
///
/// Creates a new data key using the specified master key. The returned DataKey
/// contains both the plaintext and encrypted versions of the key.
///
/// # Arguments
/// * `request` - The key generation request
/// * `context` - Optional operation context for auditing
///
/// # Returns
/// Returns a DataKey containing both plaintext and encrypted key material
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKey>;
/// Encrypt data directly using a master key
///
/// Encrypts the provided plaintext using the specified master key.
/// This is different from generate_data_key as it encrypts user data directly.
///
/// # Arguments
/// * `request` - The encryption request containing plaintext and key ID
/// * `context` - Optional operation context for auditing
async fn encrypt(&self, request: &EncryptRequest, context: Option<&OperationContext>) -> Result<EncryptResponse>;
/// Decrypt data using a master key
///
/// Decrypts the provided ciphertext. The KMS automatically determines
/// which key was used for encryption based on the ciphertext metadata.
///
/// # Arguments
/// * `request` - The decryption request containing ciphertext
/// * `context` - Optional operation context for auditing
async fn decrypt(&self, request: &DecryptRequest, context: Option<&OperationContext>) -> Result<Vec<u8>>;
/// Create a new master key
///
/// Creates a new master key in the KMS with the specified ID.
/// Returns an error if a key with the same ID already exists.
///
/// # Arguments
/// * `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<MasterKey>;
/// Get information about a specific key
///
/// Returns metadata and information about the specified key.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn describe_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<KeyInfo>;
/// List available keys
///
/// Returns a paginated list of keys available in the KMS.
///
/// # Arguments
/// * `request` - List request parameters (pagination, filters)
/// * `context` - Optional operation context for auditing
async fn list_keys(&self, request: &ListKeysRequest, context: Option<&OperationContext>) -> Result<ListKeysResponse>;
/// Enable a key
///
/// Enables a previously disabled key, allowing it to be used for cryptographic operations.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn enable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Disable a key
///
/// Disables a key, preventing it from being used for new cryptographic operations.
/// Existing encrypted data can still be decrypted.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn disable_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Schedule key deletion
///
/// Schedules a key for deletion after a specified number of days.
/// This allows for a grace period to recover the key if needed.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `pending_window_days` - Number of days before actual deletion
/// * `context` - Optional operation context for auditing
async fn schedule_key_deletion(
&self,
key_id: &str,
pending_window_days: u32,
context: Option<&OperationContext>,
) -> Result<()>;
/// Cancel key deletion
///
/// Cancels a previously scheduled key deletion.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn cancel_key_deletion(&self, key_id: &str, context: Option<&OperationContext>) -> Result<()>;
/// Rotate a key
///
/// Creates a new version of the specified key. Previous versions remain
/// available for decryption but new operations will use the new version.
///
/// # Arguments
/// * `key_id` - The key identifier
/// * `context` - Optional operation context for auditing
async fn rotate_key(&self, key_id: &str, context: Option<&OperationContext>) -> Result<MasterKey>;
/// Health check
///
/// Performs a health check on the KMS backend to ensure it's operational.
async fn health_check(&self) -> Result<()>;
/// Get backend information
///
/// Returns information about the KMS backend (type, version, etc.).
fn backend_info(&self) -> BackendInfo;
}
/// Simplified KMS backend interface for manager
#[async_trait]
pub trait KmsBackend: Send + Sync {
/// Create a new master key
async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse>;
/// Encrypt data
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse>;
/// Decrypt data
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse>;
/// Generate a data key
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse>;
/// Describe a key
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse>;
/// List keys
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse>;
/// Delete a key
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse>;
/// Cancel key deletion
async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result<CancelKeyDeletionResponse>;
/// Health check
async fn health_check(&self) -> Result<bool>;
}
/// Information about a KMS backend
#[derive(Debug, Clone)]
pub struct BackendInfo {
/// Backend type name (e.g., "local", "vault")
pub backend_type: String,
/// Backend version
pub version: String,
/// Backend endpoint or location
pub endpoint: String,
/// Whether the backend is currently healthy
pub healthy: bool,
/// Additional metadata about the backend
pub metadata: HashMap<String, String>,
}
impl BackendInfo {
/// Create a new backend info
pub fn new(backend_type: String, version: String, endpoint: String, healthy: bool) -> Self {
Self {
backend_type,
version,
endpoint,
healthy,
metadata: HashMap::new(),
}
}
/// Add metadata to the backend info
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
+788
View File
@@ -0,0 +1,788 @@
// 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.
//! Vault-based KMS backend implementation using vaultrs
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
use crate::config::{KmsConfig, VaultConfig};
use crate::error::{KmsError, Result};
use crate::types::*;
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, warn};
use vaultrs::{
client::{VaultClient, VaultClientSettingsBuilder},
kv2,
};
/// Vault KMS client implementation
pub struct VaultKmsClient {
client: VaultClient,
config: VaultConfig,
/// Mount path for the KV engine (typically "kv" or "secret")
kv_mount: String,
/// Path prefix for storing keys
key_path_prefix: String,
}
/// Key data stored in Vault
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VaultKeyData {
/// Key algorithm
algorithm: String,
/// Key usage type
usage: KeyUsage,
/// Key creation timestamp
created_at: chrono::DateTime<chrono::Utc>,
/// Key status
status: KeyStatus,
/// Key version
version: u32,
/// Key description
description: Option<String>,
/// Key metadata
metadata: HashMap<String, String>,
/// Key tags
tags: HashMap<String, String>,
/// Encrypted key material (base64 encoded)
encrypted_key_material: String,
}
impl VaultKmsClient {
/// Create a new Vault KMS client
pub async fn new(config: VaultConfig) -> Result<Self> {
// Create client settings
let mut settings_builder = VaultClientSettingsBuilder::default();
settings_builder.address(&config.address);
// Set authentication token based on method
let token = match &config.auth_method {
crate::config::VaultAuthMethod::Token { token } => token.clone(),
crate::config::VaultAuthMethod::AppRole { .. } => {
// For AppRole authentication, we would need to first authenticate
// and get a token. For simplicity, we'll require a token for now.
return Err(KmsError::backend_error(
"AppRole authentication not yet implemented. Please use token authentication.",
));
}
};
settings_builder.token(&token);
if let Some(namespace) = &config.namespace {
settings_builder.namespace(Some(namespace.clone()));
}
let settings = settings_builder
.build()
.map_err(|e| KmsError::backend_error(format!("Failed to build Vault client settings: {}", e)))?;
let client =
VaultClient::new(settings).map_err(|e| KmsError::backend_error(format!("Failed to create Vault client: {}", e)))?;
info!("Successfully connected to Vault at {}", config.address);
Ok(Self {
client,
kv_mount: config.kv_mount.clone(),
key_path_prefix: config.key_path_prefix.clone(),
config,
})
}
/// Get the full path for a key in Vault
fn key_path(&self, key_id: &str) -> String {
format!("{}/{}", self.key_path_prefix, key_id)
}
/// Generate key material for the given algorithm
fn generate_key_material(algorithm: &str) -> Result<Vec<u8>> {
let key_size = match algorithm {
"AES_256" => 32,
"AES_128" => 16,
_ => return Err(KmsError::unsupported_algorithm(algorithm)),
};
let mut key_material = vec![0u8; key_size];
rand::rng().fill_bytes(&mut key_material);
Ok(key_material)
}
/// Encrypt key material using Vault's transit engine
async fn encrypt_key_material(&self, key_material: &[u8]) -> Result<String> {
// For simplicity, we'll base64 encode the key material
// In a production setup, you would use Vault's transit engine for additional encryption
Ok(general_purpose::STANDARD.encode(key_material))
}
/// Decrypt key material
async fn decrypt_key_material(&self, encrypted_material: &str) -> Result<Vec<u8>> {
// For simplicity, we'll base64 decode the key material
// In a production setup, you would use Vault's transit engine for decryption
general_purpose::STANDARD
.decode(encrypted_material)
.map_err(|e| KmsError::cryptographic_error("decrypt", e.to_string()))
}
/// 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);
kv2::set(&self.client, &self.kv_mount, &path, key_data)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to store key in Vault: {}", e)))?;
debug!("Stored key {} in Vault at path {}", key_id, path);
Ok(())
}
async fn store_key_metadata(&self, key_id: &str, request: &CreateKeyRequest) -> Result<()> {
debug!("Storing key metadata for {}, input tags: {:?}", key_id, request.tags);
let key_data = VaultKeyData {
algorithm: "AES_256".to_string(),
usage: request.key_usage.clone(),
created_at: chrono::Utc::now(),
status: KeyStatus::Active,
version: 1,
description: request.description.clone(),
metadata: HashMap::new(),
tags: request.tags.clone(),
encrypted_key_material: String::new(), // Not used for transit keys
};
debug!("VaultKeyData tags before storage: {:?}", key_data.tags);
self.store_key_data(key_id, &key_data).await
}
/// Retrieve key data from Vault
async fn get_key_data(&self, key_id: &str) -> Result<VaultKeyData> {
let path = self.key_path(key_id);
let secret: VaultKeyData = kv2::read(&self.client, &self.kv_mount, &path).await.map_err(|e| match e {
vaultrs::error::ClientError::ResponseWrapError => KmsError::key_not_found(key_id),
vaultrs::error::ClientError::APIError { code: 404, .. } => KmsError::key_not_found(key_id),
_ => KmsError::backend_error(format!("Failed to read key from Vault: {}", e)),
})?;
debug!("Retrieved key {} from Vault, tags: {:?}", key_id, secret.tags);
Ok(secret)
}
/// List all keys stored in Vault
async fn list_vault_keys(&self) -> Result<Vec<String>> {
// List keys under the prefix
match kv2::list(&self.client, &self.kv_mount, &self.key_path_prefix).await {
Ok(keys) => {
debug!("Found {} keys in Vault", keys.len());
Ok(keys)
}
Err(vaultrs::error::ClientError::ResponseWrapError) => {
// No keys exist yet
Ok(Vec::new())
}
Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => {
// Path doesn't exist - no keys exist yet
debug!("Key path doesn't exist in Vault (404), returning empty list");
Ok(Vec::new())
}
Err(e) => Err(KmsError::backend_error(format!("Failed to list keys in Vault: {}", e))),
}
}
/// Physically delete a key from Vault storage
async fn delete_key(&self, key_id: &str) -> Result<()> {
let path = self.key_path(key_id);
// For this specific key path, we can safely delete the metadata
// since each key has its own unique path under the prefix
kv2::delete_metadata(&self.client, &self.kv_mount, &path)
.await
.map_err(|e| match e {
vaultrs::error::ClientError::APIError { code: 404, .. } => KmsError::key_not_found(key_id),
_ => KmsError::backend_error(format!("Failed to delete key metadata from Vault: {}", e)),
})?;
debug!("Permanently deleted key {} metadata from Vault at path {}", key_id, path);
Ok(())
}
}
#[async_trait]
impl KmsClient for VaultKmsClient {
async fn generate_data_key(&self, request: &GenerateKeyRequest, context: Option<&OperationContext>) -> Result<DataKey> {
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);
// Encrypt the data key with the master key
let encrypted_key = self.encrypt_key_material(&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()))?,
key_spec: request.key_spec.clone(),
metadata: request.encryption_context.clone(),
created_at: chrono::Utc::now(),
})
}
async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result<EncryptResponse> {
debug!("Encrypting data with key: {}", request.key_id);
// Get the master key
let key_data = self.get_key_data(&request.key_id).await?;
let key_material = self.decrypt_key_material(&key_data.encrypted_key_material).await?;
// For simplicity, we'll use a basic encryption approach
// In practice, you'd use proper AEAD encryption
let mut ciphertext = request.plaintext.clone();
for (i, byte) in ciphertext.iter_mut().enumerate() {
*byte ^= key_material[i % key_material.len()];
}
Ok(EncryptResponse {
ciphertext,
key_id: request.key_id.clone(),
key_version: key_data.version,
algorithm: key_data.algorithm,
})
}
async fn decrypt(&self, _request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
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"))
}
async fn create_key(&self, key_id: &str, algorithm: &str, _context: Option<&OperationContext>) -> Result<MasterKey> {
debug!("Creating master key: {} with algorithm: {}", key_id, algorithm);
// Check if key already exists
if self.get_key_data(key_id).await.is_ok() {
return Err(KmsError::key_already_exists(key_id));
}
// Generate key material
let key_material = Self::generate_key_material(algorithm)?;
let encrypted_material = self.encrypt_key_material(&key_material).await?;
// Create key data
let key_data = VaultKeyData {
algorithm: algorithm.to_string(),
usage: KeyUsage::EncryptDecrypt,
created_at: chrono::Utc::now(),
status: KeyStatus::Active,
version: 1,
description: None,
metadata: HashMap::new(),
tags: HashMap::new(),
encrypted_key_material: encrypted_material,
};
// Store in Vault
self.store_key_data(key_id, &key_data).await?;
let master_key = MasterKey {
key_id: key_id.to_string(),
version: key_data.version,
algorithm: key_data.algorithm.clone(),
usage: key_data.usage,
status: key_data.status,
description: None, // This method doesn't receive description parameter
metadata: key_data.metadata.clone(),
created_at: key_data.created_at,
rotated_at: None,
created_by: None,
};
info!("Successfully created master key: {}", key_id);
Ok(master_key)
}
async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<KeyInfo> {
debug!("Describing key: {}", key_id);
let key_data = self.get_key_data(key_id).await?;
Ok(KeyInfo {
key_id: key_id.to_string(),
description: key_data.description,
algorithm: key_data.algorithm,
usage: key_data.usage,
status: key_data.status,
version: key_data.version,
metadata: key_data.metadata,
tags: key_data.tags,
created_at: key_data.created_at,
rotated_at: None,
created_by: None,
})
}
async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result<ListKeysResponse> {
debug!("Listing keys with limit: {:?}", request.limit);
let all_keys = self.list_vault_keys().await?;
let limit = request.limit.unwrap_or(100) as usize;
// Simple pagination implementation
let start_idx = request
.marker
.as_ref()
.and_then(|m| all_keys.iter().position(|k| k == m))
.map(|idx| idx + 1)
.unwrap_or(0);
let end_idx = std::cmp::min(start_idx + limit, all_keys.len());
let keys_page = &all_keys[start_idx..end_idx];
let mut key_infos = Vec::new();
for key_id in keys_page {
if let Ok(key_info) = self.describe_key(key_id, None).await {
key_infos.push(key_info);
}
}
let next_marker = if end_idx < all_keys.len() {
Some(all_keys[end_idx - 1].clone())
} else {
None
};
Ok(ListKeysResponse {
keys: key_infos,
next_marker,
truncated: end_idx < all_keys.len(),
})
}
async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Enabling key: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
key_data.status = KeyStatus::Active;
self.store_key_data(key_id, &key_data).await?;
info!("Enabled key: {}", key_id);
Ok(())
}
async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Disabling key: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
key_data.status = KeyStatus::Disabled;
self.store_key_data(key_id, &key_data).await?;
info!("Disabled key: {}", key_id);
Ok(())
}
async fn schedule_key_deletion(
&self,
key_id: &str,
_pending_window_days: u32,
_context: Option<&OperationContext>,
) -> Result<()> {
debug!("Scheduling key deletion: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
key_data.status = KeyStatus::PendingDeletion;
self.store_key_data(key_id, &key_data).await?;
info!("Scheduled key deletion: {}", key_id);
Ok(())
}
async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> {
debug!("Canceling key deletion: {}", key_id);
let mut key_data = self.get_key_data(key_id).await?;
key_data.status = KeyStatus::Active;
self.store_key_data(key_id, &key_data).await?;
info!("Canceled key deletion: {}", key_id);
Ok(())
}
async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<MasterKey> {
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)?;
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 {
key_id: key_id.to_string(),
version: key_data.version,
algorithm: key_data.algorithm,
usage: key_data.usage,
status: key_data.status,
description: None, // Rotate preserves existing description (would need key lookup)
metadata: key_data.metadata,
created_at: key_data.created_at,
rotated_at: Some(chrono::Utc::now()),
created_by: None,
};
info!("Successfully rotated key: {}", key_id);
Ok(master_key)
}
async fn health_check(&self) -> Result<()> {
debug!("Performing Vault health check");
// Use list_vault_keys but handle the case where no keys exist (which is normal)
match self.list_vault_keys().await {
Ok(_) => {
debug!("Vault health check passed - successfully listed keys");
Ok(())
}
Err(e) => {
// Check if the error is specifically about "no keys found" or 404
let error_msg = e.to_string();
if error_msg.contains("status code 404") || error_msg.contains("No such key") {
debug!("Vault health check passed - 404 error is expected when no keys exist yet");
Ok(())
} else {
warn!("Vault health check failed: {}", e);
Err(e)
}
}
}
}
fn backend_info(&self) -> BackendInfo {
BackendInfo::new("vault".to_string(), "0.1.0".to_string(), self.config.address.clone(), true)
.with_metadata("kv_mount".to_string(), self.kv_mount.clone())
.with_metadata("key_prefix".to_string(), self.key_path_prefix.clone())
}
}
/// VaultKmsBackend wraps VaultKmsClient and implements the KmsBackend trait
pub struct VaultKmsBackend {
client: VaultKmsClient,
}
impl VaultKmsBackend {
/// Create a new VaultKmsBackend
pub async fn new(config: KmsConfig) -> Result<Self> {
let vault_config = match &config.backend_config {
crate::config::BackendConfig::Vault(vault_config) => vault_config.clone(),
_ => return Err(KmsError::configuration_error("Expected Vault backend configuration")),
};
let client = VaultKmsClient::new(vault_config).await?;
Ok(Self { client })
}
/// Update key metadata in Vault storage
async fn update_key_metadata_in_storage(&self, key_id: &str, metadata: &KeyMetadata) -> Result<()> {
// Get the current key data from Vault
let mut key_data = self.client.get_key_data(key_id).await?;
// Update the status based on the new metadata
key_data.status = match metadata.key_state {
KeyState::Enabled => KeyStatus::Active,
KeyState::Disabled => KeyStatus::Disabled,
KeyState::PendingDeletion => KeyStatus::PendingDeletion,
KeyState::Unavailable => KeyStatus::Deleted,
KeyState::PendingImport => KeyStatus::Disabled, // Treat as disabled until import completes
};
// Update the key data in Vault storage
self.client.store_key_data(key_id, &key_data).await?;
Ok(())
}
}
#[async_trait]
impl KmsBackend for VaultKmsBackend {
async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
let key_id = request.key_name.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// Create key in Vault transit engine
let _master_key = self.client.create_key(&key_id, "AES_256", None).await?;
// Also store key metadata in KV store with tags
self.client.store_key_metadata(&key_id, &request).await?;
let metadata = KeyMetadata {
key_id: key_id.clone(),
key_state: KeyState::Enabled,
key_usage: request.key_usage,
description: request.description,
creation_date: chrono::Utc::now(),
deletion_date: None,
origin: "VAULT".to_string(),
key_manager: "VAULT".to_string(),
tags: request.tags,
};
Ok(CreateKeyResponse {
key_id,
key_metadata: metadata,
})
}
async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
let encrypt_request = crate::types::EncryptRequest {
key_id: request.key_id.clone(),
plaintext: request.plaintext,
encryption_context: request.encryption_context,
grant_tokens: request.grant_tokens,
};
let response = self.client.encrypt(&encrypt_request, None).await?;
Ok(EncryptResponse {
ciphertext: response.ciphertext,
key_id: response.key_id,
key_version: response.key_version,
algorithm: response.algorithm,
})
}
async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
let plaintext = self.client.decrypt(&request, None).await?;
Ok(DecryptResponse {
plaintext,
key_id: "unknown".to_string(), // Would be extracted from ciphertext metadata
encryption_algorithm: Some("AES-256-GCM".to_string()),
})
}
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
let generate_request = GenerateKeyRequest {
master_key_id: request.key_id.clone(),
key_spec: request.key_spec.as_str().to_string(),
key_length: Some(request.key_spec.key_size() as u32),
encryption_context: request.encryption_context,
grant_tokens: Vec::new(),
};
let data_key = self.client.generate_data_key(&generate_request, None).await?;
Ok(GenerateDataKeyResponse {
key_id: request.key_id,
plaintext_key: data_key.plaintext.clone().unwrap_or_default(),
ciphertext_blob: data_key.ciphertext.clone(),
})
}
async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
let key_info = self.client.describe_key(&request.key_id, None).await?;
// Also get key metadata from KV store to retrieve tags
let key_data = self.client.get_key_data(&request.key_id).await?;
let metadata = KeyMetadata {
key_id: key_info.key_id,
key_state: match key_info.status {
KeyStatus::Active => KeyState::Enabled,
KeyStatus::Disabled => KeyState::Disabled,
KeyStatus::PendingDeletion => KeyState::PendingDeletion,
KeyStatus::Deleted => KeyState::Unavailable,
},
key_usage: key_info.usage,
description: key_info.description,
creation_date: key_info.created_at,
deletion_date: None,
origin: "VAULT".to_string(),
key_manager: "VAULT".to_string(),
tags: key_data.tags,
};
Ok(DescribeKeyResponse { key_metadata: metadata })
}
async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
let response = self.client.list_keys(&request, None).await?;
Ok(response)
}
async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
// For Vault backend, we'll mark keys for deletion but not physically delete them
// This allows for recovery during the pending window
let key_id = &request.key_id;
// First, check if the key exists and get its metadata
let describe_request = DescribeKeyRequest { key_id: key_id.clone() };
let mut key_metadata = match self.describe_key(describe_request).await {
Ok(response) => response.key_metadata,
Err(_) => {
return Err(crate::error::KmsError::key_not_found(format!("Key {} not found", key_id)));
}
};
let deletion_date = if request.force_immediate.unwrap_or(false) {
// Check if key is already in PendingDeletion state
if key_metadata.key_state == KeyState::PendingDeletion {
// Force immediate deletion: physically delete the key from Vault storage
self.client.delete_key(key_id).await?;
// Return empty deletion_date to indicate key was permanently deleted
None
} else {
// For non-pending keys, mark as PendingDeletion
key_metadata.key_state = KeyState::PendingDeletion;
key_metadata.deletion_date = Some(chrono::Utc::now());
// Update the key metadata in Vault storage to reflect the new state
self.update_key_metadata_in_storage(key_id, &key_metadata).await?;
None
}
} else {
// Schedule for deletion (default 30 days)
let days = request.pending_window_in_days.unwrap_or(30);
if !(7..=30).contains(&days) {
return Err(crate::error::KmsError::invalid_parameter(
"pending_window_in_days must be between 7 and 30".to_string(),
));
}
let deletion_date = chrono::Utc::now() + chrono::Duration::days(days as i64);
key_metadata.key_state = KeyState::PendingDeletion;
key_metadata.deletion_date = Some(deletion_date);
// Update the key metadata in Vault storage to reflect the new state
self.update_key_metadata_in_storage(key_id, &key_metadata).await?;
Some(deletion_date.to_rfc3339())
};
Ok(DeleteKeyResponse {
key_id: key_id.clone(),
deletion_date,
key_metadata,
})
}
async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result<CancelKeyDeletionResponse> {
let key_id = &request.key_id;
// Check if the key exists and is pending deletion
let describe_request = DescribeKeyRequest { key_id: key_id.clone() };
let mut key_metadata = match self.describe_key(describe_request).await {
Ok(response) => response.key_metadata,
Err(_) => {
return Err(crate::error::KmsError::key_not_found(format!("Key {} not found", key_id)));
}
};
if key_metadata.key_state != KeyState::PendingDeletion {
return Err(crate::error::KmsError::invalid_key_state(format!(
"Key {} is not pending deletion",
key_id
)));
}
// Cancel the deletion by resetting the state
key_metadata.key_state = KeyState::Enabled;
key_metadata.deletion_date = None;
Ok(CancelKeyDeletionResponse {
key_id: key_id.clone(),
key_metadata,
})
}
async fn health_check(&self) -> Result<bool> {
self.client.health_check().await.map(|_| true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{VaultAuthMethod, VaultConfig};
#[tokio::test]
#[ignore] // Requires a running Vault instance
async fn test_vault_client_integration() {
let config = VaultConfig {
address: "http://127.0.0.1:8200".to_string(),
auth_method: VaultAuthMethod::Token {
token: "dev-only-token".to_string(),
},
kv_mount: "secret".to_string(),
key_path_prefix: "rustfs/kms/keys".to_string(),
mount_path: "transit".to_string(),
namespace: None,
tls: None,
};
let client = VaultKmsClient::new(config).await.expect("Failed to create Vault client");
// Test key operations
let key_id = "test-key-vault";
let master_key = client
.create_key(key_id, "AES_256", None)
.await
.expect("Failed to create key");
assert_eq!(master_key.key_id, key_id);
assert_eq!(master_key.algorithm, "AES_256");
// Test key description
let key_info = client.describe_key(key_id, None).await.expect("Failed to describe key");
assert_eq!(key_info.key_id, key_id);
// Test data key generation
let data_key_request = GenerateKeyRequest {
master_key_id: key_id.to_string(),
key_spec: "AES_256".to_string(),
key_length: Some(32),
encryption_context: Default::default(),
grant_tokens: Vec::new(),
};
let data_key = client
.generate_data_key(&data_key_request, None)
.await
.expect("Failed to generate data key");
assert!(data_key.plaintext.is_some());
assert!(!data_key.ciphertext.is_empty());
// Test health check
client.health_check().await.expect("Health check failed");
}
}
+255
View File
@@ -0,0 +1,255 @@
// 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.
//! Caching layer for KMS operations to improve performance
use crate::types::{KeyMetadata, KeySpec};
use moka::future::Cache;
use std::time::Duration;
/// Cached data key entry
#[derive(Clone, Debug)]
pub struct CachedDataKey {
pub plaintext: Vec<u8>,
pub ciphertext: Vec<u8>,
pub key_spec: KeySpec,
}
/// KMS cache for storing frequently accessed keys and metadata
pub struct KmsCache {
key_metadata_cache: Cache<String, KeyMetadata>,
data_key_cache: Cache<String, CachedDataKey>,
}
impl KmsCache {
/// Create a new KMS cache with the specified capacity
pub fn new(capacity: u64) -> Self {
Self {
key_metadata_cache: Cache::builder()
.max_capacity(capacity / 2)
.time_to_live(Duration::from_secs(300)) // 5 minutes default TTL
.build(),
data_key_cache: Cache::builder()
.max_capacity(capacity / 2)
.time_to_live(Duration::from_secs(60)) // 1 minute for data keys (shorter for security)
.build(),
}
}
/// Get key metadata from cache
pub async fn get_key_metadata(&self, key_id: &str) -> Option<KeyMetadata> {
self.key_metadata_cache.get(key_id).await
}
/// Put key metadata into cache
pub async fn put_key_metadata(&mut self, key_id: &str, metadata: &KeyMetadata) {
self.key_metadata_cache.insert(key_id.to_string(), metadata.clone()).await;
self.key_metadata_cache.run_pending_tasks().await;
}
/// Get data key from cache
pub async fn get_data_key(&self, key_id: &str) -> Option<CachedDataKey> {
self.data_key_cache.get(key_id).await
}
/// Put data key into cache
pub async fn put_data_key(&mut self, key_id: &str, plaintext: &[u8], ciphertext: &[u8]) {
let cached_key = CachedDataKey {
plaintext: plaintext.to_vec(),
ciphertext: ciphertext.to_vec(),
key_spec: KeySpec::Aes256, // Default to AES-256
};
self.data_key_cache.insert(key_id.to_string(), cached_key).await;
self.data_key_cache.run_pending_tasks().await;
}
/// Remove key metadata from cache
pub async fn remove_key_metadata(&mut self, key_id: &str) {
self.key_metadata_cache.remove(key_id).await;
}
/// Remove data key from cache
pub async fn remove_data_key(&mut self, key_id: &str) {
self.data_key_cache.remove(key_id).await;
}
/// Clear all cached entries
pub async fn clear(&mut self) {
self.key_metadata_cache.invalidate_all();
self.data_key_cache.invalidate_all();
// Wait for invalidation to complete
self.key_metadata_cache.run_pending_tasks().await;
self.data_key_cache.run_pending_tasks().await;
}
/// Get cache statistics (hit count, miss count)
pub fn stats(&self) -> (u64, u64) {
let metadata_stats = (
self.key_metadata_cache.entry_count(),
0u64, // moka doesn't provide miss count directly
);
let data_key_stats = (self.data_key_cache.entry_count(), 0u64);
(metadata_stats.0 + data_key_stats.0, metadata_stats.1 + data_key_stats.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{KeyState, KeyUsage};
use std::time::Duration;
#[derive(Debug, Clone)]
struct CacheInfo {
key_metadata_count: u64,
data_key_count: u64,
}
impl CacheInfo {
fn total_entries(&self) -> u64 {
self.key_metadata_count + self.data_key_count
}
}
impl KmsCache {
fn with_ttl_for_tests(capacity: u64, metadata_ttl: Duration, data_key_ttl: Duration) -> Self {
Self {
key_metadata_cache: Cache::builder().max_capacity(capacity / 2).time_to_live(metadata_ttl).build(),
data_key_cache: Cache::builder().max_capacity(capacity / 2).time_to_live(data_key_ttl).build(),
}
}
fn info_for_tests(&self) -> CacheInfo {
CacheInfo {
key_metadata_count: self.key_metadata_cache.entry_count(),
data_key_count: self.data_key_cache.entry_count(),
}
}
fn contains_key_metadata_for_tests(&self, key_id: &str) -> bool {
self.key_metadata_cache.contains_key(key_id)
}
fn contains_data_key_for_tests(&self, key_id: &str) -> bool {
self.data_key_cache.contains_key(key_id)
}
}
#[tokio::test]
async fn test_cache_operations() {
let mut cache = KmsCache::new(100);
// Test key metadata caching
let metadata = KeyMetadata {
key_id: "test-key-1".to_string(),
key_state: KeyState::Enabled,
key_usage: KeyUsage::EncryptDecrypt,
description: Some("Test key".to_string()),
creation_date: chrono::Utc::now(),
deletion_date: None,
origin: "KMS".to_string(),
key_manager: "CUSTOMER".to_string(),
tags: std::collections::HashMap::new(),
};
// Put and get metadata
cache.put_key_metadata("test-key-1", &metadata).await;
let retrieved = cache.get_key_metadata("test-key-1").await;
assert!(retrieved.is_some());
assert_eq!(retrieved.expect("metadata should be cached").key_id, "test-key-1");
// Test data key caching
let plaintext = vec![1, 2, 3, 4];
let ciphertext = vec![5, 6, 7, 8];
cache.put_data_key("test-key-1", &plaintext, &ciphertext).await;
let cached_data_key = cache.get_data_key("test-key-1").await;
assert!(cached_data_key.is_some());
let cached_data_key = cached_data_key.expect("data key should be cached");
assert_eq!(cached_data_key.plaintext, plaintext);
assert_eq!(cached_data_key.ciphertext, ciphertext);
assert_eq!(cached_data_key.key_spec, KeySpec::Aes256);
// Test cache info
let info = cache.info_for_tests();
assert_eq!(info.key_metadata_count, 1);
assert_eq!(info.data_key_count, 1);
assert_eq!(info.total_entries(), 2);
// Test cache clearing
cache.clear().await;
let info_after_clear = cache.info_for_tests();
assert_eq!(info_after_clear.total_entries(), 0);
}
#[tokio::test]
async fn test_cache_with_custom_ttl() {
let mut cache = KmsCache::with_ttl_for_tests(
100,
Duration::from_millis(100), // Short TTL for testing
Duration::from_millis(50),
);
let metadata = KeyMetadata {
key_id: "ttl-test-key".to_string(),
key_state: KeyState::Enabled,
key_usage: KeyUsage::EncryptDecrypt,
description: Some("TTL test key".to_string()),
creation_date: chrono::Utc::now(),
deletion_date: None,
origin: "KMS".to_string(),
key_manager: "CUSTOMER".to_string(),
tags: std::collections::HashMap::new(),
};
cache.put_key_metadata("ttl-test-key", &metadata).await;
// Should be present immediately
assert!(cache.get_key_metadata("ttl-test-key").await.is_some());
// Wait for TTL to expire
tokio::time::sleep(Duration::from_millis(150)).await;
// Should be expired now
assert!(cache.get_key_metadata("ttl-test-key").await.is_none());
}
#[tokio::test]
async fn test_cache_contains_methods() {
let mut cache = KmsCache::new(100);
assert!(!cache.contains_key_metadata_for_tests("nonexistent"));
assert!(!cache.contains_data_key_for_tests("nonexistent"));
let metadata = KeyMetadata {
key_id: "contains-test".to_string(),
key_state: KeyState::Enabled,
key_usage: KeyUsage::EncryptDecrypt,
description: None,
creation_date: chrono::Utc::now(),
deletion_date: None,
origin: "KMS".to_string(),
key_manager: "CUSTOMER".to_string(),
tags: std::collections::HashMap::new(),
};
cache.put_key_metadata("contains-test", &metadata).await;
cache.put_data_key("contains-test", &[1, 2, 3], &[4, 5, 6]).await;
assert!(cache.contains_key_metadata_for_tests("contains-test"));
assert!(cache.contains_data_key_for_tests("contains-test"));
}
}
+433
View File
@@ -0,0 +1,433 @@
// 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 configuration management
use crate::error::{KmsError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
/// KMS backend types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KmsBackend {
/// Vault backend (recommended for production)
Vault,
/// Local file-based backend for development and testing only
Local,
}
impl Default for KmsBackend {
fn default() -> Self {
// Default to Local backend since Vault requires configuration
Self::Local
}
}
/// Main KMS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KmsConfig {
/// Backend type
pub backend: KmsBackend,
/// Default master key ID for auto-encryption
pub default_key_id: Option<String>,
/// Backend-specific configuration
pub backend_config: BackendConfig,
/// Operation timeout
pub timeout: Duration,
/// Number of retry attempts
pub retry_attempts: u32,
/// Enable caching
pub enable_cache: bool,
/// Cache configuration
pub cache_config: CacheConfig,
}
impl Default for KmsConfig {
fn default() -> Self {
Self {
backend: KmsBackend::default(),
default_key_id: None,
backend_config: BackendConfig::default(),
timeout: Duration::from_secs(30),
retry_attempts: 3,
enable_cache: true,
cache_config: CacheConfig::default(),
}
}
}
/// Backend-specific configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BackendConfig {
/// Local backend configuration
Local(LocalConfig),
/// Vault backend configuration
Vault(VaultConfig),
}
impl Default for BackendConfig {
fn default() -> Self {
Self::Local(LocalConfig::default())
}
}
/// Local KMS backend configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalConfig {
/// Directory to store key files
pub key_dir: PathBuf,
/// Master key for encrypting stored keys (if None, keys are stored in plaintext)
pub master_key: Option<String>,
/// File permissions for key files (octal)
pub file_permissions: Option<u32>,
}
impl Default for LocalConfig {
fn default() -> Self {
Self {
key_dir: std::env::temp_dir().join("rustfs_kms_keys"),
master_key: None,
file_permissions: Some(0o600), // Owner read/write only
}
}
}
/// Vault backend configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
/// Vault server URL
pub address: String,
/// Authentication method
pub auth_method: VaultAuthMethod,
/// Vault namespace (Vault Enterprise)
pub namespace: Option<String>,
/// Transit engine mount path
pub mount_path: String,
/// KV engine mount path for storing keys
pub kv_mount: String,
/// Path prefix for keys in KV store
pub key_path_prefix: String,
/// TLS configuration
pub tls: Option<TlsConfig>,
}
impl Default for VaultConfig {
fn default() -> Self {
Self {
address: "http://localhost:8200".to_string(),
auth_method: VaultAuthMethod::Token {
token: "dev-token".to_string(),
},
namespace: None,
mount_path: "transit".to_string(),
kv_mount: "secret".to_string(),
key_path_prefix: "rustfs/kms/keys".to_string(),
tls: None,
}
}
}
/// Vault authentication methods
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VaultAuthMethod {
/// Token authentication
Token { token: String },
/// AppRole authentication
AppRole { role_id: String, secret_id: String },
}
/// TLS configuration for Vault
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
/// Path to CA certificate file
pub ca_cert_path: Option<PathBuf>,
/// Path to client certificate file
pub client_cert_path: Option<PathBuf>,
/// Path to client private key file
pub client_key_path: Option<PathBuf>,
/// Skip TLS verification (insecure, for development only)
pub skip_verify: bool,
}
/// Cache configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
/// Maximum number of keys to cache
pub max_keys: usize,
/// TTL for cached keys
pub ttl: Duration,
/// Enable cache metrics
pub enable_metrics: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_keys: 1000,
ttl: Duration::from_secs(3600), // 1 hour
enable_metrics: true,
}
}
}
impl KmsConfig {
/// Create a new KMS configuration for local backend (for development and testing only)
pub fn local(key_dir: PathBuf) -> Self {
Self {
backend: KmsBackend::Local,
backend_config: BackendConfig::Local(LocalConfig {
key_dir,
..Default::default()
}),
..Default::default()
}
}
/// Create a new KMS configuration for Vault backend with token authentication (recommended for production)
pub fn vault(address: Url, token: String) -> Self {
Self {
backend: KmsBackend::Vault,
backend_config: BackendConfig::Vault(VaultConfig {
address: address.to_string(),
auth_method: VaultAuthMethod::Token { token },
..Default::default()
}),
..Default::default()
}
}
/// Create a new KMS configuration for Vault backend with AppRole authentication (recommended for production)
pub fn vault_approle(address: Url, role_id: String, secret_id: String) -> Self {
Self {
backend: KmsBackend::Vault,
backend_config: BackendConfig::Vault(VaultConfig {
address: address.to_string(),
auth_method: VaultAuthMethod::AppRole { role_id, secret_id },
..Default::default()
}),
..Default::default()
}
}
/// Get the local configuration if backend is Local
pub fn local_config(&self) -> Option<&LocalConfig> {
match &self.backend_config {
BackendConfig::Local(config) => Some(config),
_ => None,
}
}
/// Get the Vault configuration if backend is Vault
pub fn vault_config(&self) -> Option<&VaultConfig> {
match &self.backend_config {
BackendConfig::Vault(config) => Some(config),
_ => None,
}
}
/// Set default key ID
pub fn with_default_key(mut self, key_id: String) -> Self {
self.default_key_id = Some(key_id);
self
}
/// Set operation timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Enable or disable caching
pub fn with_cache(mut self, enable: bool) -> Self {
self.enable_cache = enable;
self
}
/// Validate the configuration
pub fn validate(&self) -> Result<()> {
// Validate timeout
if self.timeout.is_zero() {
return Err(KmsError::configuration_error("Timeout must be greater than 0"));
}
// Validate retry attempts
if self.retry_attempts == 0 {
return Err(KmsError::configuration_error("Retry attempts must be greater than 0"));
}
// Validate backend-specific configuration
match &self.backend_config {
BackendConfig::Local(config) => {
if !config.key_dir.is_absolute() {
return Err(KmsError::configuration_error("Local key directory must be an absolute path"));
}
}
BackendConfig::Vault(config) => {
if !config.address.starts_with("http://") && !config.address.starts_with("https://") {
return Err(KmsError::configuration_error("Vault address must use http or https scheme"));
}
if config.mount_path.is_empty() {
return Err(KmsError::configuration_error("Vault mount path cannot be empty"));
}
// Validate TLS configuration if using HTTPS
if config.address.starts_with("https://") {
if let Some(ref tls) = config.tls {
if !tls.skip_verify {
// In production, we should have proper TLS configuration
if tls.ca_cert_path.is_none() && tls.client_cert_path.is_none() {
tracing::warn!("Using HTTPS without custom TLS configuration - relying on system CA");
}
}
}
}
}
}
// Validate cache configuration
if self.enable_cache && self.cache_config.max_keys == 0 {
return Err(KmsError::configuration_error("Cache max_keys must be greater than 0"));
}
Ok(())
}
/// Load configuration from environment variables
pub fn from_env() -> Result<Self> {
let mut config = Self::default();
// Backend type
if let Ok(backend_type) = std::env::var("RUSTFS_KMS_BACKEND") {
config.backend = match backend_type.to_lowercase().as_str() {
"local" => KmsBackend::Local,
"vault" => KmsBackend::Vault,
_ => return Err(KmsError::configuration_error(format!("Unknown KMS backend: {}", backend_type))),
};
}
// Default key ID
if let Ok(key_id) = std::env::var("RUSTFS_KMS_DEFAULT_KEY_ID") {
config.default_key_id = Some(key_id);
}
// Timeout
if let Ok(timeout_str) = std::env::var("RUSTFS_KMS_TIMEOUT_SECS") {
let timeout_secs = timeout_str
.parse::<u64>()
.map_err(|_| KmsError::configuration_error("Invalid timeout value"))?;
config.timeout = Duration::from_secs(timeout_secs);
}
// Retry attempts
if let Ok(retries_str) = std::env::var("RUSTFS_KMS_RETRY_ATTEMPTS") {
config.retry_attempts = retries_str
.parse()
.map_err(|_| KmsError::configuration_error("Invalid retry attempts value"))?;
}
// Enable cache
if let Ok(cache_str) = std::env::var("RUSTFS_KMS_ENABLE_CACHE") {
config.enable_cache = cache_str.parse().unwrap_or(true);
}
// Backend-specific configuration
match config.backend {
KmsBackend::Local => {
let key_dir = std::env::var("RUSTFS_KMS_LOCAL_KEY_DIR").unwrap_or_else(|_| "./kms_keys".to_string());
let master_key = std::env::var("RUSTFS_KMS_LOCAL_MASTER_KEY").ok();
config.backend_config = BackendConfig::Local(LocalConfig {
key_dir: PathBuf::from(key_dir),
master_key,
file_permissions: Some(0o600),
});
}
KmsBackend::Vault => {
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 {
address,
auth_method: VaultAuthMethod::Token { token },
namespace: std::env::var("RUSTFS_KMS_VAULT_NAMESPACE").ok(),
mount_path: std::env::var("RUSTFS_KMS_VAULT_MOUNT_PATH").unwrap_or_else(|_| "transit".to_string()),
kv_mount: std::env::var("RUSTFS_KMS_VAULT_KV_MOUNT").unwrap_or_else(|_| "secret".to_string()),
key_path_prefix: std::env::var("RUSTFS_KMS_VAULT_KEY_PREFIX")
.unwrap_or_else(|_| "rustfs/kms/keys".to_string()),
tls: None,
});
}
}
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_default_config() {
let config = KmsConfig::default();
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_ok());
}
#[test]
fn test_local_config() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
assert_eq!(config.backend, KmsBackend::Local);
assert!(config.validate().is_ok());
let local_config = config.local_config().expect("Should have local config");
assert_eq!(local_config.key_dir, temp_dir.path());
}
#[test]
fn test_vault_config() {
let address = Url::parse("https://vault.example.com:8200").expect("Valid URL");
let config = KmsConfig::vault(address.clone(), "test-token".to_string());
assert_eq!(config.backend, KmsBackend::Vault);
assert!(config.validate().is_ok());
let vault_config = config.vault_config().expect("Should have vault config");
assert_eq!(vault_config.address, address.as_str());
}
#[test]
fn test_config_validation() {
let mut config = KmsConfig::default();
// Valid config
assert!(config.validate().is_ok());
// Invalid timeout
config.timeout = Duration::from_secs(0);
assert!(config.validate().is_err());
// Reset timeout and test invalid retry attempts
config.timeout = Duration::from_secs(30);
config.retry_attempts = 0;
assert!(config.validate().is_err());
}
}
+348
View File
@@ -0,0 +1,348 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Cipher implementations for object encryption
use crate::error::{KmsError, Result};
use crate::types::EncryptionAlgorithm;
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use chacha20poly1305::ChaCha20Poly1305;
/// Trait for object encryption ciphers
#[cfg_attr(not(test), allow(dead_code))]
pub trait ObjectCipher: Send + Sync {
/// Encrypt data with the given IV and AAD
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)>;
/// Decrypt data with the given IV, tag, and AAD
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>>;
/// Get the algorithm name
fn algorithm(&self) -> &'static str;
/// Get the required key size in bytes
fn key_size(&self) -> usize;
/// Get the required IV size in bytes
fn iv_size(&self) -> usize;
/// Get the tag size in bytes
fn tag_size(&self) -> usize;
}
/// AES-256-GCM cipher implementation
pub struct AesCipher {
cipher: Aes256Gcm,
}
impl AesCipher {
/// Create a new AES cipher with the given key
pub fn new(key: &[u8]) -> Result<Self> {
if key.len() != 32 {
return Err(KmsError::invalid_key_size(32, key.len()));
}
let key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(key);
Ok(Self { cipher })
}
}
impl ObjectCipher for AesCipher {
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
if iv.len() != 12 {
return Err(KmsError::invalid_key_size(12, iv.len()));
}
let nonce = Nonce::from_slice(iv);
// AES-GCM includes the tag in the ciphertext
let ciphertext_with_tag = self
.cipher
.encrypt(nonce, aes_gcm::aead::Payload { msg: plaintext, aad })
.map_err(KmsError::from_aes_gcm_error)?;
// Split ciphertext and tag
let tag_size = self.tag_size();
if ciphertext_with_tag.len() < tag_size {
return Err(KmsError::cryptographic_error("AES-GCM encrypt", "Ciphertext too short for tag"));
}
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
Ok((ciphertext.to_vec(), tag.to_vec()))
}
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
if iv.len() != 12 {
return Err(KmsError::invalid_key_size(12, iv.len()));
}
if tag.len() != self.tag_size() {
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
}
let nonce = Nonce::from_slice(iv);
// Combine ciphertext and tag for AES-GCM
let mut ciphertext_with_tag = ciphertext.to_vec();
ciphertext_with_tag.extend_from_slice(tag);
let plaintext = self
.cipher
.decrypt(
nonce,
aes_gcm::aead::Payload {
msg: &ciphertext_with_tag,
aad,
},
)
.map_err(KmsError::from_aes_gcm_error)?;
Ok(plaintext)
}
fn algorithm(&self) -> &'static str {
"AES-256-GCM"
}
fn key_size(&self) -> usize {
32 // 256 bits
}
fn iv_size(&self) -> usize {
12 // 96 bits for GCM
}
fn tag_size(&self) -> usize {
16 // 128 bits
}
}
/// ChaCha20-Poly1305 cipher implementation
pub struct ChaCha20Cipher {
cipher: ChaCha20Poly1305,
}
impl ChaCha20Cipher {
/// Create a new ChaCha20 cipher with the given key
pub fn new(key: &[u8]) -> Result<Self> {
if key.len() != 32 {
return Err(KmsError::invalid_key_size(32, key.len()));
}
let key = chacha20poly1305::Key::from_slice(key);
let cipher = ChaCha20Poly1305::new(key);
Ok(Self { cipher })
}
}
impl ObjectCipher for ChaCha20Cipher {
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
if iv.len() != 12 {
return Err(KmsError::invalid_key_size(12, iv.len()));
}
let nonce = chacha20poly1305::Nonce::from_slice(iv);
// ChaCha20-Poly1305 includes the tag in the ciphertext
let ciphertext_with_tag = self
.cipher
.encrypt(nonce, chacha20poly1305::aead::Payload { msg: plaintext, aad })
.map_err(KmsError::from_chacha20_error)?;
// Split ciphertext and tag
let tag_size = self.tag_size();
if ciphertext_with_tag.len() < tag_size {
return Err(KmsError::cryptographic_error("ChaCha20-Poly1305 encrypt", "Ciphertext too short for tag"));
}
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
Ok((ciphertext.to_vec(), tag.to_vec()))
}
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
if iv.len() != 12 {
return Err(KmsError::invalid_key_size(12, iv.len()));
}
if tag.len() != self.tag_size() {
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
}
let nonce = chacha20poly1305::Nonce::from_slice(iv);
// Combine ciphertext and tag for ChaCha20-Poly1305
let mut ciphertext_with_tag = ciphertext.to_vec();
ciphertext_with_tag.extend_from_slice(tag);
let plaintext = self
.cipher
.decrypt(
nonce,
chacha20poly1305::aead::Payload {
msg: &ciphertext_with_tag,
aad,
},
)
.map_err(KmsError::from_chacha20_error)?;
Ok(plaintext)
}
fn algorithm(&self) -> &'static str {
"ChaCha20-Poly1305"
}
fn key_size(&self) -> usize {
32 // 256 bits
}
fn iv_size(&self) -> usize {
12 // 96 bits
}
fn tag_size(&self) -> usize {
16 // 128 bits
}
}
/// Create a cipher instance for the given algorithm and key
pub fn create_cipher(algorithm: &EncryptionAlgorithm, key: &[u8]) -> Result<Box<dyn ObjectCipher>> {
match algorithm {
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => Ok(Box::new(AesCipher::new(key)?)),
EncryptionAlgorithm::ChaCha20Poly1305 => Ok(Box::new(ChaCha20Cipher::new(key)?)),
}
}
/// Generate a random IV for the given algorithm
pub fn generate_iv(algorithm: &EncryptionAlgorithm) -> Vec<u8> {
let iv_size = match algorithm {
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => 12,
EncryptionAlgorithm::ChaCha20Poly1305 => 12,
};
let mut iv = vec![0u8; iv_size];
OsRng.fill_bytes(&mut iv);
iv
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aes_cipher() {
let key = [0u8; 32]; // 256-bit key
let cipher = AesCipher::new(&key).expect("Failed to create AES cipher");
let plaintext = b"Hello, World!";
let iv = [0u8; 12]; // 96-bit IV
let aad = b"additional data";
// Test encryption
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
assert!(!ciphertext.is_empty());
assert_eq!(tag.len(), 16); // 128-bit tag
// Test decryption
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
assert_eq!(decrypted, plaintext);
// Test properties
assert_eq!(cipher.algorithm(), "AES-256-GCM");
assert_eq!(cipher.key_size(), 32);
assert_eq!(cipher.iv_size(), 12);
assert_eq!(cipher.tag_size(), 16);
}
#[test]
fn test_chacha20_cipher() {
let key = [0u8; 32]; // 256-bit key
let cipher = ChaCha20Cipher::new(&key).expect("Failed to create ChaCha20 cipher");
let plaintext = b"Hello, ChaCha20!";
let iv = [0u8; 12]; // 96-bit IV
let aad = b"additional data";
// Test encryption
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
assert!(!ciphertext.is_empty());
assert_eq!(tag.len(), 16); // 128-bit tag
// Test decryption
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
assert_eq!(decrypted, plaintext);
// Test properties
assert_eq!(cipher.algorithm(), "ChaCha20-Poly1305");
assert_eq!(cipher.key_size(), 32);
assert_eq!(cipher.iv_size(), 12);
assert_eq!(cipher.tag_size(), 16);
}
#[test]
fn test_create_cipher() {
let key = [0u8; 32];
// Test AES creation
let aes_cipher = create_cipher(&EncryptionAlgorithm::Aes256, &key).expect("Failed to create AES cipher");
assert_eq!(aes_cipher.algorithm(), "AES-256-GCM");
// Test ChaCha20 creation
let chacha_cipher =
create_cipher(&EncryptionAlgorithm::ChaCha20Poly1305, &key).expect("Failed to create ChaCha20 cipher");
assert_eq!(chacha_cipher.algorithm(), "ChaCha20-Poly1305");
}
#[test]
fn test_generate_iv() {
let aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
assert_eq!(aes_iv.len(), 12);
let chacha_iv = generate_iv(&EncryptionAlgorithm::ChaCha20Poly1305);
assert_eq!(chacha_iv.len(), 12);
// IVs should be different
let another_aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
assert_ne!(aes_iv, another_aes_iv);
}
#[test]
fn test_invalid_key_size() {
let short_key = [0u8; 16]; // Too short
assert!(AesCipher::new(&short_key).is_err());
assert!(ChaCha20Cipher::new(&short_key).is_err());
}
#[test]
fn test_invalid_iv_size() {
let key = [0u8; 32];
let cipher = AesCipher::new(&key).expect("Failed to create cipher");
let plaintext = b"test";
let short_iv = [0u8; 8]; // Too short
let aad = b"";
assert!(cipher.encrypt(plaintext, &short_iv, aad).is_err());
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Object encryption service implementation
mod ciphers;
pub mod service;
pub use service::ObjectEncryptionService;
+754
View File
@@ -0,0 +1,754 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Object encryption service for S3-compatible encryption
use crate::encryption::ciphers::{create_cipher, generate_iv};
use crate::error::{KmsError, Result};
use crate::manager::KmsManager;
use crate::types::*;
use zeroize::Zeroize;
/// Data key for object encryption
/// SECURITY: This struct automatically zeros sensitive key material when dropped
#[derive(Debug, Clone)]
pub struct DataKey {
/// 256-bit encryption key - automatically zeroed on drop
pub plaintext_key: [u8; 32],
/// 96-bit nonce for GCM mode - not secret so no need to zero
pub nonce: [u8; 12],
}
// SECURITY: Implement Drop to automatically zero sensitive key material
impl Drop for DataKey {
fn drop(&mut self) {
self.plaintext_key.zeroize();
}
}
use base64::Engine;
use rand::random;
use std::collections::HashMap;
use std::io::Cursor;
use tokio::io::{AsyncRead, AsyncReadExt};
use tracing::{debug, info};
/// Service for encrypting and decrypting S3 objects with KMS integration
pub struct ObjectEncryptionService {
kms_manager: KmsManager,
}
/// Result of object encryption
#[derive(Debug, Clone)]
pub struct EncryptionResult {
/// Encrypted data
pub ciphertext: Vec<u8>,
/// Encryption metadata to be stored with the object
pub metadata: EncryptionMetadata,
}
impl ObjectEncryptionService {
/// Create a new object encryption service
pub fn new(kms_manager: KmsManager) -> Self {
Self { kms_manager }
}
/// Create a new master key (delegates to KMS manager)
pub async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
self.kms_manager.create_key(request).await
}
/// Describe a master key (delegates to KMS manager)
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
self.kms_manager.describe_key(request).await
}
/// List master keys (delegates to KMS manager)
pub async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
self.kms_manager.list_keys(request).await
}
/// Generate a data encryption key (delegates to KMS manager)
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
self.kms_manager.generate_data_key(request).await
}
/// Get the default key ID
pub fn get_default_key_id(&self) -> Option<&String> {
self.kms_manager.get_default_key_id()
}
/// Get cache statistics
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
self.kms_manager.cache_stats().await
}
/// Clear the cache
pub async fn clear_cache(&self) -> Result<()> {
self.kms_manager.clear_cache().await
}
/// Get backend health status
pub async fn health_check(&self) -> Result<bool> {
self.kms_manager.health_check().await
}
/// Create a data encryption key for object encryption
pub async fn create_data_key(
&self,
kms_key_id: &Option<String>,
context: &ObjectEncryptionContext,
) -> Result<(DataKey, Vec<u8>)> {
// Determine the KMS key ID to use
let actual_key_id = kms_key_id
.as_ref()
.map(|s| s.as_str())
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
// Build encryption context
let mut enc_context = context.encryption_context.clone();
enc_context.insert("bucket".to_string(), context.bucket.clone());
enc_context.insert("object_key".to_string(), context.object_key.clone());
let request = GenerateDataKeyRequest {
key_id: actual_key_id.to_string(),
key_spec: KeySpec::Aes256,
encryption_context: enc_context,
};
let data_key_response = self.kms_manager.generate_data_key(request).await?;
// Generate a unique random nonce for this data key
// This ensures each object/part gets a unique base nonce for streaming encryption
let nonce: [u8; 12] = random();
tracing::info!("Generated random nonce for data key: {:02x?}", nonce);
let data_key = DataKey {
plaintext_key: data_key_response
.plaintext_key
.try_into()
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
nonce,
};
Ok((data_key, data_key_response.ciphertext_blob))
}
/// Decrypt a data encryption key
pub async fn decrypt_data_key(&self, encrypted_key: &[u8], _context: &ObjectEncryptionContext) -> Result<DataKey> {
let decrypt_request = DecryptRequest {
ciphertext: encrypted_key.to_vec(),
encryption_context: HashMap::new(),
grant_tokens: Vec::new(),
};
let decrypt_response = self.kms_manager.decrypt(decrypt_request).await?;
let data_key = DataKey {
plaintext_key: decrypt_response
.plaintext
.try_into()
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
nonce: [0u8; 12], // This will be replaced by stored nonce during GET
};
Ok(data_key)
}
/// Encrypt object data using server-side encryption
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `reader` - Data reader
/// * `algorithm` - Encryption algorithm to use
/// * `kms_key_id` - Optional KMS key ID (uses default if None)
/// * `encryption_context` - Additional encryption context
///
/// # Returns
/// EncryptionResult containing encrypted data and metadata
pub async fn encrypt_object<R>(
&self,
bucket: &str,
object_key: &str,
mut reader: R,
algorithm: &EncryptionAlgorithm,
kms_key_id: Option<&str>,
encryption_context: Option<&HashMap<String, String>>,
) -> Result<EncryptionResult>
where
R: AsyncRead + Unpin,
{
debug!("Encrypting object {}/{} with algorithm {:?}", bucket, object_key, algorithm);
// Read all data (for simplicity - in production, use streaming)
let mut data = Vec::new();
reader.read_to_end(&mut data).await?;
let original_size = data.len() as u64;
// Determine the KMS key ID to use
let actual_key_id = kms_key_id
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
// Build encryption context
let mut context = encryption_context.cloned().unwrap_or_default();
context.insert("bucket".to_string(), bucket.to_string());
context.insert("object".to_string(), object_key.to_string());
context.insert("algorithm".to_string(), algorithm.as_str().to_string());
// Auto-create key for SSE-S3 if it doesn't exist
if algorithm == &EncryptionAlgorithm::Aes256 {
let describe_req = DescribeKeyRequest {
key_id: actual_key_id.to_string(),
};
if let Err(KmsError::KeyNotFound { .. }) = self.kms_manager.describe_key(describe_req).await {
info!("Auto-creating SSE-S3 key: {}", actual_key_id);
let create_req = CreateKeyRequest {
key_name: Some(actual_key_id.to_string()),
key_usage: KeyUsage::EncryptDecrypt,
description: Some("Auto-created SSE-S3 key".to_string()),
policy: None,
tags: HashMap::new(),
origin: None,
};
self.kms_manager
.create_key(create_req)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to auto-create SSE-S3 key {}: {}", actual_key_id, e)))?;
}
} else {
// For SSE-KMS, key must exist
let describe_req = DescribeKeyRequest {
key_id: actual_key_id.to_string(),
};
self.kms_manager.describe_key(describe_req).await.map_err(|_| {
KmsError::invalid_operation(format!("SSE-KMS key '{}' not found. Please create it first.", actual_key_id))
})?;
}
// Generate data encryption key
let request = GenerateDataKeyRequest {
key_id: actual_key_id.to_string(),
key_spec: KeySpec::Aes256,
encryption_context: context.clone(),
};
let data_key = self
.kms_manager
.generate_data_key(request)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to generate data key: {}", e)))?;
let plaintext_key = data_key.plaintext_key;
// Create cipher and generate IV
let cipher = create_cipher(algorithm, &plaintext_key)?;
let iv = generate_iv(algorithm);
// Build AAD from encryption context
let aad = serde_json::to_vec(&context)?;
// Encrypt the data
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
// Create encryption metadata
let metadata = EncryptionMetadata {
algorithm: algorithm.as_str().to_string(),
key_id: actual_key_id.to_string(),
key_version: 1, // Default to version 1 for now
iv,
tag: Some(tag),
encryption_context: context,
encrypted_at: chrono::Utc::now(),
original_size,
encrypted_data_key: data_key.ciphertext_blob,
};
info!("Successfully encrypted object {}/{} ({} bytes)", bucket, object_key, original_size);
Ok(EncryptionResult { ciphertext, metadata })
}
/// Decrypt object data
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `ciphertext` - Encrypted data
/// * `metadata` - Encryption metadata
/// * `expected_context` - Expected encryption context for validation
///
/// # Returns
/// Decrypted data as a reader
pub async fn decrypt_object(
&self,
bucket: &str,
object_key: &str,
ciphertext: Vec<u8>,
metadata: &EncryptionMetadata,
expected_context: Option<&HashMap<String, String>>,
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
debug!("Decrypting object {}/{} with algorithm {}", bucket, object_key, metadata.algorithm);
// Validate encryption context if provided
if let Some(expected) = expected_context {
self.validate_encryption_context(&metadata.encryption_context, expected)?;
}
// Parse algorithm
let algorithm = metadata
.algorithm
.parse::<EncryptionAlgorithm>()
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
// Decrypt the data key
let decrypt_request = DecryptRequest {
ciphertext: metadata.encrypted_data_key.clone(),
encryption_context: metadata.encryption_context.clone(),
grant_tokens: Vec::new(),
};
let decrypt_response = self
.kms_manager
.decrypt(decrypt_request)
.await
.map_err(|e| KmsError::backend_error(format!("Failed to decrypt data key: {}", e)))?;
// Create cipher
let cipher = create_cipher(&algorithm, &decrypt_response.plaintext)?;
// Build AAD from encryption context
let aad = serde_json::to_vec(&metadata.encryption_context)?;
// Get tag from metadata
let tag = metadata
.tag
.as_ref()
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
// Decrypt the data
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
info!("Successfully decrypted object {}/{} ({} bytes)", bucket, object_key, plaintext.len());
Ok(Box::new(Cursor::new(plaintext)))
}
/// Encrypt object with customer-provided key (SSE-C)
///
/// # Arguments
/// * `bucket` - S3 bucket name
/// * `object_key` - S3 object key
/// * `reader` - Data reader
/// * `customer_key` - Customer-provided 256-bit key
/// * `customer_key_md5` - Optional MD5 hash of the customer key for validation
///
/// # Returns
/// EncryptionResult with SSE-C metadata
pub async fn encrypt_object_with_customer_key<R>(
&self,
bucket: &str,
object_key: &str,
mut reader: R,
customer_key: &[u8],
customer_key_md5: Option<&str>,
) -> Result<EncryptionResult>
where
R: AsyncRead + Unpin,
{
debug!("Encrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
// Validate key size
if customer_key.len() != 32 {
return Err(KmsError::invalid_key_size(32, customer_key.len()));
}
// Validate key MD5 if provided
if let Some(expected_md5) = customer_key_md5 {
let actual_md5 = md5::compute(customer_key);
let actual_md5_hex = format!("{:x}", actual_md5);
if actual_md5_hex != expected_md5.to_lowercase() {
return Err(KmsError::validation_error("Customer key MD5 mismatch"));
}
}
// Read all data
let mut data = Vec::new();
reader.read_to_end(&mut data).await?;
let original_size = data.len() as u64;
// Create cipher and generate IV
let algorithm = EncryptionAlgorithm::Aes256;
let cipher = create_cipher(&algorithm, customer_key)?;
let iv = generate_iv(&algorithm);
// Build minimal encryption context for SSE-C
let context = HashMap::from([
("bucket".to_string(), bucket.to_string()),
("object".to_string(), object_key.to_string()),
("sse_type".to_string(), "customer".to_string()),
]);
let aad = serde_json::to_vec(&context)?;
// Encrypt the data
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
// Create metadata (no encrypted data key for SSE-C)
let metadata = EncryptionMetadata {
algorithm: algorithm.as_str().to_string(),
key_id: "sse-c".to_string(), // Special marker for SSE-C
key_version: 1,
iv,
tag: Some(tag),
encryption_context: context,
encrypted_at: chrono::Utc::now(),
original_size,
encrypted_data_key: Vec::new(), // Empty for SSE-C
};
info!(
"Successfully encrypted object {}/{} with SSE-C ({} bytes)",
bucket, object_key, original_size
);
Ok(EncryptionResult { ciphertext, metadata })
}
/// Decrypt object with customer-provided key (SSE-C)
pub async fn decrypt_object_with_customer_key(
&self,
bucket: &str,
object_key: &str,
ciphertext: Vec<u8>,
metadata: &EncryptionMetadata,
customer_key: &[u8],
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
debug!("Decrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
// Validate key size
if customer_key.len() != 32 {
return Err(KmsError::invalid_key_size(32, customer_key.len()));
}
// Validate that this is SSE-C
if metadata.key_id != "sse-c" {
return Err(KmsError::invalid_operation("This object was not encrypted with SSE-C"));
}
// Parse algorithm
let algorithm = metadata
.algorithm
.parse::<EncryptionAlgorithm>()
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
// Create cipher
let cipher = create_cipher(&algorithm, customer_key)?;
// Build AAD from encryption context
let aad = serde_json::to_vec(&metadata.encryption_context)?;
// Get tag from metadata
let tag = metadata
.tag
.as_ref()
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
// Decrypt the data
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
info!(
"Successfully decrypted SSE-C object {}/{} ({} bytes)",
bucket,
object_key,
plaintext.len()
);
Ok(Box::new(Cursor::new(plaintext)))
}
/// Validate encryption context
fn validate_encryption_context(&self, actual: &HashMap<String, String>, expected: &HashMap<String, String>) -> Result<()> {
for (key, expected_value) in expected {
match actual.get(key) {
Some(actual_value) if actual_value == expected_value => continue,
Some(actual_value) => {
return Err(KmsError::context_mismatch(format!(
"Context mismatch for '{}': expected '{}', got '{}'",
key, expected_value, actual_value
)));
}
None => {
return Err(KmsError::context_mismatch(format!("Missing context key '{}'", key)));
}
}
}
Ok(())
}
/// Convert encryption metadata to HTTP headers for S3 compatibility
pub fn metadata_to_headers(&self, metadata: &EncryptionMetadata) -> HashMap<String, String> {
let mut headers = HashMap::new();
// Standard S3 encryption headers
if metadata.key_id == "sse-c" {
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
headers.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string());
} else if metadata.algorithm == "AES256" {
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
// For SSE-S3, we still need to store the key ID for internal use
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
} else {
headers.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string());
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
}
// Internal headers for decryption
headers.insert(
"x-rustfs-encryption-iv".to_string(),
base64::engine::general_purpose::STANDARD.encode(&metadata.iv),
);
if let Some(ref tag) = metadata.tag {
headers.insert(
"x-rustfs-encryption-tag".to_string(),
base64::engine::general_purpose::STANDARD.encode(tag),
);
}
headers.insert(
"x-rustfs-encryption-key".to_string(),
base64::engine::general_purpose::STANDARD.encode(&metadata.encrypted_data_key),
);
headers.insert(
"x-rustfs-encryption-context".to_string(),
serde_json::to_string(&metadata.encryption_context).unwrap_or_default(),
);
headers
}
/// Parse encryption metadata from HTTP headers
pub fn headers_to_metadata(&self, headers: &HashMap<String, String>) -> Result<EncryptionMetadata> {
let algorithm = headers
.get("x-amz-server-side-encryption")
.ok_or_else(|| KmsError::validation_error("Missing encryption algorithm header"))?
.clone();
let key_id = if algorithm == "AES256" && headers.contains_key("x-amz-server-side-encryption-customer-algorithm") {
"sse-c".to_string()
} else if let Some(kms_key_id) = headers.get("x-amz-server-side-encryption-aws-kms-key-id") {
kms_key_id.clone()
} else {
return Err(KmsError::validation_error("Missing key ID"));
};
let iv = headers
.get("x-rustfs-encryption-iv")
.ok_or_else(|| KmsError::validation_error("Missing IV header"))?;
let iv = base64::engine::general_purpose::STANDARD
.decode(iv)
.map_err(|e| KmsError::validation_error(format!("Invalid IV: {}", e)))?;
let tag = if let Some(tag_str) = headers.get("x-rustfs-encryption-tag") {
Some(
base64::engine::general_purpose::STANDARD
.decode(tag_str)
.map_err(|e| KmsError::validation_error(format!("Invalid tag: {}", e)))?,
)
} else {
None
};
let encrypted_data_key = if let Some(key_str) = headers.get("x-rustfs-encryption-key") {
base64::engine::general_purpose::STANDARD
.decode(key_str)
.map_err(|e| KmsError::validation_error(format!("Invalid encrypted key: {}", e)))?
} else {
Vec::new() // Empty for SSE-C
};
let encryption_context = if let Some(context_str) = headers.get("x-rustfs-encryption-context") {
serde_json::from_str(context_str)
.map_err(|e| KmsError::validation_error(format!("Invalid encryption context: {}", e)))?
} else {
HashMap::new()
};
Ok(EncryptionMetadata {
algorithm,
key_id,
key_version: 1, // Default for parsing
iv,
tag,
encryption_context,
encrypted_at: chrono::Utc::now(),
original_size: 0, // Not available from headers
encrypted_data_key,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KmsConfig;
use std::sync::Arc;
use tempfile::TempDir;
async fn create_test_service() -> (ObjectEncryptionService, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_default_key("test-key".to_string());
let backend = Arc::new(
crate::backends::local::LocalKmsBackend::new(config.clone())
.await
.expect("local backend should initialize"),
);
let kms_manager = KmsManager::new(backend, config);
let service = ObjectEncryptionService::new(kms_manager);
(service, temp_dir)
}
#[tokio::test]
async fn test_sse_s3_encryption() {
let (service, _temp_dir) = create_test_service().await;
let bucket = "test-bucket";
let object_key = "test-object";
let data = b"Hello, SSE-S3!";
let reader = Cursor::new(data.to_vec());
// Encrypt with SSE-S3 (auto-create key)
let result = service
.encrypt_object(
bucket,
object_key,
reader,
&EncryptionAlgorithm::Aes256,
None, // Use default key
None,
)
.await
.expect("Encryption failed");
assert!(!result.ciphertext.is_empty());
assert_eq!(result.metadata.algorithm, "AES256");
assert_eq!(result.metadata.original_size, data.len() as u64);
// Decrypt
let decrypted_reader = service
.decrypt_object(bucket, object_key, result.ciphertext, &result.metadata, None)
.await
.expect("Decryption failed");
let mut decrypted_data = Vec::new();
let mut reader = decrypted_reader;
reader
.read_to_end(&mut decrypted_data)
.await
.expect("Failed to read decrypted data");
assert_eq!(decrypted_data, data);
}
#[tokio::test]
async fn test_sse_c_encryption() {
let (service, _temp_dir) = create_test_service().await;
let bucket = "test-bucket";
let object_key = "test-object";
let data = b"Hello, SSE-C!";
let reader = Cursor::new(data.to_vec());
let customer_key = [0u8; 32]; // 256-bit key
// Encrypt with SSE-C
let result = service
.encrypt_object_with_customer_key(bucket, object_key, reader, &customer_key, None)
.await
.expect("SSE-C encryption failed");
assert!(!result.ciphertext.is_empty());
assert_eq!(result.metadata.key_id, "sse-c");
assert_eq!(result.metadata.original_size, data.len() as u64);
// Decrypt with same customer key
let decrypted_reader = service
.decrypt_object_with_customer_key(bucket, object_key, result.ciphertext, &result.metadata, &customer_key)
.await
.expect("SSE-C decryption failed");
let mut decrypted_data = Vec::new();
let mut reader = decrypted_reader;
reader
.read_to_end(&mut decrypted_data)
.await
.expect("Failed to read decrypted data");
assert_eq!(decrypted_data, data);
}
#[tokio::test]
async fn test_metadata_headers_conversion() {
let (service, _temp_dir) = create_test_service().await;
let metadata = EncryptionMetadata {
algorithm: "AES256".to_string(),
key_id: "test-key".to_string(),
key_version: 1,
iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
tag: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
encryption_context: HashMap::from([("bucket".to_string(), "test-bucket".to_string())]),
encrypted_at: chrono::Utc::now(),
original_size: 100,
encrypted_data_key: vec![1, 2, 3, 4],
};
// Convert to headers
let headers = service.metadata_to_headers(&metadata);
assert!(headers.contains_key("x-amz-server-side-encryption"));
assert!(headers.contains_key("x-rustfs-encryption-iv"));
// Convert back to metadata
let parsed_metadata = service.headers_to_metadata(&headers).expect("Failed to parse headers");
assert_eq!(parsed_metadata.algorithm, metadata.algorithm);
assert_eq!(parsed_metadata.key_id, metadata.key_id);
assert_eq!(parsed_metadata.iv, metadata.iv);
assert_eq!(parsed_metadata.tag, metadata.tag);
}
#[tokio::test]
async fn test_encryption_context_validation() {
let (service, _temp_dir) = create_test_service().await;
let actual_context = HashMap::from([
("bucket".to_string(), "test-bucket".to_string()),
("object".to_string(), "test-object".to_string()),
]);
let valid_expected = HashMap::from([("bucket".to_string(), "test-bucket".to_string())]);
let invalid_expected = HashMap::from([("bucket".to_string(), "wrong-bucket".to_string())]);
// Valid context should pass
assert!(service.validate_encryption_context(&actual_context, &valid_expected).is_ok());
// Invalid context should fail
assert!(
service
.validate_encryption_context(&actual_context, &invalid_expected)
.is_err()
);
}
}
+239
View File
@@ -0,0 +1,239 @@
// 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 error types and result handling
use thiserror::Error;
/// Result type for KMS operations
pub type Result<T> = std::result::Result<T, KmsError>;
/// KMS error types covering all possible failure scenarios
#[derive(Error, Debug, Clone)]
pub enum KmsError {
/// Configuration errors
#[error("Configuration error: {message}")]
ConfigurationError { message: String },
/// Key not found
#[error("Key not found: {key_id}")]
KeyNotFound { key_id: String },
/// Invalid key format or content
#[error("Invalid key: {message}")]
InvalidKey { message: String },
/// Cryptographic operation failed
#[error("Cryptographic error in {operation}: {message}")]
CryptographicError { operation: String, message: String },
/// Backend communication error
#[error("Backend error: {message}")]
BackendError { message: String },
/// Access denied
#[error("Access denied: {message}")]
AccessDenied { message: String },
/// Key already exists
#[error("Key already exists: {key_id}")]
KeyAlreadyExists { key_id: String },
/// Invalid operation state
#[error("Invalid operation: {message}")]
InvalidOperation { message: String },
/// Internal error
#[error("Internal error: {message}")]
InternalError { message: String },
/// Serialization/deserialization error
#[error("Serialization error: {message}")]
SerializationError { message: String },
/// I/O error
#[error("I/O error: {message}")]
IoError { message: String },
/// Cache error
#[error("Cache error: {message}")]
CacheError { message: String },
/// Validation error
#[error("Validation error: {message}")]
ValidationError { message: String },
/// Unsupported algorithm
#[error("Unsupported algorithm: {algorithm}")]
UnsupportedAlgorithm { algorithm: String },
/// Invalid key size
#[error("Invalid key size: expected {expected}, got {actual}")]
InvalidKeySize { expected: usize, actual: usize },
/// Encryption context mismatch
#[error("Encryption context mismatch: {message}")]
ContextMismatch { message: String },
}
impl KmsError {
/// Create a configuration error
pub fn configuration_error<S: Into<String>>(message: S) -> Self {
Self::ConfigurationError { message: message.into() }
}
/// Create a key not found error
pub fn key_not_found<S: Into<String>>(key_id: S) -> Self {
Self::KeyNotFound { key_id: key_id.into() }
}
/// Create an invalid key error
pub fn invalid_key<S: Into<String>>(message: S) -> Self {
Self::InvalidKey { message: message.into() }
}
/// Create a cryptographic error
pub fn cryptographic_error<S1: Into<String>, S2: Into<String>>(operation: S1, message: S2) -> Self {
Self::CryptographicError {
operation: operation.into(),
message: message.into(),
}
}
/// Create a backend error
pub fn backend_error<S: Into<String>>(message: S) -> Self {
Self::BackendError { message: message.into() }
}
/// Create an access denied error
pub fn access_denied<S: Into<String>>(message: S) -> Self {
Self::AccessDenied { message: message.into() }
}
/// Create a key already exists error
pub fn key_already_exists<S: Into<String>>(key_id: S) -> Self {
Self::KeyAlreadyExists { key_id: key_id.into() }
}
/// Create an invalid operation error
pub fn invalid_operation<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an internal error
pub fn internal_error<S: Into<String>>(message: S) -> Self {
Self::InternalError { message: message.into() }
}
/// Create a serialization error
pub fn serialization_error<S: Into<String>>(message: S) -> Self {
Self::SerializationError { message: message.into() }
}
/// Create an I/O error
pub fn io_error<S: Into<String>>(message: S) -> Self {
Self::IoError { message: message.into() }
}
/// Create a cache error
pub fn cache_error<S: Into<String>>(message: S) -> Self {
Self::CacheError { message: message.into() }
}
/// Create a validation error
pub fn validation_error<S: Into<String>>(message: S) -> Self {
Self::ValidationError { message: message.into() }
}
/// Create an invalid parameter error
pub fn invalid_parameter<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an invalid key state error
pub fn invalid_key_state<S: Into<String>>(message: S) -> Self {
Self::InvalidOperation { message: message.into() }
}
/// Create an unsupported algorithm error
pub fn unsupported_algorithm<S: Into<String>>(algorithm: S) -> Self {
Self::UnsupportedAlgorithm {
algorithm: algorithm.into(),
}
}
/// Create an invalid key size error
pub fn invalid_key_size(expected: usize, actual: usize) -> Self {
Self::InvalidKeySize { expected, actual }
}
/// Create an encryption context mismatch error
pub fn context_mismatch<S: Into<String>>(message: S) -> Self {
Self::ContextMismatch { message: message.into() }
}
}
// Convert from standard library errors
impl From<std::io::Error> for KmsError {
fn from(error: std::io::Error) -> Self {
Self::IoError {
message: error.to_string(),
}
}
}
impl From<serde_json::Error> for KmsError {
fn from(error: serde_json::Error) -> Self {
Self::SerializationError {
message: error.to_string(),
}
}
}
// Note: We can't implement From for both aes_gcm::Error and chacha20poly1305::Error
// because they might be the same type. Instead, we provide helper functions.
impl KmsError {
/// Create a KMS error from AES-GCM error
pub fn from_aes_gcm_error(error: aes_gcm::Error) -> Self {
Self::CryptographicError {
operation: "AES-GCM".to_string(),
message: error.to_string(),
}
}
/// Create a KMS error from ChaCha20-Poly1305 error
pub fn from_chacha20_error(error: chacha20poly1305::Error) -> Self {
Self::CryptographicError {
operation: "ChaCha20-Poly1305".to_string(),
message: error.to_string(),
}
}
}
impl From<url::ParseError> for KmsError {
fn from(error: url::ParseError) -> Self {
Self::ConfigurationError {
message: format!("Invalid URL: {}", error),
}
}
}
impl From<reqwest::Error> for KmsError {
fn from(error: reqwest::Error) -> Self {
Self::BackendError {
message: format!("HTTP request failed: {}", error),
}
}
}
+142
View File
@@ -0,0 +1,142 @@
#![deny(clippy::unwrap_used)]
// 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.
//! # RustFS Key Management Service (KMS)
//!
//! This crate provides a comprehensive Key Management Service (KMS) for RustFS,
//! supporting secure key generation, storage, and object encryption capabilities.
//!
//! ## Features
//!
//! - **Multiple Backends**: Local file storage and Vault (optional)
//! - **Object Encryption**: Transparent S3-compatible object encryption
//! - **Streaming Encryption**: Memory-efficient encryption for large files
//! - **Key Management**: Full lifecycle management of encryption keys
//! - **S3 Compatibility**: SSE-S3, SSE-KMS, and SSE-C encryption modes
//!
//! ## Architecture
//!
//! The KMS follows a three-layer key hierarchy:
//! - **Master Keys**: Managed by KMS backends (Local/Vault)
//! - **Data Encryption Keys (DEK)**: Generated per object, encrypted by master keys
//! - **Object Data**: Encrypted using DEKs with AES-256-GCM or ChaCha20-Poly1305
//!
//! ## Example
//!
//! ```rust,no_run
//! use rustfs_kms::{KmsConfig, init_global_kms_service_manager};
//! use std::path::PathBuf;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Initialize global KMS service manager
//! let service_manager = init_global_kms_service_manager();
//!
//! // Configure with local backend
//! let config = KmsConfig::local(PathBuf::from("./kms_keys"));
//! service_manager.configure(config).await?;
//!
//! // Start the KMS service
//! service_manager.start().await?;
//!
//! Ok(())
//! }
//! ```
// Core modules
pub mod api_types;
pub mod backends;
mod cache;
pub mod config;
mod encryption;
mod error;
pub mod manager;
pub mod service_manager;
pub mod types;
// Re-export public API
pub use api_types::{
CacheSummary, ConfigureKmsRequest, ConfigureKmsResponse, ConfigureLocalKmsRequest, ConfigureVaultKmsRequest,
KmsConfigSummary, KmsStatusResponse, StartKmsRequest, StartKmsResponse, StopKmsResponse, TagKeyRequest, TagKeyResponse,
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_manager::{
KmsServiceManager, KmsServiceStatus, get_global_encryption_service, get_global_kms_service_manager,
init_global_kms_service_manager,
};
pub use types::*;
// For backward compatibility - these functions now delegate to the service manager
/// Initialize global encryption service (backward compatibility)
///
/// This function is now deprecated. Use `init_global_kms_service_manager` and configure via API instead.
#[deprecated(note = "Use dynamic KMS configuration via service manager instead")]
pub async fn init_global_services(_service: ObjectEncryptionService) -> Result<()> {
// For backward compatibility only - not recommended for new code
Ok(())
}
/// Check if the global encryption service is initialized and healthy
pub async fn is_encryption_service_healthy() -> bool {
match get_global_encryption_service().await {
Some(service) => service.health_check().await.is_ok(),
None => false,
}
}
/// Shutdown the global encryption service (backward compatibility)
#[deprecated(note = "Use service manager shutdown instead")]
pub fn shutdown_global_services() {
// For backward compatibility only - service manager handles shutdown now
tracing::info!("KMS global services shutdown requested (deprecated)");
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_global_service_lifecycle() {
// Test service manager initialization
let manager = init_global_kms_service_manager();
// Test initial status
let status = manager.get_status().await;
assert_eq!(status, KmsServiceStatus::NotConfigured);
// Test configuration and start
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
manager.configure(config).await.expect("Configuration should succeed");
manager.start().await.expect("Start should succeed");
// Test that encryption service is now available
assert!(get_global_encryption_service().await.is_some());
// Test health check
assert!(is_encryption_service_healthy().await);
// Test stop
manager.stop().await.expect("Stop should succeed");
}
}
+240
View File
@@ -0,0 +1,240 @@
// 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 manager for handling key operations and backend coordination
use crate::backends::KmsBackend;
use crate::cache::KmsCache;
use crate::config::KmsConfig;
use crate::error::Result;
use crate::types::{
CancelKeyDeletionRequest, CancelKeyDeletionResponse, CreateKeyRequest, CreateKeyResponse, DecryptRequest, DecryptResponse,
DeleteKeyRequest, DeleteKeyResponse, DescribeKeyRequest, DescribeKeyResponse, EncryptRequest, EncryptResponse,
GenerateDataKeyRequest, GenerateDataKeyResponse, ListKeysRequest, ListKeysResponse,
};
use std::sync::Arc;
use tokio::sync::RwLock;
/// KMS Manager coordinates operations between backends and caching
#[derive(Clone)]
pub struct KmsManager {
backend: Arc<dyn KmsBackend>,
cache: Arc<RwLock<KmsCache>>,
config: KmsConfig,
}
impl KmsManager {
/// Create a new KMS manager with the given backend and config
pub fn new(backend: Arc<dyn KmsBackend>, config: KmsConfig) -> Self {
let cache = Arc::new(RwLock::new(KmsCache::new(config.cache_config.max_keys as u64)));
Self { backend, cache, config }
}
/// Get the default key ID if configured
pub fn get_default_key_id(&self) -> Option<&String> {
self.config.default_key_id.as_ref()
}
/// Create a new master key
pub async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
let response = self.backend.create_key(request).await?;
// Cache the key metadata if enabled
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.put_key_metadata(&response.key_id, &response.key_metadata).await;
}
Ok(response)
}
/// Encrypt data with a master key
pub async fn encrypt(&self, request: EncryptRequest) -> Result<EncryptResponse> {
self.backend.encrypt(request).await
}
/// Decrypt data with a master key
pub async fn decrypt(&self, request: DecryptRequest) -> Result<DecryptResponse> {
self.backend.decrypt(request).await
}
/// Generate a data encryption key
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
// Check cache first if enabled
if self.config.enable_cache {
let cache = self.cache.read().await;
if let Some(cached_key) = cache.get_data_key(&request.key_id).await {
if cached_key.key_spec == request.key_spec {
return Ok(GenerateDataKeyResponse {
key_id: request.key_id.clone(),
plaintext_key: cached_key.plaintext.clone(),
ciphertext_blob: cached_key.ciphertext.clone(),
});
}
}
}
// Generate new data key from backend
let response = self.backend.generate_data_key(request).await?;
// Cache the data key if enabled
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache
.put_data_key(&response.key_id, &response.plaintext_key, &response.ciphertext_blob)
.await;
}
Ok(response)
}
/// Describe a key
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
// Check cache first if enabled
if self.config.enable_cache {
let cache = self.cache.read().await;
if let Some(cached_metadata) = cache.get_key_metadata(&request.key_id).await {
return Ok(DescribeKeyResponse {
key_metadata: cached_metadata,
});
}
}
// Get from backend and cache
let response = self.backend.describe_key(request).await?;
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache
.put_key_metadata(&response.key_metadata.key_id, &response.key_metadata)
.await;
}
Ok(response)
}
/// List keys
pub async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
self.backend.list_keys(request).await
}
/// Get cache statistics
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
if self.config.enable_cache {
let cache = self.cache.read().await;
Some(cache.stats())
} else {
None
}
}
/// Clear the cache
pub async fn clear_cache(&self) -> Result<()> {
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.clear().await;
}
Ok(())
}
/// Delete a key
pub async fn delete_key(&self, request: DeleteKeyRequest) -> Result<DeleteKeyResponse> {
let response = self.backend.delete_key(request).await?;
// Remove from cache if enabled and key is being deleted
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.remove_key_metadata(&response.key_id).await;
cache.remove_data_key(&response.key_id).await;
}
Ok(response)
}
/// Cancel key deletion
pub async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result<CancelKeyDeletionResponse> {
let response = self.backend.cancel_key_deletion(request).await?;
// Update cache if enabled
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.put_key_metadata(&response.key_id, &response.key_metadata).await;
}
Ok(response)
}
/// Perform health check on the KMS backend
pub async fn health_check(&self) -> Result<bool> {
self.backend.health_check().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backends::local::LocalKmsBackend;
use crate::types::{KeySpec, KeyState, KeyUsage};
use tempfile::tempdir;
#[tokio::test]
async fn test_manager_operations() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config = KmsConfig::local(temp_dir.path().to_path_buf());
let backend = Arc::new(LocalKmsBackend::new(config.clone()).await.expect("Failed to create backend"));
let manager = KmsManager::new(backend, config);
// Test key creation
let create_request = CreateKeyRequest {
key_usage: KeyUsage::EncryptDecrypt,
description: Some("Test key".to_string()),
..Default::default()
};
let create_response = manager.create_key(create_request).await.expect("Failed to create key");
assert!(!create_response.key_id.is_empty());
assert_eq!(create_response.key_metadata.key_state, KeyState::Enabled);
// Test data key generation
let data_key_request = GenerateDataKeyRequest {
key_id: create_response.key_id.clone(),
key_spec: KeySpec::Aes256,
encryption_context: Default::default(),
};
let data_key_response = manager
.generate_data_key(data_key_request)
.await
.expect("Failed to generate data key");
assert_eq!(data_key_response.plaintext_key.len(), 32); // 256 bits
assert!(!data_key_response.ciphertext_blob.is_empty());
// Test describe key
let describe_request = DescribeKeyRequest {
key_id: create_response.key_id.clone(),
};
let describe_response = manager.describe_key(describe_request).await.expect("Failed to describe key");
assert_eq!(describe_response.key_metadata.key_id, create_response.key_id);
// Test cache stats
let stats = manager.cache_stats().await;
assert!(stats.is_some());
// Test health check
let health = manager.health_check().await.expect("Health check failed");
assert!(health);
}
}
+281
View File
@@ -0,0 +1,281 @@
// 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 service manager for dynamic configuration and runtime management
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;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
/// KMS service status
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum KmsServiceStatus {
/// KMS is not configured
NotConfigured,
/// KMS is configured but not running
Configured,
/// KMS is running
Running,
/// KMS encountered an error
Error(String),
}
/// Dynamic KMS service manager
pub struct KmsServiceManager {
/// Current KMS manager (if running)
manager: Arc<RwLock<Option<Arc<KmsManager>>>>,
/// Current encryption service (if running)
encryption_service: Arc<RwLock<Option<Arc<ObjectEncryptionService>>>>,
/// Current configuration
config: Arc<RwLock<Option<KmsConfig>>>,
/// Current status
status: Arc<RwLock<KmsServiceStatus>>,
}
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)),
config: Arc::new(RwLock::new(None)),
status: Arc::new(RwLock::new(KmsServiceStatus::NotConfigured)),
}
}
/// Get current service status
pub async fn get_status(&self) -> KmsServiceStatus {
self.status.read().await.clone()
}
/// Get current configuration (if any)
pub async fn get_config(&self) -> Option<KmsConfig> {
self.config.read().await.clone()
}
/// Configure KMS with new configuration
pub async fn configure(&self, new_config: KmsConfig) -> Result<()> {
tracing::info!("CLAUDE DEBUG: configure() called with backend: {:?}", new_config.backend);
info!("Configuring KMS with backend: {:?}", new_config.backend);
// Update configuration
{
let mut config = self.config.write().await;
*config = Some(new_config.clone());
}
// Update status
{
let mut status = self.status.write().await;
*status = KmsServiceStatus::Configured;
}
info!("KMS configuration updated successfully");
Ok(())
}
/// Start KMS service with current configuration
pub async fn start(&self) -> Result<()> {
tracing::info!("CLAUDE DEBUG: start() called");
let config = {
let config_guard = self.config.read().await;
match config_guard.as_ref() {
Some(config) => config.clone(),
None => {
let err_msg = "Cannot start KMS: no configuration provided";
error!("{}", err_msg);
let mut status = self.status.write().await;
*status = KmsServiceStatus::Error(err_msg.to_string());
return Err(KmsError::configuration_error(err_msg));
}
}
};
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);
}
// Update status
{
let mut status = self.status.write().await;
*status = KmsServiceStatus::Running;
}
info!("KMS service started successfully");
Ok(())
}
Err(e) => {
let err_msg = format!("Failed to create KMS backend: {}", e);
error!("{}", err_msg);
let mut status = self.status.write().await;
*status = KmsServiceStatus::Error(err_msg.clone());
Err(KmsError::backend_error(&err_msg))
}
}
}
/// Stop KMS service
pub async fn stop(&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;
}
// Update status (keep configuration)
{
let mut status = self.status.write().await;
if !matches!(*status, KmsServiceStatus::NotConfigured) {
*status = KmsServiceStatus::Configured;
}
}
info!("KMS service stopped successfully");
Ok(())
}
/// Reconfigure and restart KMS service
pub async fn reconfigure(&self, new_config: KmsConfig) -> Result<()> {
info!("Reconfiguring KMS service");
// Stop current service if running
if matches!(self.get_status().await, KmsServiceStatus::Running) {
self.stop().await?;
}
// Configure with new config
self.configure(new_config).await?;
// Start with new configuration
self.start().await?;
info!("KMS service reconfigured successfully");
Ok(())
}
/// Get KMS manager (if running)
pub async fn get_manager(&self) -> Option<Arc<KmsManager>> {
self.manager.read().await.clone()
}
/// Get encryption service (if running)
pub async fn get_encryption_service(&self) -> Option<Arc<ObjectEncryptionService>> {
self.encryption_service.read().await.clone()
}
/// Health check for the KMS service
pub async fn health_check(&self) -> Result<bool> {
let manager = self.get_manager().await;
match manager {
Some(manager) => {
// Perform health check on the backend
match manager.health_check().await {
Ok(healthy) => {
if !healthy {
warn!("KMS backend health check failed");
}
Ok(healthy)
}
Err(e) => {
error!("KMS health check error: {}", e);
// Update status to error
let mut status = self.status.write().await;
*status = KmsServiceStatus::Error(format!("Health check failed: {}", e));
Err(e)
}
}
}
None => {
warn!("Cannot perform health check: KMS service not running");
Ok(false)
}
}
}
/// Create backend from configuration
async fn create_backend(&self, config: &KmsConfig) -> Result<Arc<dyn KmsBackend>> {
match &config.backend_config {
BackendConfig::Local(_) => {
info!("Creating Local KMS backend");
let backend = LocalKmsBackend::new(config.clone()).await?;
Ok(Arc::new(backend))
}
BackendConfig::Vault(_) => {
info!("Creating Vault KMS backend");
let backend = crate::backends::vault::VaultKmsBackend::new(config.clone()).await?;
Ok(Arc::new(backend))
}
}
}
}
impl Default for KmsServiceManager {
fn default() -> Self {
Self::new()
}
}
/// Global KMS service manager instance
static GLOBAL_KMS_SERVICE_MANAGER: once_cell::sync::OnceCell<Arc<KmsServiceManager>> = once_cell::sync::OnceCell::new();
/// Initialize global KMS service manager
pub fn init_global_kms_service_manager() -> Arc<KmsServiceManager> {
GLOBAL_KMS_SERVICE_MANAGER
.get_or_init(|| Arc::new(KmsServiceManager::new()))
.clone()
}
/// Get global KMS service manager
pub fn get_global_kms_service_manager() -> Option<Arc<KmsServiceManager>> {
GLOBAL_KMS_SERVICE_MANAGER.get().cloned()
}
/// Get global encryption service (if KMS is running)
pub async fn get_global_encryption_service() -> Option<Arc<ObjectEncryptionService>> {
tracing::info!("CLAUDE DEBUG: get_global_encryption_service called");
let manager = get_global_kms_service_manager().unwrap_or_else(|| {
tracing::warn!("CLAUDE DEBUG: KMS service manager not initialized, initializing now as fallback");
init_global_kms_service_manager()
});
let service = manager.get_encryption_service().await;
tracing::info!("CLAUDE DEBUG: get_encryption_service returned: {}", service.is_some());
service
}
+744
View File
@@ -0,0 +1,744 @@
// 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.
//! Core type definitions for KMS operations
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use zeroize::Zeroize;
/// Data encryption key (DEK) used for encrypting object data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataKey {
/// Key identifier
pub key_id: String,
/// Key version
pub version: u32,
/// Plaintext key material (only available during generation)
/// SECURITY: This field is manually zeroed when dropped
pub plaintext: Option<Vec<u8>>,
/// Encrypted key material (ciphertext)
pub ciphertext: Vec<u8>,
/// Key algorithm specification
pub key_spec: String,
/// Associated metadata
pub metadata: HashMap<String, String>,
/// Key creation timestamp
pub created_at: DateTime<Utc>,
}
impl DataKey {
/// Create a new data key
pub fn new(key_id: String, version: u32, plaintext: Option<Vec<u8>>, ciphertext: Vec<u8>, key_spec: String) -> Self {
Self {
key_id,
version,
plaintext,
ciphertext,
key_spec,
metadata: HashMap::new(),
created_at: Utc::now(),
}
}
/// Clear the plaintext key material from memory for security
pub fn clear_plaintext(&mut self) {
if let Some(ref mut plaintext) = self.plaintext {
// Zero out the memory before dropping
plaintext.zeroize();
}
self.plaintext = None;
}
/// Add metadata to the data key
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
/// Master key stored in KMS backend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MasterKey {
/// Unique key identifier
pub key_id: String,
/// Key version
pub version: u32,
/// Key algorithm (e.g., "AES-256")
pub algorithm: String,
/// Key usage type
pub usage: KeyUsage,
/// Key status
pub status: KeyStatus,
/// Key description
pub description: Option<String>,
/// Associated metadata
pub metadata: HashMap<String, String>,
/// Key creation timestamp
pub created_at: DateTime<Utc>,
/// Key last rotation timestamp
pub rotated_at: Option<DateTime<Utc>>,
/// Key creator/owner
pub created_by: Option<String>,
}
impl MasterKey {
/// Create a new master key
pub fn new(key_id: String, algorithm: String, created_by: Option<String>) -> Self {
Self {
key_id,
version: 1,
algorithm,
usage: KeyUsage::EncryptDecrypt,
status: KeyStatus::Active,
description: None,
metadata: HashMap::new(),
created_at: Utc::now(),
rotated_at: None,
created_by,
}
}
/// Create a new master key with description
pub fn new_with_description(
key_id: String,
algorithm: String,
created_by: Option<String>,
description: Option<String>,
) -> Self {
Self {
key_id,
version: 1,
algorithm,
usage: KeyUsage::EncryptDecrypt,
status: KeyStatus::Active,
description,
metadata: HashMap::new(),
created_at: Utc::now(),
rotated_at: None,
created_by,
}
}
}
/// Key usage enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KeyUsage {
/// For encrypting and decrypting data
EncryptDecrypt,
/// For signing and verifying data
SignVerify,
}
/// Key status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KeyStatus {
/// Key is active and can be used
Active,
/// Key is disabled and cannot be used for new operations
Disabled,
/// Key is pending deletion
PendingDeletion,
/// Key has been deleted
Deleted,
}
/// Information about a key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyInfo {
/// Key identifier
pub key_id: String,
/// Key description
pub description: Option<String>,
/// Key algorithm
pub algorithm: String,
/// Key usage
pub usage: KeyUsage,
/// Key status
pub status: KeyStatus,
/// Key version
pub version: u32,
/// Associated metadata
pub metadata: HashMap<String, String>,
/// Key tags
pub tags: HashMap<String, String>,
/// Key creation timestamp
pub created_at: DateTime<Utc>,
/// Key last rotation timestamp
pub rotated_at: Option<DateTime<Utc>>,
/// Key creator
pub created_by: Option<String>,
}
impl From<MasterKey> for KeyInfo {
fn from(master_key: MasterKey) -> Self {
Self {
key_id: master_key.key_id,
description: master_key.description,
algorithm: master_key.algorithm,
usage: master_key.usage,
status: master_key.status,
version: master_key.version,
metadata: master_key.metadata.clone(),
tags: master_key.metadata,
created_at: master_key.created_at,
rotated_at: master_key.rotated_at,
created_by: master_key.created_by,
}
}
}
/// Request to generate a new data key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateKeyRequest {
/// Master key ID to use for encryption
pub master_key_id: String,
/// Key specification (e.g., "AES_256")
pub key_spec: String,
/// Number of bytes for the key (optional, derived from key_spec)
pub key_length: Option<u32>,
/// Encryption context for additional authenticated data
pub encryption_context: HashMap<String, String>,
/// Grant tokens for authorization (future use)
pub grant_tokens: Vec<String>,
}
impl GenerateKeyRequest {
/// Create a new generate key request
pub fn new(master_key_id: String, key_spec: String) -> Self {
Self {
master_key_id,
key_spec,
key_length: None,
encryption_context: HashMap::new(),
grant_tokens: Vec::new(),
}
}
/// Add encryption context
pub fn with_context(mut self, key: String, value: String) -> Self {
self.encryption_context.insert(key, value);
self
}
/// Set key length explicitly
pub fn with_length(mut self, length: u32) -> Self {
self.key_length = Some(length);
self
}
}
/// Request to encrypt data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptRequest {
/// Key ID to use for encryption
pub key_id: String,
/// Plaintext data to encrypt
pub plaintext: Vec<u8>,
/// Encryption context
pub encryption_context: HashMap<String, String>,
/// Grant tokens for authorization
pub grant_tokens: Vec<String>,
}
impl EncryptRequest {
/// Create a new encrypt request
pub fn new(key_id: String, plaintext: Vec<u8>) -> Self {
Self {
key_id,
plaintext,
encryption_context: HashMap::new(),
grant_tokens: Vec::new(),
}
}
/// Add encryption context
pub fn with_context(mut self, key: String, value: String) -> Self {
self.encryption_context.insert(key, value);
self
}
}
/// Response from encrypt operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptResponse {
/// Encrypted data
pub ciphertext: Vec<u8>,
/// Key ID used for encryption
pub key_id: String,
/// Key version used
pub key_version: u32,
/// Encryption algorithm used
pub algorithm: String,
}
/// Request to decrypt data
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecryptRequest {
/// Ciphertext to decrypt
pub ciphertext: Vec<u8>,
/// Encryption context (must match the context used during encryption)
pub encryption_context: HashMap<String, String>,
/// Grant tokens for authorization
pub grant_tokens: Vec<String>,
}
impl DecryptRequest {
/// Create a new decrypt request
pub fn new(ciphertext: Vec<u8>) -> Self {
Self {
ciphertext,
encryption_context: HashMap::new(),
grant_tokens: Vec::new(),
}
}
/// Add encryption context
pub fn with_context(mut self, key: String, value: String) -> Self {
self.encryption_context.insert(key, value);
self
}
}
/// Request to list keys
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListKeysRequest {
/// Maximum number of keys to return
pub limit: Option<u32>,
/// Pagination marker
pub marker: Option<String>,
/// Filter by key usage
pub usage_filter: Option<KeyUsage>,
/// Filter by key status
pub status_filter: Option<KeyStatus>,
}
impl Default for ListKeysRequest {
fn default() -> Self {
Self {
limit: Some(100),
marker: None,
usage_filter: None,
status_filter: None,
}
}
}
/// Response from list keys operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListKeysResponse {
/// List of keys
pub keys: Vec<KeyInfo>,
/// Pagination marker for next page
pub next_marker: Option<String>,
/// Whether there are more keys available
pub truncated: bool,
}
/// Operation context for auditing and access control
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationContext {
/// Operation ID for tracking
pub operation_id: Uuid,
/// User or service performing the operation
pub principal: String,
/// Source IP address
pub source_ip: Option<String>,
/// User agent
pub user_agent: Option<String>,
/// Additional context information
pub additional_context: HashMap<String, String>,
}
impl OperationContext {
/// Create a new operation context
pub fn new(principal: String) -> Self {
Self {
operation_id: Uuid::new_v4(),
principal,
source_ip: None,
user_agent: None,
additional_context: HashMap::new(),
}
}
/// Add additional context
pub fn with_context(mut self, key: String, value: String) -> Self {
self.additional_context.insert(key, value);
self
}
/// Set source IP
pub fn with_source_ip(mut self, ip: String) -> Self {
self.source_ip = Some(ip);
self
}
/// Set user agent
pub fn with_user_agent(mut self, agent: String) -> Self {
self.user_agent = Some(agent);
self
}
}
/// Object encryption context
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectEncryptionContext {
/// Bucket name
pub bucket: String,
/// Object key
pub object_key: String,
/// Content type
pub content_type: Option<String>,
/// Object size in bytes
pub size: Option<u64>,
/// Additional encryption context
pub encryption_context: HashMap<String, String>,
}
impl ObjectEncryptionContext {
/// Create a new object encryption context
pub fn new(bucket: String, object_key: String) -> Self {
Self {
bucket,
object_key,
content_type: None,
size: None,
encryption_context: HashMap::new(),
}
}
/// Set content type
pub fn with_content_type(mut self, content_type: String) -> Self {
self.content_type = Some(content_type);
self
}
/// Set object size
pub fn with_size(mut self, size: u64) -> Self {
self.size = Some(size);
self
}
/// Add encryption context
pub fn with_encryption_context(mut self, key: String, value: String) -> Self {
self.encryption_context.insert(key, value);
self
}
}
/// Encryption metadata stored with encrypted objects
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionMetadata {
/// Encryption algorithm used
pub algorithm: String,
/// Key ID used for encryption
pub key_id: String,
/// Key version
pub key_version: u32,
/// Initialization vector
pub iv: Vec<u8>,
/// Authentication tag (for AEAD ciphers)
pub tag: Option<Vec<u8>>,
/// Encryption context
pub encryption_context: HashMap<String, String>,
/// Timestamp when encrypted
pub encrypted_at: DateTime<Utc>,
/// Size of original data
pub original_size: u64,
/// Encrypted data key
pub encrypted_data_key: Vec<u8>,
}
/// Health status information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
/// Whether the KMS backend is healthy
pub kms_healthy: bool,
/// Whether encryption/decryption operations are working
pub encryption_working: bool,
/// Backend type (e.g., "local", "vault")
pub backend_type: String,
/// Additional health details
pub details: HashMap<String, String>,
}
/// Supported encryption algorithms
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum EncryptionAlgorithm {
/// AES-256-GCM
#[serde(rename = "AES256")]
Aes256,
/// ChaCha20-Poly1305
#[serde(rename = "ChaCha20Poly1305")]
ChaCha20Poly1305,
/// AWS KMS managed encryption
#[serde(rename = "aws:kms")]
AwsKms,
}
/// Key specification for data keys
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KeySpec {
/// AES-256 key (32 bytes)
Aes256,
/// AES-128 key (16 bytes)
Aes128,
/// ChaCha20 key (32 bytes)
ChaCha20,
}
impl KeySpec {
/// Get the key size in bytes
pub fn key_size(&self) -> usize {
match self {
Self::Aes256 => 32,
Self::Aes128 => 16,
Self::ChaCha20 => 32,
}
}
/// Get the string representation for backends
pub fn as_str(&self) -> &'static str {
match self {
Self::Aes256 => "AES_256",
Self::Aes128 => "AES_128",
Self::ChaCha20 => "ChaCha20",
}
}
}
/// Key metadata information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyMetadata {
/// Key identifier
pub key_id: String,
/// Key state
pub key_state: KeyState,
/// Key usage type
pub key_usage: KeyUsage,
/// Key description
pub description: Option<String>,
/// Key creation timestamp
pub creation_date: DateTime<Utc>,
/// Key deletion timestamp
pub deletion_date: Option<DateTime<Utc>>,
/// Key origin
pub origin: String,
/// Key manager
pub key_manager: String,
/// Key tags
pub tags: HashMap<String, String>,
}
/// Key state enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum KeyState {
/// Key is enabled and can be used
Enabled,
/// Key is disabled
Disabled,
/// Key is pending deletion
PendingDeletion,
/// Key is pending import
PendingImport,
/// Key is unavailable
Unavailable,
}
/// Request to create a new key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateKeyRequest {
/// Custom key name (optional, will auto-generate UUID if not provided)
pub key_name: Option<String>,
/// Key usage type
pub key_usage: KeyUsage,
/// Key description
pub description: Option<String>,
/// Key policy
pub policy: Option<String>,
/// Tags for the key
pub tags: HashMap<String, String>,
/// Origin of the key
pub origin: Option<String>,
}
impl Default for CreateKeyRequest {
fn default() -> Self {
Self {
key_name: None,
key_usage: KeyUsage::EncryptDecrypt,
description: None,
policy: None,
tags: HashMap::new(),
origin: None,
}
}
}
/// Response from create key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateKeyResponse {
/// Created key ID
pub key_id: String,
/// Key metadata
pub key_metadata: KeyMetadata,
}
/// Response from decrypt operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecryptResponse {
/// Decrypted plaintext
pub plaintext: Vec<u8>,
/// Key ID used for decryption
pub key_id: String,
/// Encryption algorithm used
pub encryption_algorithm: Option<String>,
}
/// Request to describe a key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribeKeyRequest {
/// Key ID to describe
pub key_id: String,
}
/// Response from describe key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribeKeyResponse {
/// Key metadata
pub key_metadata: KeyMetadata,
}
/// Request to generate a data key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateDataKeyRequest {
/// Key ID to use for encryption
pub key_id: String,
/// Key specification
pub key_spec: KeySpec,
/// Encryption context
pub encryption_context: HashMap<String, String>,
}
impl GenerateDataKeyRequest {
/// Create a new generate data key request
pub fn new(key_id: String, key_spec: KeySpec) -> Self {
Self {
key_id,
key_spec,
encryption_context: HashMap::new(),
}
}
}
/// Response from generate data key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateDataKeyResponse {
/// Key ID used
pub key_id: String,
/// Plaintext data key
pub plaintext_key: Vec<u8>,
/// Encrypted data key
pub ciphertext_blob: Vec<u8>,
}
impl EncryptionAlgorithm {
/// Get the algorithm name as a string
pub fn as_str(&self) -> &'static str {
match self {
Self::Aes256 => "AES256",
Self::ChaCha20Poly1305 => "ChaCha20Poly1305",
Self::AwsKms => "aws:kms",
}
}
/// Get the key size in bytes for this algorithm
pub fn key_size(&self) -> usize {
match self {
Self::Aes256 => 32, // 256 bits
Self::ChaCha20Poly1305 => 32, // 256 bits
Self::AwsKms => 32, // 256 bits (uses AES-256 internally)
}
}
/// Get the IV size in bytes for this algorithm
pub fn iv_size(&self) -> usize {
match self {
Self::Aes256 => 12, // 96 bits for GCM
Self::ChaCha20Poly1305 => 12, // 96 bits
Self::AwsKms => 12, // 96 bits (uses AES-256-GCM internally)
}
}
}
impl std::str::FromStr for EncryptionAlgorithm {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"AES256" => Ok(Self::Aes256),
"ChaCha20Poly1305" => Ok(Self::ChaCha20Poly1305),
"aws:kms" => Ok(Self::AwsKms),
_ => Err(()),
}
}
}
/// Request to delete a key
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteKeyRequest {
/// Key ID to delete
pub key_id: String,
/// Number of days to wait before deletion (7-30 days, optional)
pub pending_window_in_days: Option<u32>,
/// Force immediate deletion (for development/testing only)
pub force_immediate: Option<bool>,
}
/// Response from delete key operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteKeyResponse {
/// Key ID that was deleted or scheduled for deletion
pub key_id: String,
/// Deletion date (if scheduled)
pub deletion_date: Option<String>,
/// Key metadata
pub key_metadata: KeyMetadata,
}
/// Request to cancel key deletion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelKeyDeletionRequest {
/// Key ID to cancel deletion for
pub key_id: String,
}
/// Response from cancel key deletion operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelKeyDeletionResponse {
/// Key ID
pub key_id: String,
/// Key metadata
pub key_metadata: KeyMetadata,
}
// SECURITY: Implement Drop to automatically zero sensitive data when DataKey is dropped
impl Drop for DataKey {
fn drop(&mut self) {
self.clear_plaintext();
}
}