mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 11:32:19 +00:00
fix(sse): separate SSE-S3 and KMS key providers
This commit is contained in:
@@ -56,7 +56,7 @@ moka = { workspace = true, features = ["future"] }
|
||||
# Additional dependencies
|
||||
md5 = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
rustfs-utils = { workspace = true }
|
||||
rustfs-utils = { workspace = true, features = ["http"] }
|
||||
rustfs-security-governance = { workspace = true }
|
||||
|
||||
# HTTP client for Vault
|
||||
|
||||
@@ -44,6 +44,23 @@ pub struct DataKeyEnvelope {
|
||||
pub created_at: Zoned,
|
||||
}
|
||||
|
||||
/// Return whether bytes contain a complete RustFS KMS data-key envelope.
|
||||
pub fn is_data_key_envelope(ciphertext: &[u8]) -> bool {
|
||||
const MAX_ENVELOPE_SIZE: usize = 64 * 1024;
|
||||
|
||||
if ciphertext.is_empty() || ciphertext.len() > MAX_ENVELOPE_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
serde_json::from_slice::<DataKeyEnvelope>(ciphertext).is_ok_and(|envelope| {
|
||||
!envelope.key_id.trim().is_empty()
|
||||
&& !envelope.master_key_id.trim().is_empty()
|
||||
&& envelope.key_spec == "AES_256"
|
||||
&& !envelope.encrypted_key.is_empty()
|
||||
&& (envelope.nonce.is_empty() || envelope.nonce.len() == 12)
|
||||
})
|
||||
}
|
||||
|
||||
/// Trait for encrypting and decrypting data encryption keys (DEK)
|
||||
///
|
||||
/// This trait abstracts the encryption operations used to protect
|
||||
@@ -293,6 +310,24 @@ mod tests {
|
||||
assert_eq!(deserialized.key_id, envelope.key_id);
|
||||
assert_eq!(deserialized.master_key_id, envelope.master_key_id);
|
||||
assert_eq!(deserialized.encrypted_key, envelope.encrypted_key);
|
||||
assert!(is_data_key_envelope(&serialized));
|
||||
assert!(!is_data_key_envelope(b"not-a-kms-envelope"));
|
||||
let mut boundary_envelope = serialized;
|
||||
boundary_envelope.resize(64 * 1024, b' ');
|
||||
assert!(is_data_key_envelope(&boundary_envelope));
|
||||
boundary_envelope.push(b' ');
|
||||
assert!(!is_data_key_envelope(&boundary_envelope));
|
||||
|
||||
let mut invalid = serde_json::to_value(&envelope).expect("Envelope should convert to JSON");
|
||||
invalid["key_spec"] = serde_json::Value::String("AES_128".to_string());
|
||||
assert!(!is_data_key_envelope(
|
||||
&serde_json::to_vec(&invalid).expect("Invalid envelope should serialize")
|
||||
));
|
||||
invalid["key_spec"] = serde_json::Value::String("AES_256".to_string());
|
||||
invalid["unknown"] = serde_json::Value::Bool(true);
|
||||
assert!(is_data_key_envelope(
|
||||
&serde_json::to_vec(&invalid).expect("Envelope with unknown field should serialize")
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
pub mod ciphers;
|
||||
pub mod dek;
|
||||
|
||||
pub use dek::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
|
||||
pub use dek::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material, is_data_key_envelope};
|
||||
|
||||
@@ -70,6 +70,7 @@ mod cache;
|
||||
pub mod config;
|
||||
mod encryption;
|
||||
mod error;
|
||||
mod managed_context;
|
||||
pub mod manager;
|
||||
pub mod service;
|
||||
pub mod service_manager;
|
||||
@@ -83,7 +84,11 @@ pub use api_types::{
|
||||
TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse,
|
||||
};
|
||||
pub use config::*;
|
||||
pub use encryption::is_data_key_envelope;
|
||||
pub use error::{KmsError, Result};
|
||||
pub use managed_context::{
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER, RUSTFS_ENCRYPTION_CONTEXT_HEADER, decode_managed_kms_context,
|
||||
};
|
||||
pub use manager::KmsManager;
|
||||
pub use service::{DataKey, ObjectEncryptionService};
|
||||
pub use service_manager::{
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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.
|
||||
|
||||
use crate::{KmsError, Result};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use rustfs_utils::http::get_consistent_metadata_value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const RUSTFS_ENCRYPTION_CONTEXT_HEADER: &str = "x-rustfs-encryption-context";
|
||||
pub const MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Context";
|
||||
|
||||
pub fn decode_managed_kms_context(metadata: &HashMap<String, String>) -> Result<Option<HashMap<String, String>>> {
|
||||
let minio_context = consistent_value(metadata, MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)?
|
||||
.map(|context| {
|
||||
let decoded = BASE64_STANDARD
|
||||
.decode(context)
|
||||
.map_err(|err| KmsError::serialization_error(format!("Failed to decode MinIO KMS context: {err}")))?;
|
||||
serde_json::from_slice(&decoded)
|
||||
.map_err(|err| KmsError::serialization_error(format!("Failed to parse MinIO KMS context: {err}")))
|
||||
})
|
||||
.transpose()?;
|
||||
let rustfs_context = consistent_value(metadata, RUSTFS_ENCRYPTION_CONTEXT_HEADER)?
|
||||
.map(|context| {
|
||||
serde_json::from_str(context)
|
||||
.map_err(|err| KmsError::serialization_error(format!("Failed to parse RustFS KMS context: {err}")))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
match (minio_context, rustfs_context) {
|
||||
(Some(minio), Some(rustfs)) if minio != rustfs => {
|
||||
Err(KmsError::context_mismatch("Conflicting RustFS and MinIO KMS contexts"))
|
||||
}
|
||||
(Some(context), _) | (_, Some(context)) => Ok(Some(context)),
|
||||
(None, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn consistent_value<'a>(metadata: &'a HashMap<String, String>, name: &str) -> Result<Option<&'a str>> {
|
||||
get_consistent_metadata_value(metadata, name)
|
||||
.map_err(|_| KmsError::validation_error(format!("Conflicting managed encryption metadata for {name}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_context_accepts_compatible_headers_and_rejects_conflicts() {
|
||||
let expected = HashMap::from([("tenant".to_string(), "alpha".to_string())]);
|
||||
let metadata = HashMap::from([
|
||||
(
|
||||
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
|
||||
serde_json::to_string(&expected).expect("RustFS KMS context should serialize"),
|
||||
),
|
||||
(
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
|
||||
BASE64_STANDARD.encode(serde_json::to_vec(&expected).expect("MinIO KMS context should serialize")),
|
||||
),
|
||||
]);
|
||||
assert_eq!(
|
||||
decode_managed_kms_context(&metadata).expect("matching KMS contexts should parse"),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
decode_managed_kms_context(&HashMap::from([(
|
||||
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
|
||||
serde_json::to_string(&expected).expect("legacy RustFS KMS context should serialize"),
|
||||
)]))
|
||||
.expect("legacy RustFS KMS context should parse"),
|
||||
Some(expected)
|
||||
);
|
||||
|
||||
let conflicting = HashMap::from([
|
||||
(
|
||||
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
|
||||
serde_json::to_string(&HashMap::from([("tenant", "alpha")])).expect("RustFS KMS context should serialize"),
|
||||
),
|
||||
(
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
|
||||
BASE64_STANDARD.encode(
|
||||
serde_json::to_vec(&HashMap::from([("tenant", "beta")])).expect("MinIO KMS context should serialize"),
|
||||
),
|
||||
),
|
||||
]);
|
||||
assert!(decode_managed_kms_context(&conflicting).is_err());
|
||||
|
||||
assert!(
|
||||
decode_managed_kms_context(&HashMap::from([(
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
|
||||
"not-base64".to_string(),
|
||||
)]))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
decode_managed_kms_context(&HashMap::from([(RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(), "not-json".to_string(),)]))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user