From 8e3e552576aad18e9994523151264f7cf9594611 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Sun, 2 Aug 2026 10:27:23 +0800 Subject: [PATCH] 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. --- rustfs/src/storage/sse.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/rustfs/src/storage/sse.rs b/rustfs/src/storage/sse.rs index 6023143cd..83b540147 100644 --- a/rustfs/src/storage/sse.rs +++ b/rustfs/src/storage/sse.rs @@ -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) { - 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() {