mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 05:43:14 +00:00
fix(s3): strip every encryption marker from a copy destination (#5597)
fix(sse): strip inherited SSE key-id and algorithm on copy strip_managed_encryption_metadata cleared the MinIO spellings of the managed-SSE key id and seal algorithm but not their RustFS-native counterparts, so a CopyObject destination kept the source object's x-rustfs-encryption-key-id and x-rustfs-encryption-algorithm. When the destination resolves to no server-side encryption, nothing rewrites those keys. is_object_encryption_marker treats any remaining x-rustfs-encryption-* key as proof the payload is encrypted, so the plaintext destination reports ObjectInfo::is_encrypted, the reader takes its encrypted branch, and the read fails closed with "encrypted object metadata is incomplete" because the actual key material was stripped. Add both constants to the strip list so a destination inherits no encryption marker it has no material for.
This commit is contained in:
@@ -3144,12 +3144,14 @@ pub fn is_managed_sse(server_side_encryption: &ServerSideEncryption) -> bool {
|
||||
/// Encryption metadata describes the physical source representation and must never be
|
||||
/// inherited by a plaintext destination or by a destination using a different key.
|
||||
pub fn strip_managed_encryption_metadata(metadata: &mut HashMap<String, String>) {
|
||||
const KEYS: [&str; 19] = [
|
||||
const KEYS: [&str; 21] = [
|
||||
"x-amz-server-side-encryption",
|
||||
"x-amz-server-side-encryption-aws-kms-key-id",
|
||||
"x-amz-server-side-encryption-customer-algorithm",
|
||||
"x-amz-server-side-encryption-customer-key-md5",
|
||||
SSEC_ORIGINAL_SIZE_HEADER,
|
||||
INTERNAL_ENCRYPTION_KEY_ID_HEADER,
|
||||
INTERNAL_ENCRYPTION_ALGORITHM_HEADER,
|
||||
INTERNAL_ENCRYPTION_IV_HEADER,
|
||||
"x-rustfs-encryption-tag",
|
||||
INTERNAL_ENCRYPTION_KEY_HEADER,
|
||||
@@ -4549,6 +4551,38 @@ mod tests {
|
||||
assert!(metadata.contains_key("content-type"));
|
||||
}
|
||||
|
||||
/// A copy destination must not inherit any key that `is_object_encryption_marker`
|
||||
/// accepts as proof the payload is encrypted.
|
||||
///
|
||||
/// Guards this chain: `ObjectInfo::is_encrypted` is keyed on that predicate, so a
|
||||
/// single leftover marker makes a plaintext destination report itself encrypted.
|
||||
/// `object_api::readers` then takes its encrypted branch and demands read material,
|
||||
/// but the material itself was stripped, so `sse_decryption` reports no encryption
|
||||
/// and the read fails with "encrypted object metadata is incomplete" — a destination
|
||||
/// that CopyObject wrote successfully becomes permanently unreadable.
|
||||
#[test]
|
||||
fn test_strip_managed_encryption_metadata_clears_encryption_markers() {
|
||||
let mut metadata = HashMap::from([
|
||||
("x-amz-server-side-encryption".to_string(), "aws:kms".to_string()),
|
||||
("x-amz-server-side-encryption-aws-kms-key-id".to_string(), "source-key".to_string()),
|
||||
(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), "source-key".to_string()),
|
||||
(INTERNAL_ENCRYPTION_ALGORITHM_HEADER.to_string(), "AES256".to_string()),
|
||||
(INTERNAL_ENCRYPTION_KEY_HEADER.to_string(), "wrapped-dek".to_string()),
|
||||
(INTERNAL_ENCRYPTION_IV_HEADER.to_string(), "base-nonce".to_string()),
|
||||
("content-type".to_string(), "text/plain".to_string()),
|
||||
]);
|
||||
|
||||
strip_managed_encryption_metadata(&mut metadata);
|
||||
|
||||
let inherited: Vec<&str> = metadata
|
||||
.keys()
|
||||
.filter(|key| rustfs_utils::http::is_object_encryption_marker(key))
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
assert!(inherited.is_empty(), "copy destination inherited encryption markers: {inherited:?}");
|
||||
assert!(metadata.contains_key("content-type"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
#[test]
|
||||
fn test_legacy_managed_metadata_excludes_sealed_keys() {
|
||||
|
||||
Reference in New Issue
Block a user