diff --git a/crates/filemeta/src/fileinfo.rs b/crates/filemeta/src/fileinfo.rs index 10be680f7..454b9d6c0 100644 --- a/crates/filemeta/src/fileinfo.rs +++ b/crates/filemeta/src/fileinfo.rs @@ -277,9 +277,10 @@ pub struct FileInfo { /// Values of these keys must never reach logs at any level. fn is_sensitive_metadata_key(key: &str) -> bool { // `is_encryption_metadata_key` covers the x-minio-internal- SSE prefix but not - // its x-rustfs-internal- twin, which the dual-key invariant writes alongside it. + // its reserved x-rustfs-internal- twin, which has no writer today but must + // stay redacted in case one appears. is_encryption_metadata_key(key) - || starts_with_ignore_ascii_case(key, "x-rustfs-internal-server-side-encryption-") + || starts_with_ignore_ascii_case(key, rustfs_utils::http::RUSTFS_INTERNAL_ENCRYPTION_PREFIX) || rustfs_utils::http::REPLICATION_SSE_TRANSPORT_PREFIXES .iter() .any(|prefix| starts_with_ignore_ascii_case(key, prefix)) diff --git a/crates/kms/src/service.rs b/crates/kms/src/service.rs index 87466b601..77fbdea3d 100644 --- a/crates/kms/src/service.rs +++ b/crates/kms/src/service.rs @@ -27,6 +27,10 @@ use base64::Engine; use jiff::Zoned; use md5::{Digest as Md5Digest, Md5}; use rand::random; +use rustfs_utils::http::object_encryption_keys::{ + INTERNAL_ENCRYPTION_ALGORITHM_HEADER, INTERNAL_ENCRYPTION_CONTEXT_HEADER, INTERNAL_ENCRYPTION_IV_HEADER, + INTERNAL_ENCRYPTION_KEY_HEADER, INTERNAL_ENCRYPTION_KEY_ID_HEADER, INTERNAL_ENCRYPTION_TAG_HEADER, +}; use std::collections::HashMap; use std::io::Cursor; use tokio::io::{AsyncRead, AsyncReadExt}; @@ -81,15 +85,6 @@ fn request_encryption_context(context: &ObjectEncryptionContext) -> HashMap context_aad(&metadata.encryption_context).unwrap_or_default(), }; headers.insert( - "x-rustfs-encryption-context".to_string(), + INTERNAL_ENCRYPTION_CONTEXT_HEADER.to_string(), String::from_utf8_lossy(&context_bytes).into_owned(), ); @@ -874,13 +869,13 @@ impl ObjectEncryptionService { }; let iv = headers - .get("x-rustfs-encryption-iv") + .get(INTERNAL_ENCRYPTION_IV_HEADER) .ok_or_else(|| KmsError::validation_error("Missing IV header"))?; let iv = base64::engine::general_purpose::STANDARD .decode(iv) .map_err(|e| KmsError::validation_error(format!("Invalid IV: {e}")))?; - let tag = if let Some(tag_str) = headers.get("x-rustfs-encryption-tag") { + let tag = if let Some(tag_str) = headers.get(INTERNAL_ENCRYPTION_TAG_HEADER) { Some( base64::engine::general_purpose::STANDARD .decode(tag_str) @@ -890,7 +885,7 @@ impl ObjectEncryptionService { None }; - let encrypted_data_key = if let Some(key_str) = headers.get("x-rustfs-encryption-key") { + let encrypted_data_key = if let Some(key_str) = headers.get(INTERNAL_ENCRYPTION_KEY_HEADER) { base64::engine::general_purpose::STANDARD .decode(key_str) .map_err(|e| KmsError::validation_error(format!("Invalid encrypted key: {e}")))? @@ -902,7 +897,7 @@ impl ObjectEncryptionService { // callers that inspect the context, but the bytes are carried through // untouched: re-serializing the parsed map is exactly how the original // ordering — and with it the ability to open the object — was lost. - let (encryption_context, context_aad) = match headers.get("x-rustfs-encryption-context") { + let (encryption_context, context_aad) = match headers.get(INTERNAL_ENCRYPTION_CONTEXT_HEADER) { Some(context_str) => ( serde_json::from_str(context_str) .map_err(|e| KmsError::validation_error(format!("Invalid encryption context: {e}")))?, diff --git a/crates/utils/src/http/header_compat.rs b/crates/utils/src/http/header_compat.rs index a9d539abe..38486ca07 100644 --- a/crates/utils/src/http/header_compat.rs +++ b/crates/utils/src/http/header_compat.rs @@ -30,8 +30,8 @@ use std::borrow::Cow; const RUSTFS_PREFIX: &str = "x-rustfs-"; const MINIO_PREFIX: &str = "x-minio-"; -const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-"; -const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-"; +pub const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-"; +pub const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-"; const MINIO_INTERNAL_ENCRYPTION_PREFIX: &str = "x-minio-internal-server-side-encryption-"; const MINIO_INTERNAL_ENCRYPTED_MULTIPART: &str = "x-minio-internal-encrypted-multipart"; const RUSTFS_ENCRYPTION_ORIGINAL_SIZE: &str = super::object_encryption_keys::INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER; diff --git a/crates/utils/src/http/object_encryption_keys.rs b/crates/utils/src/http/object_encryption_keys.rs index 394572921..2a623e8cc 100644 --- a/crates/utils/src/http/object_encryption_keys.rs +++ b/crates/utils/src/http/object_encryption_keys.rs @@ -30,6 +30,13 @@ use super::headers::{SSEC_ALGORITHM_HEADER, SSEC_KEY_MD5_HEADER}; pub const INTERNAL_ENCRYPTION_KEY_ID_HEADER: &str = "x-rustfs-encryption-key-id"; pub const INTERNAL_ENCRYPTION_KEY_HEADER: &str = "x-rustfs-encryption-key"; pub const INTERNAL_ENCRYPTION_IV_HEADER: &str = "x-rustfs-encryption-iv"; +/// Carries the AEAD algorithm the object was sealed with. +/// +/// The S3 `x-amz-server-side-encryption` header records the *SSE mode* +/// (`AES256` / `aws:kms`), not the cipher, so it cannot round-trip +/// `ChaCha20Poly1305`. Without this header a ChaCha-sealed object comes back +/// from the projection claiming `aws:kms` and is then opened with the wrong +/// cipher. pub const INTERNAL_ENCRYPTION_ALGORITHM_HEADER: &str = "x-rustfs-encryption-algorithm"; pub const INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER: &str = "x-rustfs-encryption-original-size"; pub const INTERNAL_ENCRYPTION_CONTEXT_HEADER: &str = "x-rustfs-encryption-context"; @@ -45,6 +52,15 @@ pub const MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER: &str = "X-Minio-Internal- pub const MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key"; pub const MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Context"; +/// Reserved RustFS-branded twin of the MinIO-internal SSE key family. +/// +/// No RustFS writer emits these keys today — the SSE writer persists the +/// MinIO-branded `X-Minio-Internal-Server-Side-Encryption-*` keys verbatim for +/// interoperability — but redaction (`rustfs_filemeta`) and replication +/// stripping treat the family as sensitive so that a future or third-party +/// writer cannot leak sealed material through the reserved names. +pub const RUSTFS_INTERNAL_ENCRYPTION_PREFIX: &str = "x-rustfs-internal-server-side-encryption-"; + pub const REPLICATION_SSEC_ALGORITHM_HEADER: &str = "X-Rustfs-Replication-Ssec-Algorithm"; pub const REPLICATION_SSEC_KEY_MD5_HEADER: &str = "X-Rustfs-Replication-Ssec-Key-Md5"; pub const REPLICATION_SSEC_ORIGINAL_SIZE_HEADER: &str = "X-Rustfs-Replication-Ssec-Original-Size"; @@ -125,13 +141,14 @@ pub fn ssec_replication_transport_header(stored_key: &str) -> Option<&'static st /// SSE-C material. SSE-C passthrough re-adds its keys through the transport /// mapping instead. pub fn is_replication_stripped_encryption_key(key: &str) -> bool { - // The dual-key invariant writes an x-rustfs-internal- twin next to every - // x-minio-internal- SSE key; cover it here so this predicate is safe to - // use standalone, without an is_internal_key backstop. + // The x-rustfs-internal- SSE prefix is a reserved name family with no + // writer today (see RUSTFS_INTERNAL_ENCRYPTION_PREFIX); cover it here so + // this predicate is safe to use standalone, without an is_internal_key + // backstop. super::is_encryption_metadata_key(key) || super::is_sse_header(key) || key.eq_ignore_ascii_case(SSEC_ORIGINAL_SIZE_HEADER) - || super::starts_with_ignore_ascii_case(key, "x-rustfs-internal-server-side-encryption-") + || super::starts_with_ignore_ascii_case(key, RUSTFS_INTERNAL_ENCRYPTION_PREFIX) } #[cfg(test)] diff --git a/rustfs/src/app/select_object.rs b/rustfs/src/app/select_object.rs index 581dc7c08..3a93ebade 100644 --- a/rustfs/src/app/select_object.rs +++ b/rustfs/src/app/select_object.rs @@ -30,6 +30,11 @@ use rustfs_utils::http::headers::{ AMZ_ENCRYPTION_AES, AMZ_ENCRYPTION_KMS, AMZ_SERVER_SIDE_ENCRYPTION, AMZ_SERVER_SIDE_ENCRYPTION_KMS_CONTEXT, AMZ_SERVER_SIDE_ENCRYPTION_KMS_ID, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER, }; +use rustfs_utils::http::object_encryption_keys::{ + INTERNAL_ENCRYPTION_KEY_ID_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER, + MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER, MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER, + MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, +}; use s3s::dto::{ CSVOutput, CompressionType, ContinuationEvent, EndEvent, ExpressionType, FileHeaderInfo, InputSerialization, JSONInput, JSONOutput, JSONType, OutputSerialization, Progress, ProgressEvent, QuoteFields, RecordsEvent, SelectObjectContentEvent, @@ -57,12 +62,6 @@ const BUSY_MESSAGE: &str = "The service is unavailable. Try again later."; const EMPTY_SELECT_EXPRESSION_MESSAGE: &str = "empty SQL expression"; const SLOW_DOWN_MESSAGE: &str = "Reduce your request rate."; const UNSUPPORTED_SQL_STRUCTURE_MESSAGE: &str = "We encountered an unsupported SQL structure. Check the SQL Reference."; -use rustfs_utils::http::object_encryption_keys::{ - INTERNAL_ENCRYPTION_KEY_ID_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER, - MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER, MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER, - MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, -}; - // No canonical owner exists for the KMS key ARN prefix; keep it local. const SELECT_KMS_ARN_PREFIX: &str = "arn:aws:kms:"; diff --git a/rustfs/src/storage/options.rs b/rustfs/src/storage/options.rs index 64cb40c2d..609b2ea62 100644 --- a/rustfs/src/storage/options.rs +++ b/rustfs/src/storage/options.rs @@ -19,7 +19,9 @@ use http::{HeaderMap, HeaderValue}; use rustfs_utils::http::{ AMZ_BUCKET_REPLICATION_STATUS, SUFFIX_FORCE_DELETE, SUFFIX_REPLICATION_ACTUAL_OBJECT_SIZE, SUFFIX_REPLICATION_SSEC_CRC, SUFFIX_SOURCE_DELETEMARKER, SUFFIX_SOURCE_ETAG, SUFFIX_SOURCE_MTIME, SUFFIX_SOURCE_REPLICATION_REQUEST, - SUFFIX_SOURCE_VERSION_ID, get_header, insert_header_map, + SUFFIX_SOURCE_VERSION_ID, get_header, + header_compat::{MINIO_ENCRYPTION_PREFIX, RUSTFS_ENCRYPTION_PREFIX}, + insert_header_map, metadata_compat::{MINIO_INTERNAL_PREFIX, RUSTFS_INTERNAL_PREFIX}, }; use rustfs_utils::http::{ @@ -646,8 +648,6 @@ fn archive_content_encoding_strict_mode() -> bool { const USER_METADATA_PREFIXES: &[&str] = &["x-amz-meta-", "x-rustfs-meta-", "x-minio-meta-"]; const CANONICAL_USER_METADATA_PREFIX: &str = "x-amz-meta-"; -const RUSTFS_ENCRYPTION_PREFIX: &str = "x-rustfs-encryption-"; -const MINIO_ENCRYPTION_PREFIX: &str = "x-minio-encryption-"; /// Keys a client must not be able to materialize as bare stored metadata. ///