mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 05:26:50 +00:00
feature: support kms && encryt (#573)
* feat(kms): implement key management service with local and vault backends Signed-off-by: junxiang Mu <1948535941@qq.com> * feat(kms): enhance security with zeroize for sensitive data and improve key management Signed-off-by: junxiang Mu <1948535941@qq.com> * remove Hashi word Signed-off-by: junxiang Mu <1948535941@qq.com> * refactor: remove unused request structs from kms handlers Signed-off-by: junxiang Mu <1948535941@qq.com> --------- Signed-off-by: junxiang Mu <1948535941@qq.com>
This commit is contained in:
@@ -0,0 +1,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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Cipher implementations for object encryption
|
||||
|
||||
use crate::error::{KmsError, Result};
|
||||
use crate::types::EncryptionAlgorithm;
|
||||
use aes_gcm::aead::rand_core::RngCore;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key, Nonce,
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
};
|
||||
use chacha20poly1305::ChaCha20Poly1305;
|
||||
|
||||
/// Trait for object encryption ciphers
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub trait ObjectCipher: Send + Sync {
|
||||
/// Encrypt data with the given IV and AAD
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)>;
|
||||
|
||||
/// Decrypt data with the given IV, tag, and AAD
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>>;
|
||||
|
||||
/// Get the algorithm name
|
||||
fn algorithm(&self) -> &'static str;
|
||||
|
||||
/// Get the required key size in bytes
|
||||
fn key_size(&self) -> usize;
|
||||
|
||||
/// Get the required IV size in bytes
|
||||
fn iv_size(&self) -> usize;
|
||||
|
||||
/// Get the tag size in bytes
|
||||
fn tag_size(&self) -> usize;
|
||||
}
|
||||
|
||||
/// AES-256-GCM cipher implementation
|
||||
pub struct AesCipher {
|
||||
cipher: Aes256Gcm,
|
||||
}
|
||||
|
||||
impl AesCipher {
|
||||
/// Create a new AES cipher with the given key
|
||||
pub fn new(key: &[u8]) -> Result<Self> {
|
||||
if key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, key.len()));
|
||||
}
|
||||
|
||||
let key = Key::<Aes256Gcm>::from_slice(key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
Ok(Self { cipher })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectCipher for AesCipher {
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(iv);
|
||||
|
||||
// AES-GCM includes the tag in the ciphertext
|
||||
let ciphertext_with_tag = self
|
||||
.cipher
|
||||
.encrypt(nonce, aes_gcm::aead::Payload { msg: plaintext, aad })
|
||||
.map_err(KmsError::from_aes_gcm_error)?;
|
||||
|
||||
// Split ciphertext and tag
|
||||
let tag_size = self.tag_size();
|
||||
if ciphertext_with_tag.len() < tag_size {
|
||||
return Err(KmsError::cryptographic_error("AES-GCM encrypt", "Ciphertext too short for tag"));
|
||||
}
|
||||
|
||||
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
|
||||
|
||||
Ok((ciphertext.to_vec(), tag.to_vec()))
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
if tag.len() != self.tag_size() {
|
||||
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
|
||||
}
|
||||
|
||||
let nonce = Nonce::from_slice(iv);
|
||||
|
||||
// Combine ciphertext and tag for AES-GCM
|
||||
let mut ciphertext_with_tag = ciphertext.to_vec();
|
||||
ciphertext_with_tag.extend_from_slice(tag);
|
||||
|
||||
let plaintext = self
|
||||
.cipher
|
||||
.decrypt(
|
||||
nonce,
|
||||
aes_gcm::aead::Payload {
|
||||
msg: &ciphertext_with_tag,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.map_err(KmsError::from_aes_gcm_error)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static str {
|
||||
"AES-256-GCM"
|
||||
}
|
||||
|
||||
fn key_size(&self) -> usize {
|
||||
32 // 256 bits
|
||||
}
|
||||
|
||||
fn iv_size(&self) -> usize {
|
||||
12 // 96 bits for GCM
|
||||
}
|
||||
|
||||
fn tag_size(&self) -> usize {
|
||||
16 // 128 bits
|
||||
}
|
||||
}
|
||||
|
||||
/// ChaCha20-Poly1305 cipher implementation
|
||||
pub struct ChaCha20Cipher {
|
||||
cipher: ChaCha20Poly1305,
|
||||
}
|
||||
|
||||
impl ChaCha20Cipher {
|
||||
/// Create a new ChaCha20 cipher with the given key
|
||||
pub fn new(key: &[u8]) -> Result<Self> {
|
||||
if key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, key.len()));
|
||||
}
|
||||
|
||||
let key = chacha20poly1305::Key::from_slice(key);
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
|
||||
Ok(Self { cipher })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectCipher for ChaCha20Cipher {
|
||||
fn encrypt(&self, plaintext: &[u8], iv: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
let nonce = chacha20poly1305::Nonce::from_slice(iv);
|
||||
|
||||
// ChaCha20-Poly1305 includes the tag in the ciphertext
|
||||
let ciphertext_with_tag = self
|
||||
.cipher
|
||||
.encrypt(nonce, chacha20poly1305::aead::Payload { msg: plaintext, aad })
|
||||
.map_err(KmsError::from_chacha20_error)?;
|
||||
|
||||
// Split ciphertext and tag
|
||||
let tag_size = self.tag_size();
|
||||
if ciphertext_with_tag.len() < tag_size {
|
||||
return Err(KmsError::cryptographic_error("ChaCha20-Poly1305 encrypt", "Ciphertext too short for tag"));
|
||||
}
|
||||
|
||||
let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - tag_size);
|
||||
|
||||
Ok((ciphertext.to_vec(), tag.to_vec()))
|
||||
}
|
||||
|
||||
fn decrypt(&self, ciphertext: &[u8], iv: &[u8], tag: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
|
||||
if iv.len() != 12 {
|
||||
return Err(KmsError::invalid_key_size(12, iv.len()));
|
||||
}
|
||||
|
||||
if tag.len() != self.tag_size() {
|
||||
return Err(KmsError::invalid_key_size(self.tag_size(), tag.len()));
|
||||
}
|
||||
|
||||
let nonce = chacha20poly1305::Nonce::from_slice(iv);
|
||||
|
||||
// Combine ciphertext and tag for ChaCha20-Poly1305
|
||||
let mut ciphertext_with_tag = ciphertext.to_vec();
|
||||
ciphertext_with_tag.extend_from_slice(tag);
|
||||
|
||||
let plaintext = self
|
||||
.cipher
|
||||
.decrypt(
|
||||
nonce,
|
||||
chacha20poly1305::aead::Payload {
|
||||
msg: &ciphertext_with_tag,
|
||||
aad,
|
||||
},
|
||||
)
|
||||
.map_err(KmsError::from_chacha20_error)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static str {
|
||||
"ChaCha20-Poly1305"
|
||||
}
|
||||
|
||||
fn key_size(&self) -> usize {
|
||||
32 // 256 bits
|
||||
}
|
||||
|
||||
fn iv_size(&self) -> usize {
|
||||
12 // 96 bits
|
||||
}
|
||||
|
||||
fn tag_size(&self) -> usize {
|
||||
16 // 128 bits
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cipher instance for the given algorithm and key
|
||||
pub fn create_cipher(algorithm: &EncryptionAlgorithm, key: &[u8]) -> Result<Box<dyn ObjectCipher>> {
|
||||
match algorithm {
|
||||
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => Ok(Box::new(AesCipher::new(key)?)),
|
||||
EncryptionAlgorithm::ChaCha20Poly1305 => Ok(Box::new(ChaCha20Cipher::new(key)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random IV for the given algorithm
|
||||
pub fn generate_iv(algorithm: &EncryptionAlgorithm) -> Vec<u8> {
|
||||
let iv_size = match algorithm {
|
||||
EncryptionAlgorithm::Aes256 | EncryptionAlgorithm::AwsKms => 12,
|
||||
EncryptionAlgorithm::ChaCha20Poly1305 => 12,
|
||||
};
|
||||
|
||||
let mut iv = vec![0u8; iv_size];
|
||||
OsRng.fill_bytes(&mut iv);
|
||||
iv
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_aes_cipher() {
|
||||
let key = [0u8; 32]; // 256-bit key
|
||||
let cipher = AesCipher::new(&key).expect("Failed to create AES cipher");
|
||||
|
||||
let plaintext = b"Hello, World!";
|
||||
let iv = [0u8; 12]; // 96-bit IV
|
||||
let aad = b"additional data";
|
||||
|
||||
// Test encryption
|
||||
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
|
||||
assert!(!ciphertext.is_empty());
|
||||
assert_eq!(tag.len(), 16); // 128-bit tag
|
||||
|
||||
// Test decryption
|
||||
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Test properties
|
||||
assert_eq!(cipher.algorithm(), "AES-256-GCM");
|
||||
assert_eq!(cipher.key_size(), 32);
|
||||
assert_eq!(cipher.iv_size(), 12);
|
||||
assert_eq!(cipher.tag_size(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chacha20_cipher() {
|
||||
let key = [0u8; 32]; // 256-bit key
|
||||
let cipher = ChaCha20Cipher::new(&key).expect("Failed to create ChaCha20 cipher");
|
||||
|
||||
let plaintext = b"Hello, ChaCha20!";
|
||||
let iv = [0u8; 12]; // 96-bit IV
|
||||
let aad = b"additional data";
|
||||
|
||||
// Test encryption
|
||||
let (ciphertext, tag) = cipher.encrypt(plaintext, &iv, aad).expect("Encryption failed");
|
||||
assert!(!ciphertext.is_empty());
|
||||
assert_eq!(tag.len(), 16); // 128-bit tag
|
||||
|
||||
// Test decryption
|
||||
let decrypted = cipher.decrypt(&ciphertext, &iv, &tag, aad).expect("Decryption failed");
|
||||
assert_eq!(decrypted, plaintext);
|
||||
|
||||
// Test properties
|
||||
assert_eq!(cipher.algorithm(), "ChaCha20-Poly1305");
|
||||
assert_eq!(cipher.key_size(), 32);
|
||||
assert_eq!(cipher.iv_size(), 12);
|
||||
assert_eq!(cipher.tag_size(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_cipher() {
|
||||
let key = [0u8; 32];
|
||||
|
||||
// Test AES creation
|
||||
let aes_cipher = create_cipher(&EncryptionAlgorithm::Aes256, &key).expect("Failed to create AES cipher");
|
||||
assert_eq!(aes_cipher.algorithm(), "AES-256-GCM");
|
||||
|
||||
// Test ChaCha20 creation
|
||||
let chacha_cipher =
|
||||
create_cipher(&EncryptionAlgorithm::ChaCha20Poly1305, &key).expect("Failed to create ChaCha20 cipher");
|
||||
assert_eq!(chacha_cipher.algorithm(), "ChaCha20-Poly1305");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_iv() {
|
||||
let aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
|
||||
assert_eq!(aes_iv.len(), 12);
|
||||
|
||||
let chacha_iv = generate_iv(&EncryptionAlgorithm::ChaCha20Poly1305);
|
||||
assert_eq!(chacha_iv.len(), 12);
|
||||
|
||||
// IVs should be different
|
||||
let another_aes_iv = generate_iv(&EncryptionAlgorithm::Aes256);
|
||||
assert_ne!(aes_iv, another_aes_iv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_key_size() {
|
||||
let short_key = [0u8; 16]; // Too short
|
||||
|
||||
assert!(AesCipher::new(&short_key).is_err());
|
||||
assert!(ChaCha20Cipher::new(&short_key).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_iv_size() {
|
||||
let key = [0u8; 32];
|
||||
let cipher = AesCipher::new(&key).expect("Failed to create cipher");
|
||||
|
||||
let plaintext = b"test";
|
||||
let short_iv = [0u8; 8]; // Too short
|
||||
let aad = b"";
|
||||
|
||||
assert!(cipher.encrypt(plaintext, &short_iv, aad).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Object encryption service implementation
|
||||
|
||||
mod ciphers;
|
||||
pub mod service;
|
||||
|
||||
pub use service::ObjectEncryptionService;
|
||||
@@ -0,0 +1,754 @@
|
||||
// Copyright 2024 RustFS Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Object encryption service for S3-compatible encryption
|
||||
|
||||
use crate::encryption::ciphers::{create_cipher, generate_iv};
|
||||
use crate::error::{KmsError, Result};
|
||||
use crate::manager::KmsManager;
|
||||
use crate::types::*;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Data key for object encryption
|
||||
/// SECURITY: This struct automatically zeros sensitive key material when dropped
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DataKey {
|
||||
/// 256-bit encryption key - automatically zeroed on drop
|
||||
pub plaintext_key: [u8; 32],
|
||||
/// 96-bit nonce for GCM mode - not secret so no need to zero
|
||||
pub nonce: [u8; 12],
|
||||
}
|
||||
|
||||
// SECURITY: Implement Drop to automatically zero sensitive key material
|
||||
impl Drop for DataKey {
|
||||
fn drop(&mut self) {
|
||||
self.plaintext_key.zeroize();
|
||||
}
|
||||
}
|
||||
use base64::Engine;
|
||||
use rand::random;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Service for encrypting and decrypting S3 objects with KMS integration
|
||||
pub struct ObjectEncryptionService {
|
||||
kms_manager: KmsManager,
|
||||
}
|
||||
|
||||
/// Result of object encryption
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionResult {
|
||||
/// Encrypted data
|
||||
pub ciphertext: Vec<u8>,
|
||||
/// Encryption metadata to be stored with the object
|
||||
pub metadata: EncryptionMetadata,
|
||||
}
|
||||
|
||||
impl ObjectEncryptionService {
|
||||
/// Create a new object encryption service
|
||||
pub fn new(kms_manager: KmsManager) -> Self {
|
||||
Self { kms_manager }
|
||||
}
|
||||
|
||||
/// Create a new master key (delegates to KMS manager)
|
||||
pub async fn create_key(&self, request: CreateKeyRequest) -> Result<CreateKeyResponse> {
|
||||
self.kms_manager.create_key(request).await
|
||||
}
|
||||
|
||||
/// Describe a master key (delegates to KMS manager)
|
||||
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
||||
self.kms_manager.describe_key(request).await
|
||||
}
|
||||
|
||||
/// List master keys (delegates to KMS manager)
|
||||
pub async fn list_keys(&self, request: ListKeysRequest) -> Result<ListKeysResponse> {
|
||||
self.kms_manager.list_keys(request).await
|
||||
}
|
||||
|
||||
/// Generate a data encryption key (delegates to KMS manager)
|
||||
pub async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||
self.kms_manager.generate_data_key(request).await
|
||||
}
|
||||
|
||||
/// Get the default key ID
|
||||
pub fn get_default_key_id(&self) -> Option<&String> {
|
||||
self.kms_manager.get_default_key_id()
|
||||
}
|
||||
|
||||
/// Get cache statistics
|
||||
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
|
||||
self.kms_manager.cache_stats().await
|
||||
}
|
||||
|
||||
/// Clear the cache
|
||||
pub async fn clear_cache(&self) -> Result<()> {
|
||||
self.kms_manager.clear_cache().await
|
||||
}
|
||||
|
||||
/// Get backend health status
|
||||
pub async fn health_check(&self) -> Result<bool> {
|
||||
self.kms_manager.health_check().await
|
||||
}
|
||||
|
||||
/// Create a data encryption key for object encryption
|
||||
pub async fn create_data_key(
|
||||
&self,
|
||||
kms_key_id: &Option<String>,
|
||||
context: &ObjectEncryptionContext,
|
||||
) -> Result<(DataKey, Vec<u8>)> {
|
||||
// Determine the KMS key ID to use
|
||||
let actual_key_id = kms_key_id
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
|
||||
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
|
||||
|
||||
// Build encryption context
|
||||
let mut enc_context = context.encryption_context.clone();
|
||||
enc_context.insert("bucket".to_string(), context.bucket.clone());
|
||||
enc_context.insert("object_key".to_string(), context.object_key.clone());
|
||||
|
||||
let request = GenerateDataKeyRequest {
|
||||
key_id: actual_key_id.to_string(),
|
||||
key_spec: KeySpec::Aes256,
|
||||
encryption_context: enc_context,
|
||||
};
|
||||
|
||||
let data_key_response = self.kms_manager.generate_data_key(request).await?;
|
||||
|
||||
// Generate a unique random nonce for this data key
|
||||
// This ensures each object/part gets a unique base nonce for streaming encryption
|
||||
let nonce: [u8; 12] = random();
|
||||
tracing::info!("Generated random nonce for data key: {:02x?}", nonce);
|
||||
|
||||
let data_key = DataKey {
|
||||
plaintext_key: data_key_response
|
||||
.plaintext_key
|
||||
.try_into()
|
||||
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
|
||||
nonce,
|
||||
};
|
||||
|
||||
Ok((data_key, data_key_response.ciphertext_blob))
|
||||
}
|
||||
|
||||
/// Decrypt a data encryption key
|
||||
pub async fn decrypt_data_key(&self, encrypted_key: &[u8], _context: &ObjectEncryptionContext) -> Result<DataKey> {
|
||||
let decrypt_request = DecryptRequest {
|
||||
ciphertext: encrypted_key.to_vec(),
|
||||
encryption_context: HashMap::new(),
|
||||
grant_tokens: Vec::new(),
|
||||
};
|
||||
|
||||
let decrypt_response = self.kms_manager.decrypt(decrypt_request).await?;
|
||||
|
||||
let data_key = DataKey {
|
||||
plaintext_key: decrypt_response
|
||||
.plaintext
|
||||
.try_into()
|
||||
.map_err(|_| KmsError::internal_error("Invalid key length"))?,
|
||||
nonce: [0u8; 12], // This will be replaced by stored nonce during GET
|
||||
};
|
||||
|
||||
Ok(data_key)
|
||||
}
|
||||
|
||||
/// Encrypt object data using server-side encryption
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bucket` - S3 bucket name
|
||||
/// * `object_key` - S3 object key
|
||||
/// * `reader` - Data reader
|
||||
/// * `algorithm` - Encryption algorithm to use
|
||||
/// * `kms_key_id` - Optional KMS key ID (uses default if None)
|
||||
/// * `encryption_context` - Additional encryption context
|
||||
///
|
||||
/// # Returns
|
||||
/// EncryptionResult containing encrypted data and metadata
|
||||
pub async fn encrypt_object<R>(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object_key: &str,
|
||||
mut reader: R,
|
||||
algorithm: &EncryptionAlgorithm,
|
||||
kms_key_id: Option<&str>,
|
||||
encryption_context: Option<&HashMap<String, String>>,
|
||||
) -> Result<EncryptionResult>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
debug!("Encrypting object {}/{} with algorithm {:?}", bucket, object_key, algorithm);
|
||||
|
||||
// Read all data (for simplicity - in production, use streaming)
|
||||
let mut data = Vec::new();
|
||||
reader.read_to_end(&mut data).await?;
|
||||
|
||||
let original_size = data.len() as u64;
|
||||
|
||||
// Determine the KMS key ID to use
|
||||
let actual_key_id = kms_key_id
|
||||
.or_else(|| self.kms_manager.get_default_key_id().map(|s| s.as_str()))
|
||||
.ok_or_else(|| KmsError::configuration_error("No KMS key ID specified and no default configured"))?;
|
||||
|
||||
// Build encryption context
|
||||
let mut context = encryption_context.cloned().unwrap_or_default();
|
||||
context.insert("bucket".to_string(), bucket.to_string());
|
||||
context.insert("object".to_string(), object_key.to_string());
|
||||
context.insert("algorithm".to_string(), algorithm.as_str().to_string());
|
||||
|
||||
// Auto-create key for SSE-S3 if it doesn't exist
|
||||
if algorithm == &EncryptionAlgorithm::Aes256 {
|
||||
let describe_req = DescribeKeyRequest {
|
||||
key_id: actual_key_id.to_string(),
|
||||
};
|
||||
if let Err(KmsError::KeyNotFound { .. }) = self.kms_manager.describe_key(describe_req).await {
|
||||
info!("Auto-creating SSE-S3 key: {}", actual_key_id);
|
||||
let create_req = CreateKeyRequest {
|
||||
key_name: Some(actual_key_id.to_string()),
|
||||
key_usage: KeyUsage::EncryptDecrypt,
|
||||
description: Some("Auto-created SSE-S3 key".to_string()),
|
||||
policy: None,
|
||||
tags: HashMap::new(),
|
||||
origin: None,
|
||||
};
|
||||
self.kms_manager
|
||||
.create_key(create_req)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to auto-create SSE-S3 key {}: {}", actual_key_id, e)))?;
|
||||
}
|
||||
} else {
|
||||
// For SSE-KMS, key must exist
|
||||
let describe_req = DescribeKeyRequest {
|
||||
key_id: actual_key_id.to_string(),
|
||||
};
|
||||
self.kms_manager.describe_key(describe_req).await.map_err(|_| {
|
||||
KmsError::invalid_operation(format!("SSE-KMS key '{}' not found. Please create it first.", actual_key_id))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Generate data encryption key
|
||||
let request = GenerateDataKeyRequest {
|
||||
key_id: actual_key_id.to_string(),
|
||||
key_spec: KeySpec::Aes256,
|
||||
encryption_context: context.clone(),
|
||||
};
|
||||
|
||||
let data_key = self
|
||||
.kms_manager
|
||||
.generate_data_key(request)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to generate data key: {}", e)))?;
|
||||
|
||||
let plaintext_key = data_key.plaintext_key;
|
||||
|
||||
// Create cipher and generate IV
|
||||
let cipher = create_cipher(algorithm, &plaintext_key)?;
|
||||
let iv = generate_iv(algorithm);
|
||||
|
||||
// Build AAD from encryption context
|
||||
let aad = serde_json::to_vec(&context)?;
|
||||
|
||||
// Encrypt the data
|
||||
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
|
||||
|
||||
// Create encryption metadata
|
||||
let metadata = EncryptionMetadata {
|
||||
algorithm: algorithm.as_str().to_string(),
|
||||
key_id: actual_key_id.to_string(),
|
||||
key_version: 1, // Default to version 1 for now
|
||||
iv,
|
||||
tag: Some(tag),
|
||||
encryption_context: context,
|
||||
encrypted_at: chrono::Utc::now(),
|
||||
original_size,
|
||||
encrypted_data_key: data_key.ciphertext_blob,
|
||||
};
|
||||
|
||||
info!("Successfully encrypted object {}/{} ({} bytes)", bucket, object_key, original_size);
|
||||
|
||||
Ok(EncryptionResult { ciphertext, metadata })
|
||||
}
|
||||
|
||||
/// Decrypt object data
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bucket` - S3 bucket name
|
||||
/// * `object_key` - S3 object key
|
||||
/// * `ciphertext` - Encrypted data
|
||||
/// * `metadata` - Encryption metadata
|
||||
/// * `expected_context` - Expected encryption context for validation
|
||||
///
|
||||
/// # Returns
|
||||
/// Decrypted data as a reader
|
||||
pub async fn decrypt_object(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object_key: &str,
|
||||
ciphertext: Vec<u8>,
|
||||
metadata: &EncryptionMetadata,
|
||||
expected_context: Option<&HashMap<String, String>>,
|
||||
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
|
||||
debug!("Decrypting object {}/{} with algorithm {}", bucket, object_key, metadata.algorithm);
|
||||
|
||||
// Validate encryption context if provided
|
||||
if let Some(expected) = expected_context {
|
||||
self.validate_encryption_context(&metadata.encryption_context, expected)?;
|
||||
}
|
||||
|
||||
// Parse algorithm
|
||||
let algorithm = metadata
|
||||
.algorithm
|
||||
.parse::<EncryptionAlgorithm>()
|
||||
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
|
||||
|
||||
// Decrypt the data key
|
||||
let decrypt_request = DecryptRequest {
|
||||
ciphertext: metadata.encrypted_data_key.clone(),
|
||||
encryption_context: metadata.encryption_context.clone(),
|
||||
grant_tokens: Vec::new(),
|
||||
};
|
||||
|
||||
let decrypt_response = self
|
||||
.kms_manager
|
||||
.decrypt(decrypt_request)
|
||||
.await
|
||||
.map_err(|e| KmsError::backend_error(format!("Failed to decrypt data key: {}", e)))?;
|
||||
|
||||
// Create cipher
|
||||
let cipher = create_cipher(&algorithm, &decrypt_response.plaintext)?;
|
||||
|
||||
// Build AAD from encryption context
|
||||
let aad = serde_json::to_vec(&metadata.encryption_context)?;
|
||||
|
||||
// Get tag from metadata
|
||||
let tag = metadata
|
||||
.tag
|
||||
.as_ref()
|
||||
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
|
||||
|
||||
// Decrypt the data
|
||||
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
|
||||
|
||||
info!("Successfully decrypted object {}/{} ({} bytes)", bucket, object_key, plaintext.len());
|
||||
|
||||
Ok(Box::new(Cursor::new(plaintext)))
|
||||
}
|
||||
|
||||
/// Encrypt object with customer-provided key (SSE-C)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `bucket` - S3 bucket name
|
||||
/// * `object_key` - S3 object key
|
||||
/// * `reader` - Data reader
|
||||
/// * `customer_key` - Customer-provided 256-bit key
|
||||
/// * `customer_key_md5` - Optional MD5 hash of the customer key for validation
|
||||
///
|
||||
/// # Returns
|
||||
/// EncryptionResult with SSE-C metadata
|
||||
pub async fn encrypt_object_with_customer_key<R>(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object_key: &str,
|
||||
mut reader: R,
|
||||
customer_key: &[u8],
|
||||
customer_key_md5: Option<&str>,
|
||||
) -> Result<EncryptionResult>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
debug!("Encrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
|
||||
|
||||
// Validate key size
|
||||
if customer_key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, customer_key.len()));
|
||||
}
|
||||
|
||||
// Validate key MD5 if provided
|
||||
if let Some(expected_md5) = customer_key_md5 {
|
||||
let actual_md5 = md5::compute(customer_key);
|
||||
let actual_md5_hex = format!("{:x}", actual_md5);
|
||||
if actual_md5_hex != expected_md5.to_lowercase() {
|
||||
return Err(KmsError::validation_error("Customer key MD5 mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
// Read all data
|
||||
let mut data = Vec::new();
|
||||
reader.read_to_end(&mut data).await?;
|
||||
let original_size = data.len() as u64;
|
||||
|
||||
// Create cipher and generate IV
|
||||
let algorithm = EncryptionAlgorithm::Aes256;
|
||||
let cipher = create_cipher(&algorithm, customer_key)?;
|
||||
let iv = generate_iv(&algorithm);
|
||||
|
||||
// Build minimal encryption context for SSE-C
|
||||
let context = HashMap::from([
|
||||
("bucket".to_string(), bucket.to_string()),
|
||||
("object".to_string(), object_key.to_string()),
|
||||
("sse_type".to_string(), "customer".to_string()),
|
||||
]);
|
||||
|
||||
let aad = serde_json::to_vec(&context)?;
|
||||
|
||||
// Encrypt the data
|
||||
let (ciphertext, tag) = cipher.encrypt(&data, &iv, &aad)?;
|
||||
|
||||
// Create metadata (no encrypted data key for SSE-C)
|
||||
let metadata = EncryptionMetadata {
|
||||
algorithm: algorithm.as_str().to_string(),
|
||||
key_id: "sse-c".to_string(), // Special marker for SSE-C
|
||||
key_version: 1,
|
||||
iv,
|
||||
tag: Some(tag),
|
||||
encryption_context: context,
|
||||
encrypted_at: chrono::Utc::now(),
|
||||
original_size,
|
||||
encrypted_data_key: Vec::new(), // Empty for SSE-C
|
||||
};
|
||||
|
||||
info!(
|
||||
"Successfully encrypted object {}/{} with SSE-C ({} bytes)",
|
||||
bucket, object_key, original_size
|
||||
);
|
||||
|
||||
Ok(EncryptionResult { ciphertext, metadata })
|
||||
}
|
||||
|
||||
/// Decrypt object with customer-provided key (SSE-C)
|
||||
pub async fn decrypt_object_with_customer_key(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object_key: &str,
|
||||
ciphertext: Vec<u8>,
|
||||
metadata: &EncryptionMetadata,
|
||||
customer_key: &[u8],
|
||||
) -> Result<Box<dyn AsyncRead + Send + Sync + Unpin>> {
|
||||
debug!("Decrypting object {}/{} with customer-provided key (SSE-C)", bucket, object_key);
|
||||
|
||||
// Validate key size
|
||||
if customer_key.len() != 32 {
|
||||
return Err(KmsError::invalid_key_size(32, customer_key.len()));
|
||||
}
|
||||
|
||||
// Validate that this is SSE-C
|
||||
if metadata.key_id != "sse-c" {
|
||||
return Err(KmsError::invalid_operation("This object was not encrypted with SSE-C"));
|
||||
}
|
||||
|
||||
// Parse algorithm
|
||||
let algorithm = metadata
|
||||
.algorithm
|
||||
.parse::<EncryptionAlgorithm>()
|
||||
.map_err(|_| KmsError::unsupported_algorithm(&metadata.algorithm))?;
|
||||
|
||||
// Create cipher
|
||||
let cipher = create_cipher(&algorithm, customer_key)?;
|
||||
|
||||
// Build AAD from encryption context
|
||||
let aad = serde_json::to_vec(&metadata.encryption_context)?;
|
||||
|
||||
// Get tag from metadata
|
||||
let tag = metadata
|
||||
.tag
|
||||
.as_ref()
|
||||
.ok_or_else(|| KmsError::invalid_operation("Missing authentication tag"))?;
|
||||
|
||||
// Decrypt the data
|
||||
let plaintext = cipher.decrypt(&ciphertext, &metadata.iv, tag, &aad)?;
|
||||
|
||||
info!(
|
||||
"Successfully decrypted SSE-C object {}/{} ({} bytes)",
|
||||
bucket,
|
||||
object_key,
|
||||
plaintext.len()
|
||||
);
|
||||
|
||||
Ok(Box::new(Cursor::new(plaintext)))
|
||||
}
|
||||
|
||||
/// Validate encryption context
|
||||
fn validate_encryption_context(&self, actual: &HashMap<String, String>, expected: &HashMap<String, String>) -> Result<()> {
|
||||
for (key, expected_value) in expected {
|
||||
match actual.get(key) {
|
||||
Some(actual_value) if actual_value == expected_value => continue,
|
||||
Some(actual_value) => {
|
||||
return Err(KmsError::context_mismatch(format!(
|
||||
"Context mismatch for '{}': expected '{}', got '{}'",
|
||||
key, expected_value, actual_value
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(KmsError::context_mismatch(format!("Missing context key '{}'", key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert encryption metadata to HTTP headers for S3 compatibility
|
||||
pub fn metadata_to_headers(&self, metadata: &EncryptionMetadata) -> HashMap<String, String> {
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
// Standard S3 encryption headers
|
||||
if metadata.key_id == "sse-c" {
|
||||
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
|
||||
headers.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string());
|
||||
} else if metadata.algorithm == "AES256" {
|
||||
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
|
||||
// For SSE-S3, we still need to store the key ID for internal use
|
||||
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
|
||||
} else {
|
||||
headers.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string());
|
||||
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
|
||||
}
|
||||
|
||||
// Internal headers for decryption
|
||||
headers.insert(
|
||||
"x-rustfs-encryption-iv".to_string(),
|
||||
base64::engine::general_purpose::STANDARD.encode(&metadata.iv),
|
||||
);
|
||||
|
||||
if let Some(ref tag) = metadata.tag {
|
||||
headers.insert(
|
||||
"x-rustfs-encryption-tag".to_string(),
|
||||
base64::engine::general_purpose::STANDARD.encode(tag),
|
||||
);
|
||||
}
|
||||
|
||||
headers.insert(
|
||||
"x-rustfs-encryption-key".to_string(),
|
||||
base64::engine::general_purpose::STANDARD.encode(&metadata.encrypted_data_key),
|
||||
);
|
||||
|
||||
headers.insert(
|
||||
"x-rustfs-encryption-context".to_string(),
|
||||
serde_json::to_string(&metadata.encryption_context).unwrap_or_default(),
|
||||
);
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
/// Parse encryption metadata from HTTP headers
|
||||
pub fn headers_to_metadata(&self, headers: &HashMap<String, String>) -> Result<EncryptionMetadata> {
|
||||
let algorithm = headers
|
||||
.get("x-amz-server-side-encryption")
|
||||
.ok_or_else(|| KmsError::validation_error("Missing encryption algorithm header"))?
|
||||
.clone();
|
||||
|
||||
let key_id = if algorithm == "AES256" && headers.contains_key("x-amz-server-side-encryption-customer-algorithm") {
|
||||
"sse-c".to_string()
|
||||
} else if let Some(kms_key_id) = headers.get("x-amz-server-side-encryption-aws-kms-key-id") {
|
||||
kms_key_id.clone()
|
||||
} else {
|
||||
return Err(KmsError::validation_error("Missing key ID"));
|
||||
};
|
||||
|
||||
let iv = headers
|
||||
.get("x-rustfs-encryption-iv")
|
||||
.ok_or_else(|| KmsError::validation_error("Missing IV header"))?;
|
||||
let iv = base64::engine::general_purpose::STANDARD
|
||||
.decode(iv)
|
||||
.map_err(|e| KmsError::validation_error(format!("Invalid IV: {}", e)))?;
|
||||
|
||||
let tag = if let Some(tag_str) = headers.get("x-rustfs-encryption-tag") {
|
||||
Some(
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(tag_str)
|
||||
.map_err(|e| KmsError::validation_error(format!("Invalid tag: {}", e)))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let encrypted_data_key = if let Some(key_str) = headers.get("x-rustfs-encryption-key") {
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(key_str)
|
||||
.map_err(|e| KmsError::validation_error(format!("Invalid encrypted key: {}", e)))?
|
||||
} else {
|
||||
Vec::new() // Empty for SSE-C
|
||||
};
|
||||
|
||||
let encryption_context = if let Some(context_str) = headers.get("x-rustfs-encryption-context") {
|
||||
serde_json::from_str(context_str)
|
||||
.map_err(|e| KmsError::validation_error(format!("Invalid encryption context: {}", e)))?
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
Ok(EncryptionMetadata {
|
||||
algorithm,
|
||||
key_id,
|
||||
key_version: 1, // Default for parsing
|
||||
iv,
|
||||
tag,
|
||||
encryption_context,
|
||||
encrypted_at: chrono::Utc::now(),
|
||||
original_size: 0, // Not available from headers
|
||||
encrypted_data_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::KmsConfig;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn create_test_service() -> (ObjectEncryptionService, TempDir) {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let config = KmsConfig::local(temp_dir.path().to_path_buf()).with_default_key("test-key".to_string());
|
||||
let backend = Arc::new(
|
||||
crate::backends::local::LocalKmsBackend::new(config.clone())
|
||||
.await
|
||||
.expect("local backend should initialize"),
|
||||
);
|
||||
let kms_manager = KmsManager::new(backend, config);
|
||||
let service = ObjectEncryptionService::new(kms_manager);
|
||||
(service, temp_dir)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sse_s3_encryption() {
|
||||
let (service, _temp_dir) = create_test_service().await;
|
||||
|
||||
let bucket = "test-bucket";
|
||||
let object_key = "test-object";
|
||||
let data = b"Hello, SSE-S3!";
|
||||
let reader = Cursor::new(data.to_vec());
|
||||
|
||||
// Encrypt with SSE-S3 (auto-create key)
|
||||
let result = service
|
||||
.encrypt_object(
|
||||
bucket,
|
||||
object_key,
|
||||
reader,
|
||||
&EncryptionAlgorithm::Aes256,
|
||||
None, // Use default key
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Encryption failed");
|
||||
|
||||
assert!(!result.ciphertext.is_empty());
|
||||
assert_eq!(result.metadata.algorithm, "AES256");
|
||||
assert_eq!(result.metadata.original_size, data.len() as u64);
|
||||
|
||||
// Decrypt
|
||||
let decrypted_reader = service
|
||||
.decrypt_object(bucket, object_key, result.ciphertext, &result.metadata, None)
|
||||
.await
|
||||
.expect("Decryption failed");
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
let mut reader = decrypted_reader;
|
||||
reader
|
||||
.read_to_end(&mut decrypted_data)
|
||||
.await
|
||||
.expect("Failed to read decrypted data");
|
||||
assert_eq!(decrypted_data, data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sse_c_encryption() {
|
||||
let (service, _temp_dir) = create_test_service().await;
|
||||
|
||||
let bucket = "test-bucket";
|
||||
let object_key = "test-object";
|
||||
let data = b"Hello, SSE-C!";
|
||||
let reader = Cursor::new(data.to_vec());
|
||||
let customer_key = [0u8; 32]; // 256-bit key
|
||||
|
||||
// Encrypt with SSE-C
|
||||
let result = service
|
||||
.encrypt_object_with_customer_key(bucket, object_key, reader, &customer_key, None)
|
||||
.await
|
||||
.expect("SSE-C encryption failed");
|
||||
|
||||
assert!(!result.ciphertext.is_empty());
|
||||
assert_eq!(result.metadata.key_id, "sse-c");
|
||||
assert_eq!(result.metadata.original_size, data.len() as u64);
|
||||
|
||||
// Decrypt with same customer key
|
||||
let decrypted_reader = service
|
||||
.decrypt_object_with_customer_key(bucket, object_key, result.ciphertext, &result.metadata, &customer_key)
|
||||
.await
|
||||
.expect("SSE-C decryption failed");
|
||||
|
||||
let mut decrypted_data = Vec::new();
|
||||
let mut reader = decrypted_reader;
|
||||
reader
|
||||
.read_to_end(&mut decrypted_data)
|
||||
.await
|
||||
.expect("Failed to read decrypted data");
|
||||
assert_eq!(decrypted_data, data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_metadata_headers_conversion() {
|
||||
let (service, _temp_dir) = create_test_service().await;
|
||||
|
||||
let metadata = EncryptionMetadata {
|
||||
algorithm: "AES256".to_string(),
|
||||
key_id: "test-key".to_string(),
|
||||
key_version: 1,
|
||||
iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
tag: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
||||
encryption_context: HashMap::from([("bucket".to_string(), "test-bucket".to_string())]),
|
||||
encrypted_at: chrono::Utc::now(),
|
||||
original_size: 100,
|
||||
encrypted_data_key: vec![1, 2, 3, 4],
|
||||
};
|
||||
|
||||
// Convert to headers
|
||||
let headers = service.metadata_to_headers(&metadata);
|
||||
assert!(headers.contains_key("x-amz-server-side-encryption"));
|
||||
assert!(headers.contains_key("x-rustfs-encryption-iv"));
|
||||
|
||||
// Convert back to metadata
|
||||
let parsed_metadata = service.headers_to_metadata(&headers).expect("Failed to parse headers");
|
||||
assert_eq!(parsed_metadata.algorithm, metadata.algorithm);
|
||||
assert_eq!(parsed_metadata.key_id, metadata.key_id);
|
||||
assert_eq!(parsed_metadata.iv, metadata.iv);
|
||||
assert_eq!(parsed_metadata.tag, metadata.tag);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encryption_context_validation() {
|
||||
let (service, _temp_dir) = create_test_service().await;
|
||||
|
||||
let actual_context = HashMap::from([
|
||||
("bucket".to_string(), "test-bucket".to_string()),
|
||||
("object".to_string(), "test-object".to_string()),
|
||||
]);
|
||||
|
||||
let valid_expected = HashMap::from([("bucket".to_string(), "test-bucket".to_string())]);
|
||||
|
||||
let invalid_expected = HashMap::from([("bucket".to_string(), "wrong-bucket".to_string())]);
|
||||
|
||||
// Valid context should pass
|
||||
assert!(service.validate_encryption_context(&actual_context, &valid_expected).is_ok());
|
||||
|
||||
// Invalid context should fail
|
||||
assert!(
|
||||
service
|
||||
.validate_encryption_context(&actual_context, &invalid_expected)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user