From 97cd19becc6d38f11dc8e85db0b03af082205dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=89=E6=AD=A3=E8=B6=85?= Date: Sat, 30 May 2026 10:38:53 +0800 Subject: [PATCH] fix(sse): handle case-insensitive encryption metadata (#3127) * fix(sse): handle case-insensitive encryption metadata * test(ecstore): cover case-insensitive managed material resolution --- crates/ecstore/src/store_api/readers.rs | 80 +++++++++++++++++++------ crates/ecstore/src/store_api/types.rs | 39 ++++++++---- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/crates/ecstore/src/store_api/readers.rs b/crates/ecstore/src/store_api/readers.rs index 63436978d..0218bc3bb 100644 --- a/crates/ecstore/src/store_api/readers.rs +++ b/crates/ecstore/src/store_api/readers.rs @@ -652,12 +652,20 @@ fn multipart_part_numbers(parts: &[rustfs_filemeta::ObjectPartInfo]) -> Vec(metadata: &'a HashMap, key: &str) -> Option<&'a str> { + metadata.get(key).map(String::as_str).or_else(|| { + metadata + .iter() + .find_map(|(candidate, value)| candidate.eq_ignore_ascii_case(key).then_some(value.as_str())) + }) +} + async fn resolve_encryption_material(oi: &ObjectInfo, headers: &HeaderMap) -> Result { - if oi.user_defined.contains_key(SSEC_ALGORITHM_HEADER) { + if metadata_get(&oi.user_defined, SSEC_ALGORITHM_HEADER).is_some() { return resolve_ssec_material(oi, headers); } - if oi.user_defined.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER) { + if metadata_get(&oi.user_defined, INTERNAL_ENCRYPTION_KEY_HEADER).is_some() { return resolve_managed_material(&oi.user_defined).await; } @@ -697,11 +705,9 @@ fn resolve_ssec_material(oi: &ObjectInfo, headers: &HeaderMap) -> R return Err(Error::other("SSE-C key MD5 mismatch")); } - let stored_md5 = oi - .user_defined - .get(SSEC_KEY_MD5_HEADER) - .ok_or_else(|| Error::other("missing stored SSE-C key md5"))?; - if stored_md5 != &expected_md5 { + let stored_md5 = + metadata_get(&oi.user_defined, SSEC_KEY_MD5_HEADER).ok_or_else(|| Error::other("missing stored SSE-C key md5"))?; + if stored_md5 != expected_md5 { return Err(Error::other("SSE-C key does not match object metadata")); } @@ -712,16 +718,14 @@ fn resolve_ssec_material(oi: &ObjectInfo, headers: &HeaderMap) -> R } async fn resolve_managed_material(metadata: &HashMap) -> Result { - let encrypted_dek = metadata - .get(INTERNAL_ENCRYPTION_KEY_HEADER) - .ok_or_else(|| Error::other("missing managed encrypted DEK"))?; + let encrypted_dek = + metadata_get(metadata, INTERNAL_ENCRYPTION_KEY_HEADER).ok_or_else(|| Error::other("missing managed encrypted DEK"))?; let encrypted_dek = BASE64_STANDARD .decode(encrypted_dek) .map_err(|e| Error::other(format!("failed to decode managed encrypted DEK: {e}")))?; - let iv_b64 = metadata - .get(INTERNAL_ENCRYPTION_IV_HEADER) - .ok_or_else(|| Error::other("missing managed encryption IV"))?; + let iv_b64 = + metadata_get(metadata, INTERNAL_ENCRYPTION_IV_HEADER).ok_or_else(|| Error::other("missing managed encryption IV"))?; let iv = BASE64_STANDARD .decode(iv_b64) .map_err(|e| Error::other(format!("failed to decode managed encryption IV: {e}")))?; @@ -730,10 +734,7 @@ async fn resolve_managed_material(metadata: &HashMap) -> Result< .try_into() .map_err(|_| Error::other("managed encryption IV must be 12 bytes"))?; - let kms_key_id = metadata - .get(INTERNAL_ENCRYPTION_KEY_ID_HEADER) - .map(String::as_str) - .unwrap_or("default"); + let kms_key_id = metadata_get(metadata, INTERNAL_ENCRYPTION_KEY_ID_HEADER).unwrap_or("default"); let key_bytes = if let Some(service) = get_global_encryption_service().await { service @@ -1122,6 +1123,51 @@ mod tests { format!("{}:{}", BASE64_STANDARD.encode(nonce), BASE64_STANDARD.encode(ciphertext)) } + #[tokio::test] + async fn resolve_managed_material_accepts_case_insensitive_metadata_keys() { + async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([0u8; 32])))], async { + let data_key = [0x24; 32]; + let base_nonce = [0x14; 12]; + let encrypted_dek = encrypt_managed_dek_for_test(data_key, [0u8; 32]); + let metadata = HashMap::from([ + ("X-Rustfs-Encryption-Key".to_string(), BASE64_STANDARD.encode(encrypted_dek.as_bytes())), + ("X-Rustfs-Encryption-IV".to_string(), BASE64_STANDARD.encode(base_nonce)), + ]); + + let material = resolve_managed_material(&metadata) + .await + .expect("managed material should resolve mixed-case metadata keys"); + + assert_eq!(material.key_bytes, data_key); + assert_eq!(material.base_nonce, base_nonce); + }) + .await; + } + + #[tokio::test] + async fn resolve_encryption_material_accepts_case_insensitive_metadata_keys() { + async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([0u8; 32])))], async { + let data_key = [0x24; 32]; + let base_nonce = [0x14; 12]; + let encrypted_dek = encrypt_managed_dek_for_test(data_key, [0u8; 32]); + let metadata = HashMap::from([ + ("X-Rustfs-Encryption-Key".to_string(), BASE64_STANDARD.encode(encrypted_dek.as_bytes())), + ("X-Rustfs-Encryption-IV".to_string(), BASE64_STANDARD.encode(base_nonce)), + ]); + let object_info = ObjectInfo { + user_defined: metadata, + ..Default::default() + }; + let material = resolve_encryption_material(&object_info, &HeaderMap::new()) + .await + .expect("resolve_encryption_material should accept mixed-case managed metadata"); + + assert_eq!(material.key_bytes, data_key); + assert_eq!(material.base_nonce, base_nonce); + }) + .await; + } + #[tokio::test] async fn test_get_object_reader_rejects_ssec_read_without_headers() { let object_info = ObjectInfo { diff --git a/crates/ecstore/src/store_api/types.rs b/crates/ecstore/src/store_api/types.rs index 9e9baaa25..42a2c7f6f 100644 --- a/crates/ecstore/src/store_api/types.rs +++ b/crates/ecstore/src/store_api/types.rs @@ -387,17 +387,21 @@ impl ObjectInfo { // Corresponding to the logic in rustfs/src/sse.rs/encryption_material_to_metadata function use rustfs_utils::http::{SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER}; - self.user_defined - .keys() - .any(|key| key.to_lowercase().starts_with("x-minio-encryption-")) // Covers MinIO metadata - || self.user_defined.contains_key("x-rustfs-encryption-key") // SSE-S3/SSE-KMS - || self.user_defined.contains_key("x-rustfs-encryption-algorithm") // SSE-S3/SSE-KMS - || self.user_defined.contains_key("x-rustfs-encryption-iv") // SSE-S3/SSE-KMS - || self.user_defined.contains_key("x-amz-server-side-encryption-aws-kms-key-id") // SSE-KMS - || self.user_defined.contains_key(SSEC_ALGORITHM_HEADER) // SSE-C - || self.user_defined.contains_key(SSEC_KEY_HEADER) // SSE-C - || self.user_defined.contains_key(SSEC_KEY_MD5_HEADER) // SSE-C - || self.user_defined.contains_key("x-amz-server-side-encryption") // SSE-S3/SSE-KMS/SSE-C + self.user_defined.keys().any(|key| { + let key = key.to_lowercase(); + key.starts_with("x-minio-encryption-") + || matches!( + key.as_str(), + "x-rustfs-encryption-key" + | "x-rustfs-encryption-algorithm" + | "x-rustfs-encryption-iv" + | "x-amz-server-side-encryption-aws-kms-key-id" + | SSEC_ALGORITHM_HEADER + | SSEC_KEY_HEADER + | SSEC_KEY_MD5_HEADER + | "x-amz-server-side-encryption" + ) + }) } pub fn encryption_original_size(&self) -> std::io::Result> { @@ -1253,4 +1257,17 @@ mod tests { assert!(info.is_encrypted()); } + + #[test] + fn is_encrypted_handles_case_insensitive_rustfs_metadata_keys() { + let mut user_defined: HashMap = HashMap::new(); + user_defined.insert("X-Rustfs-Encryption-Key".to_string(), "encrypted-key".to_string()); + + let info = ObjectInfo { + user_defined, + ..Default::default() + }; + + assert!(info.is_encrypted()); + } }