mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user