mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-06 13:27:43 +00:00
fix(kms): unify persisted SSE data key envelopes (#5343)
* feat(kms): implement secure handling of static KMS secret keys and enhance encryption context validation * feat: enhance local SSE DEK handling with JSON envelope format and versioning
This commit is contained in:
@@ -28,7 +28,6 @@ use md5::{Digest, Md5};
|
||||
use rustfs_kms::{KmsUnavailableError, is_data_key_envelope, types::ObjectEncryptionContext};
|
||||
use rustfs_utils::http::{SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER};
|
||||
use rustfs_utils::path::path_join_buf;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use serde::Deserialize;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use sha2::Sha256;
|
||||
@@ -44,6 +43,7 @@ const INTERNAL_ENCRYPTION_IV_HEADER: &str = "x-rustfs-encryption-iv";
|
||||
const INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER: &str = "x-rustfs-encryption-original-size";
|
||||
const SSEC_ORIGINAL_SIZE_HEADER: &str = "x-amz-server-side-encryption-customer-original-size";
|
||||
const DEFAULT_SSE_ALGORITHM: &str = "AES256";
|
||||
const LOCAL_SSE_DEK_FORMAT_VERSION: u8 = 1;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const DARE_PAYLOAD_SIZE: i64 = 64 * 1024;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
@@ -1716,16 +1716,39 @@ fn decrypt_local_sse_dek(encrypted_dek: &[u8], _kms_key_id: &str, object_context
|
||||
|
||||
fn decrypt_rustfs_local_sse_dek(encrypted_dek: &[u8]) -> Result<[u8; 32]> {
|
||||
let encrypted_dek = std::str::from_utf8(encrypted_dek).map_err(|_| Error::other("managed DEK is not valid UTF-8"))?;
|
||||
let parts: Vec<&str> = encrypted_dek.split(':').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::other("invalid managed DEK format"));
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LocalSseDekEnvelope<'a> {
|
||||
version: u8,
|
||||
nonce: &'a str,
|
||||
ciphertext: &'a str,
|
||||
}
|
||||
|
||||
let (nonce, ciphertext) = match serde_json::from_str::<LocalSseDekEnvelope<'_>>(encrypted_dek) {
|
||||
Ok(envelope) => {
|
||||
if envelope.version != LOCAL_SSE_DEK_FORMAT_VERSION {
|
||||
return Err(Error::other(format!("unsupported managed DEK format version: {}", envelope.version)));
|
||||
}
|
||||
(envelope.nonce, envelope.ciphertext)
|
||||
}
|
||||
Err(_) => {
|
||||
// DEPRECATED: read-only compatibility for persisted colon-delimited DEKs.
|
||||
// RUSTFS_COMPAT_TODO(sse-local-dek-json-v1): Remove after all supported upgrades have rewritten legacy DEKs.
|
||||
let Some((nonce, ciphertext)) = encrypted_dek.split_once(':') else {
|
||||
return Err(Error::other("invalid managed DEK format"));
|
||||
};
|
||||
if ciphertext.contains(':') {
|
||||
return Err(Error::other("invalid managed DEK format"));
|
||||
}
|
||||
(nonce, ciphertext)
|
||||
}
|
||||
};
|
||||
|
||||
let nonce_vec = BASE64_STANDARD
|
||||
.decode(parts[0])
|
||||
.decode(nonce)
|
||||
.map_err(|_| Error::other("invalid managed DEK nonce"))?;
|
||||
let ciphertext = BASE64_STANDARD
|
||||
.decode(parts[1])
|
||||
.decode(ciphertext)
|
||||
.map_err(|_| Error::other("invalid managed DEK ciphertext"))?;
|
||||
|
||||
let nonce_array: [u8; 12] = nonce_vec
|
||||
@@ -2324,9 +2347,36 @@ mod tests {
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::from([0u8; 12]);
|
||||
let ciphertext = cipher.encrypt(&nonce, dek.as_slice()).expect("encrypt managed dek");
|
||||
serde_json::json!({
|
||||
"version": LOCAL_SSE_DEK_FORMAT_VERSION,
|
||||
"nonce": BASE64_STANDARD.encode(nonce),
|
||||
"ciphertext": BASE64_STANDARD.encode(ciphertext),
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn encrypt_legacy_managed_dek_for_test(dek: [u8; 32], master_key: [u8; 32]) -> String {
|
||||
let key = Key::<Aes256Gcm>::from(master_key);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::from([0u8; 12]);
|
||||
let ciphertext = cipher.encrypt(&nonce, dek.as_slice()).expect("encrypt legacy managed dek");
|
||||
format!("{}:{}", BASE64_STANDARD.encode(nonce), BASE64_STANDARD.encode(ciphertext))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_rustfs_local_sse_dek_rejects_unknown_json_version() {
|
||||
let envelope = serde_json::json!({
|
||||
"version": LOCAL_SSE_DEK_FORMAT_VERSION + 1,
|
||||
"nonce": BASE64_STANDARD.encode([0u8; 12]),
|
||||
"ciphertext": BASE64_STANDARD.encode([0u8; 48]),
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let error =
|
||||
decrypt_rustfs_local_sse_dek(envelope.as_bytes()).expect_err("unknown local SSE DEK versions must fail closed");
|
||||
assert!(error.to_string().contains("unsupported managed DEK format version"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
fn seal_managed_s3_object_key_for_test(
|
||||
bucket: &str,
|
||||
@@ -2433,7 +2483,7 @@ mod tests {
|
||||
async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([7u8; 32])))], async {
|
||||
let data_key = [0x24; 32];
|
||||
let base_nonce = [0x14; 12];
|
||||
let encrypted_dek = encrypt_managed_dek_for_test(data_key, [7u8; 32]);
|
||||
let encrypted_dek = encrypt_legacy_managed_dek_for_test(data_key, [7u8; 32]);
|
||||
let metadata = HashMap::from([
|
||||
(
|
||||
INTERNAL_ENCRYPTION_KEY_HEADER.to_string(),
|
||||
|
||||
@@ -161,6 +161,14 @@ pub struct ConfigureStaticKmsRequest {
|
||||
pub allow_insecure_dev_defaults: Option<bool>,
|
||||
}
|
||||
|
||||
impl Drop for ConfigureStaticKmsRequest {
|
||||
fn drop(&mut self) {
|
||||
use zeroize::Zeroize;
|
||||
|
||||
self.secret_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ConfigureStaticKmsRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ConfigureStaticKmsRequest")
|
||||
|
||||
@@ -23,16 +23,17 @@
|
||||
|
||||
use crate::backends::{BackendInfo, KmsBackend, KmsClient};
|
||||
use crate::config::{BackendConfig, KmsConfig};
|
||||
use crate::encryption::DataKeyEnvelope;
|
||||
use crate::error::{KmsError, Result};
|
||||
use crate::types::*;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key, Nonce,
|
||||
aead::{Aead, KeyInit},
|
||||
aead::{Aead, KeyInit, Payload},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use jiff::Zoned;
|
||||
use rand::RngExt;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use tracing::debug;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -41,6 +42,11 @@ const NONCE_SIZE: usize = 12;
|
||||
/// AES-256 key size in bytes.
|
||||
const KEY_SIZE: usize = 32;
|
||||
|
||||
fn context_aad(context: &HashMap<String, String>) -> Result<Vec<u8>> {
|
||||
let canonical: BTreeMap<&str, &str> = context.iter().map(|(key, value)| (key.as_str(), value.as_str())).collect();
|
||||
serde_json::to_vec(&canonical).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Static single-key KMS backend.
|
||||
///
|
||||
/// Uses a pre-configured AES-256 key to derive data encryption keys. This is a
|
||||
@@ -111,21 +117,35 @@ impl KmsClient for StaticKmsBackend {
|
||||
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::from(nonce_bytes);
|
||||
let aad = context_aad(&request.encryption_context)?;
|
||||
|
||||
let encrypted = cipher
|
||||
.encrypt(&nonce, plaintext.as_ref())
|
||||
.encrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: plaintext.as_ref(),
|
||||
aad: &aad,
|
||||
},
|
||||
)
|
||||
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?;
|
||||
|
||||
// Ciphertext format: encrypted_dek || nonce
|
||||
let mut ciphertext = encrypted;
|
||||
ciphertext.extend_from_slice(&nonce_bytes);
|
||||
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,
|
||||
nonce: nonce_bytes.to_vec(),
|
||||
encryption_context: request.encryption_context.clone(),
|
||||
created_at: Zoned::now(),
|
||||
};
|
||||
let ciphertext = serde_json::to_vec(&envelope)?;
|
||||
|
||||
Ok(DataKeyInfo::new(
|
||||
self.key_id.clone(),
|
||||
0, // version is always 0 for static KMS
|
||||
0,
|
||||
Some(plaintext.to_vec()),
|
||||
ciphertext,
|
||||
"AES_256".to_string(),
|
||||
request.key_spec.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -141,14 +161,28 @@ impl KmsClient for StaticKmsBackend {
|
||||
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::from(nonce_bytes);
|
||||
let aad = context_aad(&request.encryption_context)?;
|
||||
|
||||
let encrypted = cipher
|
||||
.encrypt(&nonce, request.plaintext.as_ref())
|
||||
.encrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: request.plaintext.as_ref(),
|
||||
aad: &aad,
|
||||
},
|
||||
)
|
||||
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?;
|
||||
|
||||
// Ciphertext format: encrypted_data || nonce
|
||||
let mut ciphertext = encrypted;
|
||||
ciphertext.extend_from_slice(&nonce_bytes);
|
||||
let envelope = DataKeyEnvelope {
|
||||
key_id: uuid::Uuid::new_v4().to_string(),
|
||||
master_key_id: request.key_id.clone(),
|
||||
key_spec: "AES_256".to_string(),
|
||||
encrypted_key: encrypted,
|
||||
nonce: nonce_bytes.to_vec(),
|
||||
encryption_context: request.encryption_context.clone(),
|
||||
created_at: Zoned::now(),
|
||||
};
|
||||
let ciphertext = serde_json::to_vec(&envelope)?;
|
||||
|
||||
Ok(EncryptResponse {
|
||||
ciphertext,
|
||||
@@ -159,21 +193,39 @@ impl KmsClient for StaticKmsBackend {
|
||||
}
|
||||
|
||||
async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result<Vec<u8>> {
|
||||
if request.ciphertext.len() < NONCE_SIZE + 1 {
|
||||
return Err(KmsError::cryptographic_error("decrypt", "Ciphertext too short for static KMS format"));
|
||||
let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext)
|
||||
.map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?;
|
||||
if envelope.master_key_id != self.key_id {
|
||||
return Err(KmsError::key_not_found(&envelope.master_key_id));
|
||||
}
|
||||
|
||||
// Split ciphertext: encrypted_data || nonce(12)
|
||||
let split_at = request.ciphertext.len() - NONCE_SIZE;
|
||||
let encrypted = &request.ciphertext[..split_at];
|
||||
let nonce_slice = &request.ciphertext[split_at..];
|
||||
for (key, expected_value) in &envelope.encryption_context {
|
||||
match request.encryption_context.get(key) {
|
||||
Some(actual_value) if actual_value == expected_value => {}
|
||||
Some(actual_value) => {
|
||||
return Err(KmsError::context_mismatch(format!(
|
||||
"Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'"
|
||||
)));
|
||||
}
|
||||
None if request.encryption_context.is_empty() => {}
|
||||
None => return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))),
|
||||
}
|
||||
}
|
||||
|
||||
let key = Key::<Aes256Gcm>::from(*self.key);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::try_from(nonce_slice).map_err(|_| KmsError::cryptographic_error("nonce", "invalid nonce length"))?;
|
||||
let nonce = Nonce::try_from(envelope.nonce.as_slice())
|
||||
.map_err(|_| KmsError::cryptographic_error("nonce", "invalid nonce length"))?;
|
||||
let aad = context_aad(&envelope.encryption_context)?;
|
||||
|
||||
let plaintext = cipher
|
||||
.decrypt(&nonce, encrypted)
|
||||
.decrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: envelope.encrypted_key.as_ref(),
|
||||
aad: &aad,
|
||||
},
|
||||
)
|
||||
.map_err(|e| KmsError::cryptographic_error("AES-256-GCM decrypt", e.to_string()))?;
|
||||
|
||||
Ok(plaintext)
|
||||
@@ -317,7 +369,13 @@ impl KmsBackend for StaticKmsBackend {
|
||||
}
|
||||
|
||||
async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result<GenerateDataKeyResponse> {
|
||||
let gen_req = GenerateKeyRequest::new(request.key_id.clone(), request.key_spec.as_str().to_string());
|
||||
let gen_req = GenerateKeyRequest {
|
||||
master_key_id: request.key_id.clone(),
|
||||
key_spec: request.key_spec.as_str().to_string(),
|
||||
key_length: None,
|
||||
encryption_context: request.encryption_context,
|
||||
grant_tokens: Vec::new(),
|
||||
};
|
||||
let data_key = <Self as KmsClient>::generate_data_key(self, &gen_req, None).await?;
|
||||
|
||||
let plaintext_key = data_key
|
||||
@@ -378,8 +436,9 @@ impl KmsBackend for StaticKmsBackend {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backends::KmsClient;
|
||||
use crate::backends::{KmsBackend as KmsBackendTrait, KmsClient};
|
||||
use crate::config::{BackendConfig, KmsBackend, StaticConfig};
|
||||
use crate::encryption::is_data_key_envelope;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
|
||||
@@ -430,8 +489,12 @@ mod tests {
|
||||
assert_eq!(data_key.version, 0);
|
||||
assert!(data_key.plaintext.is_some());
|
||||
assert_eq!(data_key.plaintext.as_ref().expect("plaintext should be set").len(), 32);
|
||||
// Ciphertext should be: encrypted(32) + tag(16) + nonce(12)
|
||||
assert_eq!(data_key.ciphertext.len(), 32 + 16 + NONCE_SIZE);
|
||||
let envelope: DataKeyEnvelope =
|
||||
serde_json::from_slice(&data_key.ciphertext).expect("static data key should use a KMS envelope");
|
||||
assert_eq!(envelope.master_key_id, key_id);
|
||||
assert_eq!(envelope.encrypted_key.len(), 32 + 16);
|
||||
assert_eq!(envelope.nonce.len(), NONCE_SIZE);
|
||||
assert_eq!(envelope.encryption_context.get("bucket").map(String::as_str), Some("test-bucket"));
|
||||
|
||||
// Decrypt the data key
|
||||
let decrypt_request =
|
||||
@@ -443,6 +506,75 @@ mod tests {
|
||||
assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_data_key_uses_kms_envelope_for_sse_read_routing() {
|
||||
let (backend, key_id, _key) = create_test_backend().await;
|
||||
let request = GenerateKeyRequest::new(key_id, "AES_256".to_string())
|
||||
.with_context("bucket".to_string(), "source-bucket".to_string())
|
||||
.with_context("object".to_string(), "source-object".to_string());
|
||||
|
||||
let data_key = KmsClient::generate_data_key(&backend, &request, None)
|
||||
.await
|
||||
.expect("generate static KMS data key");
|
||||
|
||||
assert!(
|
||||
is_data_key_envelope(&data_key.ciphertext),
|
||||
"static KMS ciphertext must use the KMS envelope recognized by the SSE read path"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_data_key_rejects_a_different_encryption_context() {
|
||||
let (backend, key_id, _key) = create_test_backend().await;
|
||||
let generate_request = GenerateDataKeyRequest {
|
||||
key_id,
|
||||
key_spec: KeySpec::Aes256,
|
||||
encryption_context: HashMap::from([
|
||||
("bucket".to_string(), "source-bucket".to_string()),
|
||||
("object".to_string(), "source-object".to_string()),
|
||||
]),
|
||||
};
|
||||
let generated = KmsBackendTrait::generate_data_key(&backend, generate_request)
|
||||
.await
|
||||
.expect("generate context-bound static KMS data key");
|
||||
let decrypt_request = DecryptRequest {
|
||||
ciphertext: generated.ciphertext_blob,
|
||||
encryption_context: HashMap::from([
|
||||
("bucket".to_string(), "different-bucket".to_string()),
|
||||
("object".to_string(), "different-object".to_string()),
|
||||
]),
|
||||
grant_tokens: Vec::new(),
|
||||
};
|
||||
|
||||
let error = KmsBackendTrait::decrypt(&backend, decrypt_request)
|
||||
.await
|
||||
.expect_err("a static KMS data key must not decrypt under a different object context");
|
||||
|
||||
assert!(matches!(error, KmsError::ContextMismatch { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn generated_data_key_rejects_tampered_envelope_context() {
|
||||
let (backend, key_id, _key) = create_test_backend().await;
|
||||
let request = GenerateKeyRequest::new(key_id, "AES_256".to_string())
|
||||
.with_context("bucket".to_string(), "source-bucket".to_string());
|
||||
let generated = KmsClient::generate_data_key(&backend, &request, None)
|
||||
.await
|
||||
.expect("generate context-bound data key");
|
||||
let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope");
|
||||
envelope
|
||||
.encryption_context
|
||||
.insert("bucket".to_string(), "different-bucket".to_string());
|
||||
let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope"))
|
||||
.with_context("bucket".to_string(), "different-bucket".to_string());
|
||||
|
||||
let error = KmsClient::decrypt(&backend, &decrypt_request, None)
|
||||
.await
|
||||
.expect_err("tampering with authenticated envelope context must fail");
|
||||
|
||||
assert!(matches!(error, KmsError::CryptographicError { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_data_key_wrong_key_id() {
|
||||
let (backend, _key_id, _key) = create_test_backend().await;
|
||||
|
||||
@@ -214,9 +214,18 @@ pub struct StaticConfig {
|
||||
/// Key identifier (name) for the single configured key
|
||||
pub key_id: String,
|
||||
/// Base64-encoded 32-byte AES-256 key material (zeroed on drop)
|
||||
#[serde(skip_serializing, default)]
|
||||
pub secret_key: String,
|
||||
}
|
||||
|
||||
impl Drop for StaticConfig {
|
||||
fn drop(&mut self) {
|
||||
use zeroize::Zeroize;
|
||||
|
||||
self.secret_key.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StaticConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StaticConfig")
|
||||
@@ -1054,6 +1063,21 @@ mod tests {
|
||||
assert!(serialized.contains("persisted-token-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_kms_config_serialization_does_not_expose_key_material() {
|
||||
use base64::Engine as _;
|
||||
|
||||
let encoded_key = base64::engine::general_purpose::STANDARD.encode([0x5au8; 32]);
|
||||
let config = KmsConfig::static_kms("static-key".to_string(), encoded_key.clone());
|
||||
|
||||
let serialized = serde_json::to_string(&config).expect("static KMS config should serialize");
|
||||
|
||||
assert!(
|
||||
!serialized.contains(&encoded_key),
|
||||
"persisted static KMS configuration must not contain plaintext key material"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation() {
|
||||
let mut config = KmsConfig {
|
||||
|
||||
+16
-10
@@ -31,19 +31,25 @@ use tokio::sync::RwLock;
|
||||
pub struct KmsManager {
|
||||
backend: Arc<dyn KmsBackend>,
|
||||
cache: Arc<RwLock<KmsCache>>,
|
||||
config: KmsConfig,
|
||||
default_key_id: Option<String>,
|
||||
enable_cache: bool,
|
||||
}
|
||||
|
||||
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 }
|
||||
Self {
|
||||
backend,
|
||||
cache,
|
||||
default_key_id: config.default_key_id,
|
||||
enable_cache: config.enable_cache,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default key ID if configured
|
||||
pub fn get_default_key_id(&self) -> Option<&String> {
|
||||
self.config.default_key_id.as_ref()
|
||||
self.default_key_id.as_ref()
|
||||
}
|
||||
|
||||
/// Create a new master key
|
||||
@@ -51,7 +57,7 @@ impl KmsManager {
|
||||
let response = self.backend.create_key(request).await?;
|
||||
|
||||
// Cache the key metadata if enabled
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.put_key_metadata(&response.key_id, &response.key_metadata).await;
|
||||
}
|
||||
@@ -77,7 +83,7 @@ impl KmsManager {
|
||||
/// Describe a key
|
||||
pub async fn describe_key(&self, request: DescribeKeyRequest) -> Result<DescribeKeyResponse> {
|
||||
// Check cache first if enabled
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let cache = self.cache.read().await;
|
||||
if let Some(cached_metadata) = cache.get_key_metadata(&request.key_id).await {
|
||||
return Ok(DescribeKeyResponse {
|
||||
@@ -89,7 +95,7 @@ impl KmsManager {
|
||||
// Get from backend and cache
|
||||
let response = self.backend.describe_key(request).await?;
|
||||
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache
|
||||
.put_key_metadata(&response.key_metadata.key_id, &response.key_metadata)
|
||||
@@ -106,7 +112,7 @@ impl KmsManager {
|
||||
|
||||
/// Get cache statistics
|
||||
pub async fn cache_stats(&self) -> Option<(u64, u64)> {
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let cache = self.cache.read().await;
|
||||
Some(cache.stats())
|
||||
} else {
|
||||
@@ -116,7 +122,7 @@ impl KmsManager {
|
||||
|
||||
/// Clear the cache
|
||||
pub async fn clear_cache(&self) -> Result<()> {
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.clear().await;
|
||||
}
|
||||
@@ -128,7 +134,7 @@ impl KmsManager {
|
||||
let response = self.backend.delete_key(request).await?;
|
||||
|
||||
// Remove from cache if enabled and key is being deleted
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.remove_key_metadata(&response.key_id).await;
|
||||
}
|
||||
@@ -141,7 +147,7 @@ impl KmsManager {
|
||||
let response = self.backend.cancel_key_deletion(request).await?;
|
||||
|
||||
// Update cache if enabled
|
||||
if self.config.enable_cache {
|
||||
if self.enable_cache {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.put_key_metadata(&response.key_id, &response.key_metadata).await;
|
||||
}
|
||||
|
||||
@@ -350,11 +350,7 @@ impl ObjectEncryptionService {
|
||||
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 data_key = self.kms_manager.generate_data_key(request).await?;
|
||||
|
||||
let plaintext_key = data_key.plaintext_key;
|
||||
|
||||
@@ -431,11 +427,7 @@ impl ObjectEncryptionService {
|
||||
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}")))?;
|
||||
let decrypt_response = self.kms_manager.decrypt(decrypt_request).await?;
|
||||
|
||||
// Create cipher
|
||||
let cipher = create_cipher(&algorithm, &decrypt_response.plaintext)?;
|
||||
|
||||
@@ -94,6 +94,16 @@ impl KmsServiceManager {
|
||||
self.config.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get configuration for status and management responses without static key material.
|
||||
pub async fn get_redacted_config(&self) -> Option<KmsConfig> {
|
||||
let mut config = self.config.read().await.clone()?;
|
||||
if let BackendConfig::Static(static_config) = &mut config.backend_config {
|
||||
use zeroize::Zeroize;
|
||||
static_config.secret_key.zeroize();
|
||||
}
|
||||
Some(config)
|
||||
}
|
||||
|
||||
/// Configure KMS with new configuration
|
||||
pub async fn configure(&self, new_config: KmsConfig) -> Result<()> {
|
||||
new_config.validate()?;
|
||||
@@ -449,4 +459,22 @@ mod tests {
|
||||
assert_eq!(manager.get_status().await, KmsServiceStatus::NotConfigured);
|
||||
assert!(manager.get_config().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn redacted_config_omits_static_key_material() {
|
||||
use base64::Engine as _;
|
||||
|
||||
let manager = KmsServiceManager::new();
|
||||
let encoded_key = base64::engine::general_purpose::STANDARD.encode([0x5au8; 32]);
|
||||
manager
|
||||
.configure(KmsConfig::static_kms("static-key".to_string(), encoded_key))
|
||||
.await
|
||||
.expect("configure static KMS");
|
||||
|
||||
let config = manager.get_redacted_config().await.expect("redacted config");
|
||||
let BackendConfig::Static(static_config) = config.backend_config else {
|
||||
panic!("expected static config");
|
||||
};
|
||||
assert!(static_config.secret_key.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user