mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 07:57:01 +00:00
feat(s3): advance parity coverage (#2278)
This commit is contained in:
@@ -57,7 +57,9 @@ struct StoredMasterKey {
|
||||
status: KeyStatus,
|
||||
description: Option<String>,
|
||||
metadata: HashMap<String, String>,
|
||||
#[serde(with = "crate::time_serde::zoned")]
|
||||
created_at: Zoned,
|
||||
#[serde(with = "crate::time_serde::option_zoned")]
|
||||
rotated_at: Option<Zoned>,
|
||||
created_by: Option<String>,
|
||||
/// Encrypted key material (32 bytes encoded in base64 for AES-256)
|
||||
@@ -840,6 +842,7 @@ impl KmsBackend for LocalKmsBackend {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn create_test_client() -> (LocalKmsClient, TempDir) {
|
||||
@@ -943,4 +946,39 @@ mod tests {
|
||||
// Note: Direct decryption of encrypt() results is not implemented in this simple version
|
||||
// In a real implementation, encrypt() would create a different envelope format
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_load_master_key_accepts_legacy_rfc3339_timestamp() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let config = LocalConfig {
|
||||
key_dir: temp_dir.path().to_path_buf(),
|
||||
master_key: None,
|
||||
file_permissions: Some(0o600),
|
||||
};
|
||||
let client = LocalKmsClient::new(config).await.expect("Failed to create client");
|
||||
|
||||
let stored_key = serde_json::json!({
|
||||
"key_id": "legacy-key",
|
||||
"version": 1u32,
|
||||
"algorithm": "AES_256",
|
||||
"usage": "EncryptDecrypt",
|
||||
"status": "Active",
|
||||
"description": serde_json::Value::Null,
|
||||
"metadata": HashMap::<String, String>::new(),
|
||||
"created_at": "2024-01-01T00:00:00+00:00",
|
||||
"rotated_at": serde_json::Value::Null,
|
||||
"created_by": "legacy-test",
|
||||
"encrypted_key_material": BASE64.encode([7u8; 32]),
|
||||
"nonce": Vec::<u8>::new()
|
||||
});
|
||||
|
||||
let key_path = client.master_key_path("legacy-key");
|
||||
fs::write(&key_path, serde_json::to_vec_pretty(&stored_key).expect("serialize test key"))
|
||||
.await
|
||||
.expect("write legacy key");
|
||||
|
||||
let key_info = client.load_master_key("legacy-key").await.expect("legacy key should load");
|
||||
assert_eq!(key_info.key_id, "legacy-key");
|
||||
assert_eq!(key_info.created_at.time_zone().iana_name(), Some("UTC"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct DataKeyEnvelope {
|
||||
pub encrypted_key: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
pub encryption_context: HashMap<String, String>,
|
||||
#[serde(with = "crate::time_serde::zoned")]
|
||||
pub created_at: Zoned,
|
||||
}
|
||||
|
||||
@@ -311,4 +312,21 @@ mod tests {
|
||||
assert_eq!(deserialized.key_id, "test-key-id");
|
||||
assert_eq!(deserialized.master_key_id, "master-key-id");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_data_key_envelope_accepts_legacy_rfc3339_timestamp() {
|
||||
let envelope_json = r#"{
|
||||
"key_id": "test-key-id",
|
||||
"master_key_id": "master-key-id",
|
||||
"key_spec": "AES_256",
|
||||
"encrypted_key": [1, 2, 3, 4],
|
||||
"nonce": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
"encryption_context": {"bucket": "test-bucket"},
|
||||
"created_at": "2024-01-01T00:00:00+00:00"
|
||||
}"#;
|
||||
|
||||
let deserialized: DataKeyEnvelope = serde_json::from_str(envelope_json).expect("Should deserialize legacy format");
|
||||
assert_eq!(deserialized.key_id, "test-key-id");
|
||||
assert_eq!(deserialized.master_key_id, "master-key-id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ mod error;
|
||||
pub mod manager;
|
||||
pub mod service;
|
||||
pub mod service_manager;
|
||||
mod time_serde;
|
||||
pub mod types;
|
||||
|
||||
// Re-export public API
|
||||
|
||||
@@ -49,6 +49,8 @@ pub struct ObjectEncryptionService {
|
||||
kms_manager: KmsManager,
|
||||
}
|
||||
|
||||
const INTERNAL_ENCRYPTION_KEY_ID_HEADER: &str = "x-rustfs-encryption-key-id";
|
||||
|
||||
/// Result of object encryption
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncryptionResult {
|
||||
@@ -604,12 +606,13 @@ impl ObjectEncryptionService {
|
||||
headers.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string());
|
||||
} else if metadata.algorithm == "AES256" {
|
||||
headers.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
|
||||
// For SSE-S3, we still need to store the key ID for internal use
|
||||
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
|
||||
} else {
|
||||
headers.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string());
|
||||
headers.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), metadata.key_id.clone());
|
||||
}
|
||||
if metadata.key_id != "sse-c" {
|
||||
headers.insert(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), metadata.key_id.clone());
|
||||
}
|
||||
|
||||
// Internal headers for decryption
|
||||
headers.insert(
|
||||
@@ -653,8 +656,14 @@ impl ObjectEncryptionService {
|
||||
|
||||
let key_id = if algorithm == "AES256" && headers.contains_key("x-amz-server-side-encryption-customer-algorithm") {
|
||||
"sse-c".to_string()
|
||||
} else if let Some(key_id) = headers.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER) {
|
||||
key_id.clone()
|
||||
} else if let Some(kms_key_id) = headers.get("x-amz-server-side-encryption-aws-kms-key-id") {
|
||||
kms_key_id.clone()
|
||||
} else if algorithm == "AES256" {
|
||||
self.get_default_key_id()
|
||||
.cloned()
|
||||
.ok_or_else(|| KmsError::validation_error("Missing key ID"))?
|
||||
} else {
|
||||
return Err(KmsError::validation_error("Missing key ID"));
|
||||
};
|
||||
@@ -821,6 +830,8 @@ mod tests {
|
||||
let headers = service.metadata_to_headers(&metadata);
|
||||
assert!(headers.contains_key("x-amz-server-side-encryption"));
|
||||
assert!(headers.contains_key("x-rustfs-encryption-iv"));
|
||||
assert!(headers.contains_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER));
|
||||
assert!(!headers.contains_key("x-amz-server-side-encryption-aws-kms-key-id"));
|
||||
|
||||
// Convert back to metadata
|
||||
let parsed_metadata = service.headers_to_metadata(&headers).expect("Failed to parse headers");
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use jiff::{Timestamp, Zoned, tz::TimeZone};
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub(crate) mod zoned {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn serialize<S>(value: &Zoned, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&value.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Zoned, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
parse_zoned_compat(&value).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod option_zoned {
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn serialize<S>(value: &Option<Zoned>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match value {
|
||||
Some(value) => serializer.serialize_some(&value.to_string()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Zoned>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
value
|
||||
.map(|value| parse_zoned_compat(&value).map_err(serde::de::Error::custom))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_zoned_compat(value: &str) -> Result<Zoned, String> {
|
||||
if let Ok(zoned) = value.parse::<Zoned>() {
|
||||
return Ok(zoned);
|
||||
}
|
||||
|
||||
let timestamp = value
|
||||
.parse::<Timestamp>()
|
||||
.map_err(|err| format!("failed to parse legacy timestamp '{value}': {err}"))?;
|
||||
Ok(timestamp.to_zoned(TimeZone::UTC))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_zoned_compat_accepts_current_zoned_format() {
|
||||
let zoned = parse_zoned_compat("2024-01-01T00:00:00+00:00[UTC]").expect("current format should parse");
|
||||
assert_eq!(zoned.time_zone().iana_name(), Some("UTC"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_zoned_compat_accepts_legacy_rfc3339_format() {
|
||||
let zoned = parse_zoned_compat("2024-01-01T00:00:00+00:00").expect("legacy format should parse");
|
||||
assert_eq!(zoned.time_zone().iana_name(), Some("UTC"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user