mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 12:26:37 +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()
|
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> {
|
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);
|
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;
|
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"));
|
return Err(Error::other("SSE-C key MD5 mismatch"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let stored_md5 = oi
|
let stored_md5 =
|
||||||
.user_defined
|
metadata_get(&oi.user_defined, SSEC_KEY_MD5_HEADER).ok_or_else(|| Error::other("missing stored SSE-C key md5"))?;
|
||||||
.get(SSEC_KEY_MD5_HEADER)
|
if stored_md5 != expected_md5 {
|
||||||
.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"));
|
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> {
|
async fn resolve_managed_material(metadata: &HashMap<String, String>) -> Result<EncryptionMaterial> {
|
||||||
let encrypted_dek = metadata
|
let encrypted_dek =
|
||||||
.get(INTERNAL_ENCRYPTION_KEY_HEADER)
|
metadata_get(metadata, INTERNAL_ENCRYPTION_KEY_HEADER).ok_or_else(|| Error::other("missing managed encrypted DEK"))?;
|
||||||
.ok_or_else(|| Error::other("missing managed encrypted DEK"))?;
|
|
||||||
let encrypted_dek = BASE64_STANDARD
|
let encrypted_dek = BASE64_STANDARD
|
||||||
.decode(encrypted_dek)
|
.decode(encrypted_dek)
|
||||||
.map_err(|e| Error::other(format!("failed to decode managed encrypted DEK: {e}")))?;
|
.map_err(|e| Error::other(format!("failed to decode managed encrypted DEK: {e}")))?;
|
||||||
|
|
||||||
let iv_b64 = metadata
|
let iv_b64 =
|
||||||
.get(INTERNAL_ENCRYPTION_IV_HEADER)
|
metadata_get(metadata, INTERNAL_ENCRYPTION_IV_HEADER).ok_or_else(|| Error::other("missing managed encryption IV"))?;
|
||||||
.ok_or_else(|| Error::other("missing managed encryption IV"))?;
|
|
||||||
let iv = BASE64_STANDARD
|
let iv = BASE64_STANDARD
|
||||||
.decode(iv_b64)
|
.decode(iv_b64)
|
||||||
.map_err(|e| Error::other(format!("failed to decode managed encryption IV: {e}")))?;
|
.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()
|
.try_into()
|
||||||
.map_err(|_| Error::other("managed encryption IV must be 12 bytes"))?;
|
.map_err(|_| Error::other("managed encryption IV must be 12 bytes"))?;
|
||||||
|
|
||||||
let kms_key_id = metadata
|
let kms_key_id = metadata_get(metadata, INTERNAL_ENCRYPTION_KEY_ID_HEADER).unwrap_or("default");
|
||||||
.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
|
||||||
.map(String::as_str)
|
|
||||||
.unwrap_or("default");
|
|
||||||
|
|
||||||
let key_bytes = if let Some(service) = get_global_encryption_service().await {
|
let key_bytes = if let Some(service) = get_global_encryption_service().await {
|
||||||
service
|
service
|
||||||
@@ -1122,6 +1123,51 @@ mod tests {
|
|||||||
format!("{}:{}", BASE64_STANDARD.encode(nonce), BASE64_STANDARD.encode(ciphertext))
|
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]
|
#[tokio::test]
|
||||||
async fn test_get_object_reader_rejects_ssec_read_without_headers() {
|
async fn test_get_object_reader_rejects_ssec_read_without_headers() {
|
||||||
let object_info = ObjectInfo {
|
let object_info = ObjectInfo {
|
||||||
|
|||||||
@@ -387,17 +387,21 @@ impl ObjectInfo {
|
|||||||
// Corresponding to the logic in rustfs/src/sse.rs/encryption_material_to_metadata function
|
// 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};
|
use rustfs_utils::http::{SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER};
|
||||||
|
|
||||||
self.user_defined
|
self.user_defined.keys().any(|key| {
|
||||||
.keys()
|
let key = key.to_lowercase();
|
||||||
.any(|key| key.to_lowercase().starts_with("x-minio-encryption-")) // Covers MinIO metadata
|
key.starts_with("x-minio-encryption-")
|
||||||
|| self.user_defined.contains_key("x-rustfs-encryption-key") // SSE-S3/SSE-KMS
|
|| matches!(
|
||||||
|| self.user_defined.contains_key("x-rustfs-encryption-algorithm") // SSE-S3/SSE-KMS
|
key.as_str(),
|
||||||
|| self.user_defined.contains_key("x-rustfs-encryption-iv") // SSE-S3/SSE-KMS
|
"x-rustfs-encryption-key"
|
||||||
|| self.user_defined.contains_key("x-amz-server-side-encryption-aws-kms-key-id") // SSE-KMS
|
| "x-rustfs-encryption-algorithm"
|
||||||
|| self.user_defined.contains_key(SSEC_ALGORITHM_HEADER) // SSE-C
|
| "x-rustfs-encryption-iv"
|
||||||
|| self.user_defined.contains_key(SSEC_KEY_HEADER) // SSE-C
|
| "x-amz-server-side-encryption-aws-kms-key-id"
|
||||||
|| self.user_defined.contains_key(SSEC_KEY_MD5_HEADER) // SSE-C
|
| SSEC_ALGORITHM_HEADER
|
||||||
|| self.user_defined.contains_key("x-amz-server-side-encryption") // SSE-S3/SSE-KMS/SSE-C
|
| SSEC_KEY_HEADER
|
||||||
|
| SSEC_KEY_MD5_HEADER
|
||||||
|
| "x-amz-server-side-encryption"
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encryption_original_size(&self) -> std::io::Result<Option<i64>> {
|
pub fn encryption_original_size(&self) -> std::io::Result<Option<i64>> {
|
||||||
@@ -1253,4 +1257,17 @@ mod tests {
|
|||||||
|
|
||||||
assert!(info.is_encrypted());
|
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