fix(sse): handle case-insensitive encryption metadata (#3127)

* fix(sse): handle case-insensitive encryption metadata

* test(ecstore): cover case-insensitive managed material resolution
This commit is contained in:
安正超
2026-05-30 10:38:53 +08:00
committed by GitHub
parent d5f9467368
commit 97cd19becc
2 changed files with 91 additions and 28 deletions
+63 -17
View File
@@ -652,12 +652,20 @@ fn multipart_part_numbers(parts: &[rustfs_filemeta::ObjectPartInfo]) -> Vec<usiz
parts.iter().map(|part| part.number).collect()
}
fn metadata_get<'a>(metadata: &'a HashMap<String, String>, 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<HeaderValue>) -> Result<EncryptionMaterial> {
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<HeaderValue>) -> 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<HeaderValue>) -> R
}
async fn resolve_managed_material(metadata: &HashMap<String, String>) -> Result<EncryptionMaterial> {
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<String, String>) -> 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 {
+28 -11
View File
@@ -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<Option<i64>> {
@@ -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<String, String> = 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());
}
}