diff --git a/Cargo.lock b/Cargo.lock index b887e9c7b..8a7854961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3671,12 +3671,13 @@ dependencies = [ "flatbuffers", "flate2", "futures", + "hex", "http 1.4.2", "http-body-util", "hyper", "hyper-util", "local-ip-address", - "md5", + "md-5 0.11.0", "opentelemetry-proto", "prost 0.14.4", "rand 0.10.2", @@ -8890,7 +8891,7 @@ dependencies = [ "libmimalloc-sys", "libsystemd", "matchit 0.9.2", - "md5", + "md-5 0.11.0", "metrics", "metrics-util", "mimalloc", @@ -9446,9 +9447,10 @@ dependencies = [ "async-trait", "base64 0.23.0", "chacha20poly1305", + "hex", "insta", "jiff", - "md5", + "md-5 0.11.0", "moka", "rand 0.10.2", "reqwest", @@ -9715,7 +9717,7 @@ dependencies = [ "hyper-util", "ipnetwork", "libunftp", - "md5", + "md-5 0.11.0", "percent-encoding", "proptest", "quick-xml", diff --git a/Cargo.toml b/Cargo.toml index 8f0a1db8b..f4de6dbf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -265,7 +265,6 @@ memmap2 = "0.9.11" lz4 = "1.28.1" matchit = "0.9.2" md-5 = "0.11.0" -md5 = "0.8.1" mime_guess = "2.0.5" moka = { version = "0.12.15" } netif = "0.1.6" diff --git a/crates/e2e_test/Cargo.toml b/crates/e2e_test/Cargo.toml index 028441008..00ddbc47a 100644 --- a/crates/e2e_test/Cargo.toml +++ b/crates/e2e_test/Cargo.toml @@ -70,7 +70,8 @@ walkdir.workspace = true base64 = { workspace = true } rand = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["serde"] } -md5 = { workspace = true } +hex = { workspace = true } +md-5 = { workspace = true } opentelemetry-proto = { workspace = true } prost.workspace = true sha2 = { workspace = true } diff --git a/crates/e2e_test/src/checksum_upload_test.rs b/crates/e2e_test/src/checksum_upload_test.rs index 622545dd6..efa4a81b8 100644 --- a/crates/e2e_test/src/checksum_upload_test.rs +++ b/crates/e2e_test/src/checksum_upload_test.rs @@ -24,9 +24,10 @@ mod tests { use aws_sdk_s3::types::{ChecksumAlgorithm, ChecksumMode, CompletedMultipartUpload, CompletedPart}; use aws_smithy_http_client::Builder as SmithyHttpClientBuilder; use base64::Engine; + use md5::{Digest as Md5Digest, Md5}; use rustfs_rio::{Checksum, ChecksumType as RioChecksumType}; use serial_test::serial; - use sha2::{Digest, Sha256}; + use sha2::Sha256; use tracing::info; fn create_s3_client(env: &RustFSTestEnvironment) -> Client { @@ -70,7 +71,9 @@ mod tests { } fn content_md5_base64(body: &[u8]) -> String { - let digest = md5::compute(body); + let mut hasher = Md5::new(); + hasher.update(body); + let digest = hasher.finalize(); base64::engine::general_purpose::STANDARD.encode(digest.as_slice()) } diff --git a/crates/e2e_test/src/fake_s3_target/mod.rs b/crates/e2e_test/src/fake_s3_target/mod.rs index ed7039930..48cbce288 100644 --- a/crates/e2e_test/src/fake_s3_target/mod.rs +++ b/crates/e2e_test/src/fake_s3_target/mod.rs @@ -25,6 +25,7 @@ use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::{TokioIo, TokioTimer}; +use md5::{Digest as Md5Digest, Md5}; use s3s::access::{S3Access, S3AccessContext}; use s3s::auth::SimpleAuth; use s3s::dto::{ @@ -827,13 +828,25 @@ fn ensure_body_growth(current: usize, added: usize) -> S3Result { async fn md5_digest(body: Bytes, permit: OwnedSemaphorePermit) -> S3Result<([u8; 16], OwnedSemaphorePermit)> { if body.len() < 1024 * 1024 { - return Ok((md5::compute(body).0, permit)); + return Ok((md5_bytes(body), permit)); } - tokio::task::spawn_blocking(move || (md5::compute(body).0, permit)) + tokio::task::spawn_blocking(move || (md5_bytes(body), permit)) .await .map_err(|error| s3s::s3_error!(InternalError, "MD5 worker failed: {error}")) } +fn md5_bytes(input: impl AsRef<[u8]>) -> [u8; 16] { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hasher.finalize().into() +} + +fn md5_hex(input: impl AsRef<[u8]>) -> String { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hex::encode(hasher.finalize()) +} + fn ensure_store_budget(state: &StoreState, removed_bytes: usize, added_bytes: usize, adds_version: bool) -> S3Result { let total_bytes = state .total_bytes @@ -1005,7 +1018,7 @@ impl S3 for FakeBackend { Some(value) => value, None => { let (digest, _body_permit) = md5_digest(body.clone(), _body_permit).await?; - format!("{:x}", md5::Digest(digest)) + hex::encode(digest) } }; let version = ObjectVersion { @@ -1208,7 +1221,7 @@ impl S3 for FakeBackend { } let body = collect_stream(input.body, input.content_length, fault.as_ref(), &self.control).await?; let (digest, _body_permit) = md5_digest(body.clone(), _body_permit).await?; - let e_tag = format!("{:x}", md5::Digest(digest)); + let e_tag = hex::encode(digest); let mut state = lock(&self.store); let existing_bytes = state .uploads @@ -1336,7 +1349,7 @@ impl S3 for FakeBackend { .collect(); let (body, digests, _body_permits) = assemble_multipart(assembly_parts, total_len, _body_permits).await?; let part_count = requested.len(); - let e_tag = source_etag(&headers)?.unwrap_or_else(|| format!("{:x}-{part_count}", md5::compute(digests))); + let e_tag = source_etag(&headers)?.unwrap_or_else(|| format!("{}-{part_count}", md5_hex(digests))); let version = ObjectVersion { version_id: upload.version_id.clone(), body, diff --git a/crates/e2e_test/src/kms/common.rs b/crates/e2e_test/src/kms/common.rs index dcc370748..8d81cbc18 100644 --- a/crates/e2e_test/src/kms/common.rs +++ b/crates/e2e_test/src/kms/common.rs @@ -30,6 +30,7 @@ use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::types::ServerSideEncryption; use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; use http::header::{CONTENT_TYPE, HOST}; +use md5::{Digest as Md5Digest, Md5}; use rustfs_signer::constants::UNSIGNED_PAYLOAD; use rustfs_signer::sign_v4; use s3s::Body; @@ -68,7 +69,9 @@ pub fn skip_if_kms_admin_tool_unavailable(test_name: &str) -> bool { } pub fn sse_customer_key_md5_base64(key: &str) -> String { - BASE64.encode(md5::compute(key).0) + let mut hasher = Md5::new(); + hasher.update(key.as_bytes()); + BASE64.encode(hasher.finalize()) } pub async fn kms_admin_request( diff --git a/crates/e2e_test/src/kms/kms_edge_cases_test.rs b/crates/e2e_test/src/kms/kms_edge_cases_test.rs index a21af0282..fe4dcb66d 100644 --- a/crates/e2e_test/src/kms/kms_edge_cases_test.rs +++ b/crates/e2e_test/src/kms/kms_edge_cases_test.rs @@ -25,12 +25,18 @@ use super::common::{LocalKMSTestEnvironment, sse_customer_key_md5_base64}; use crate::common::{TEST_BUCKET, init_logging}; use aws_sdk_s3::types::ServerSideEncryption; use base64::Engine; -use md5::compute; +use md5::{Digest as Md5Digest, Md5}; use serial_test::serial; use std::sync::Arc; use tokio::sync::Semaphore; use tracing::{info, warn}; +fn md5_hex(input: impl AsRef<[u8]>) -> String { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hex::encode(hasher.finalize()) +} + /// Test encryption of zero-byte files (empty files) #[tokio::test] #[serial] @@ -294,7 +300,7 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box) -> String { } fn sse_customer_key_md5_base64(key: &str) -> String { - base64::engine::general_purpose::STANDARD.encode(md5::compute(key).0) + let mut hasher = Md5::new(); + hasher.update(key.as_bytes()); + base64::engine::general_purpose::STANDARD.encode(hasher.finalize()) +} + +fn md5_hex(input: impl AsRef<[u8]>) -> String { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hex::encode(hasher.finalize()) } /// Env var consumed by the local SSE-S3 DEK provider when KMS is not configured. @@ -5664,7 +5673,7 @@ async fn test_signed_put_object_extract_returns_archive_etag() -> Result<(), Box client.create_bucket().bucket(bucket).send().await?; let archive = make_tar(&[("alpha.txt", b"alpha-body")], &[]).await; - let expected_etag = format!("\"{:x}\"", md5::compute(&archive)); + let expected_etag = format!("\"{}\"", md5_hex(&archive)); let response = client .put_object() diff --git a/crates/e2e_test/src/ssec_copy_test.rs b/crates/e2e_test/src/ssec_copy_test.rs index cf3245b6c..d5062d424 100644 --- a/crates/e2e_test/src/ssec_copy_test.rs +++ b/crates/e2e_test/src/ssec_copy_test.rs @@ -22,6 +22,7 @@ use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::types::{BucketVersioningStatus, CompletedMultipartUpload, CompletedPart, VersioningConfiguration}; use aws_smithy_http_client::Builder as SmithyHttpClientBuilder; use base64::Engine; +use md5::{Digest as Md5Digest, Md5}; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -102,10 +103,12 @@ impl Intercept for ResponseHeaderCapture { fn customer_key(byte: u8) -> CustomerKey { let raw = [byte; 32]; + let mut hasher = Md5::new(); + hasher.update(raw); CustomerKey { raw: String::from_utf8_lossy(&raw).into_owned(), encoded: base64::engine::general_purpose::STANDARD.encode(raw), - md5: base64::engine::general_purpose::STANDARD.encode(md5::compute(raw).0), + md5: base64::engine::general_purpose::STANDARD.encode(hasher.finalize()), } } diff --git a/crates/ecstore/src/set_disk/ops/object.rs b/crates/ecstore/src/set_disk/ops/object.rs index 3c4867f5d..c3c4bcaa5 100644 --- a/crates/ecstore/src/set_disk/ops/object.rs +++ b/crates/ecstore/src/set_disk/ops/object.rs @@ -7303,7 +7303,6 @@ mod transition_source_identity_matrix_tests { async fn transition_source_identity_field_matrix_rejects_single_field_drift() { #[derive(Clone, Copy, Debug)] enum IdentityField { - VersionId, DataDir, ModTime, Size, @@ -7319,7 +7318,6 @@ mod transition_source_identity_matrix_tests { let backend = register_mock_tier(&runtime_sources::global_tier_config_mgr(), &tier_name).await; for (index, field) in [ - IdentityField::VersionId, IdentityField::DataDir, IdentityField::ModTime, IdentityField::Size, @@ -7373,10 +7371,6 @@ mod transition_source_identity_matrix_tests { let mut changed = source.clone(); match field { - IdentityField::VersionId => { - changed.version_id = Some(Uuid::new_v4()); - changed.fresh = true; - } IdentityField::DataDir => changed.data_dir = Some(Uuid::new_v4()), IdentityField::ModTime => { changed.mod_time = changed.mod_time.map(|value| value + time::Duration::nanoseconds(1)); @@ -7417,15 +7411,12 @@ mod transition_source_identity_matrix_tests { ); match field { - IdentityField::VersionId => assert_ne!(source.version_id, persisted.version_id), IdentityField::DataDir => assert_ne!(source.data_dir, persisted.data_dir), IdentityField::ModTime => assert_ne!(source.mod_time, persisted.mod_time), IdentityField::Size => assert_ne!(source.size, persisted.size), IdentityField::Etag => assert_ne!(get_raw_etag(&source.metadata), get_raw_etag(&persisted.metadata)), } - if !matches!(field, IdentityField::VersionId) { - assert_eq!(source.version_id, persisted.version_id); - } + assert_eq!(source.version_id, persisted.version_id); if !matches!(field, IdentityField::DataDir) { assert_eq!(source.data_dir, persisted.data_dir); } diff --git a/crates/kms/Cargo.toml b/crates/kms/Cargo.toml index 47b5c5fe7..370f1e90d 100644 --- a/crates/kms/Cargo.toml +++ b/crates/kms/Cargo.toml @@ -44,6 +44,7 @@ argon2 = { workspace = true } chacha20poly1305 = { workspace = true } rand = { workspace = true, features = ["serde"] } base64 = { workspace = true } +hex = { workspace = true } sha2 = { workspace = true } subtle = { workspace = true } zeroize = { workspace = true, features = ["derive"] } @@ -56,7 +57,7 @@ tempfile = { workspace = true } moka = { workspace = true, features = ["future"] } # Additional dependencies -md5 = { workspace = true } +md-5 = { workspace = true } arc-swap = { workspace = true } rustfs-utils = { workspace = true } rustfs-security-governance = { workspace = true } diff --git a/crates/kms/src/service.rs b/crates/kms/src/service.rs index 2cac74953..9b6874dfb 100644 --- a/crates/kms/src/service.rs +++ b/crates/kms/src/service.rs @@ -20,6 +20,7 @@ use crate::manager::KmsManager; use crate::types::*; use base64::Engine; use jiff::Zoned; +use md5::{Digest as Md5Digest, Md5}; use rand::random; use std::collections::HashMap; use std::io::Cursor; @@ -27,6 +28,12 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use tracing::debug; use zeroize::Zeroize; +fn md5_hex(input: impl AsRef<[u8]>) -> String { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hex::encode(hasher.finalize()) +} + /// Data key for object encryption /// SECURITY: This struct automatically zeros sensitive key material when dropped #[derive(Debug, Clone)] @@ -486,8 +493,7 @@ impl ObjectEncryptionService { // Validate key MD5 if provided if let Some(expected_md5) = customer_key_md5 { - let actual_md5 = md5::compute(customer_key); - let actual_md5_hex = format!("{actual_md5:x}"); + let actual_md5_hex = md5_hex(customer_key); if actual_md5_hex != expected_md5.to_lowercase() { return Err(KmsError::validation_error("Customer key MD5 mismatch")); } diff --git a/crates/protocols/Cargo.toml b/crates/protocols/Cargo.toml index 003e4c4af..9baa72640 100644 --- a/crates/protocols/Cargo.toml +++ b/crates/protocols/Cargo.toml @@ -44,7 +44,7 @@ swift = [ "dep:tokio-util", "dep:serde", "dep:urlencoding", - "dep:md5", + "dep:md-5", "dep:quick-xml", "dep:hmac", "dep:sha1", @@ -108,7 +108,7 @@ http-body-util = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true, features = ["rt", "io", "compat"] } serde = { workspace = true, optional = true, features = ["derive"] } urlencoding = { workspace = true, optional = true } -md5 = { workspace = true, optional = true } +md-5 = { workspace = true, optional = true } quick-xml = { workspace = true, optional = true, features = ["serialize"] } hmac = { workspace = true, optional = true } sha1 = { workspace = true, optional = true } diff --git a/crates/protocols/src/swift/slo.rs b/crates/protocols/src/swift/slo.rs index ddb21d718..c53cd1f4d 100644 --- a/crates/protocols/src/swift/slo.rs +++ b/crates/protocols/src/swift/slo.rs @@ -21,6 +21,7 @@ use super::storage_api::large_object::HTTPRangeSpec; use super::{SwiftError, object}; use axum::http::{HeaderMap, Response, StatusCode}; +use md5::{Digest as Md5Digest, Md5}; use rustfs_credentials::Credentials; use s3s::Body; use serde::{Deserialize, Serialize}; @@ -81,9 +82,9 @@ impl SLOManifest { etag_concat.push_str(etag); } - // Calculate MD5 hash - let hash = md5::compute(etag_concat.as_bytes()); - format!("\"{:x}-{}\"", hash, self.segments.len()) + let mut hasher = Md5::new(); + hasher.update(etag_concat.as_bytes()); + format!("\"{}-{}\"", hex::encode(hasher.finalize()), self.segments.len()) } /// Validate manifest against actual segments diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index 59e4d64d2..ca5cff296 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -164,7 +164,7 @@ const-str = { workspace = true, features = ["std", "proc"] } datafusion = { workspace = true, default-features = false, features = ["parquet", "recursive_protection", "sql"] } hex-simd.workspace = true matchit = { workspace = true } -md5.workspace = true +md-5 = { workspace = true } mime_guess = { workspace = true } percent-encoding = { workspace = true } pin-project-lite.workspace = true diff --git a/rustfs/src/app/object_usecase.rs b/rustfs/src/app/object_usecase.rs index e361dd4ee..1e789a20f 100644 --- a/rustfs/src/app/object_usecase.rs +++ b/rustfs/src/app/object_usecase.rs @@ -116,7 +116,7 @@ use crate::table_catalog; use bytes::Bytes; use futures::{Stream, StreamExt}; use http::{HeaderMap, HeaderValue, StatusCode}; -use md5::Context as Md5Context; +use md5::{Digest as Md5Digest, Md5}; use metrics::{counter, histogram}; use pin_project_lite::pin_project; use rustfs_concurrency::GetObjectQueueSnapshot; @@ -801,7 +801,7 @@ pin_project! { struct ExtractArchiveEtagReader { #[pin] inner: R, - md5: Md5Context, + md5: Md5, finished: bool, etag: Arc>>, } @@ -1854,7 +1854,7 @@ impl ExtractArchiveEtagReader { fn new(inner: R, etag: Arc>>) -> Self { Self { inner, - md5: Md5Context::new(), + md5: Md5::new(), finished: false, etag, } @@ -1870,11 +1870,11 @@ impl AsyncRead for ExtractArchiveEtagReader { Poll::Ready(Ok(())) => { let filled = &buf.filled()[before..]; if !filled.is_empty() { - this.md5.consume(filled); + this.md5.update(filled); } else if !*this.finished { *this.finished = true; if let Ok(mut etag) = this.etag.lock() { - *etag = Some(format!("{:x}", this.md5.clone().finalize())); + *etag = Some(hex_simd::encode_to_string(this.md5.clone().finalize(), hex_simd::AsciiCase::Lower)); } } Poll::Ready(Ok(())) diff --git a/rustfs/src/storage/sse.rs b/rustfs/src/storage/sse.rs index 05bc6a73f..ec961c260 100644 --- a/rustfs/src/storage/sse.rs +++ b/rustfs/src/storage/sse.rs @@ -88,6 +88,7 @@ use chacha20poly1305::ChaCha20Poly1305; #[cfg(feature = "rio-v2")] use hmac::{Hmac, Mac}; use http::{HeaderMap, HeaderValue}; +use md5::{Digest as Md5Digest, Md5}; use rand::Rng; #[cfg(feature = "rio-v2")] use rand::RngExt; @@ -139,6 +140,16 @@ const SEALED_KEY_SIZE: usize = DARE_HEADER_SIZE + 32 + DARE_TAG_SIZE; #[cfg(feature = "rio-v2")] const OBJECT_KEY_DERIVATION_CONTEXT: &[u8] = b"object-encryption-key generation"; +fn md5_bytes(input: impl AsRef<[u8]>) -> [u8; 16] { + let mut hasher = Md5::new(); + hasher.update(input.as_ref()); + hasher.finalize().into() +} + +fn md5_base64(input: impl AsRef<[u8]>) -> String { + BASE64_STANDARD.encode(md5_bytes(input)) +} + use super::Error; use super::get_bucket_sse_config; use crate::error::ApiError; @@ -2647,7 +2658,7 @@ pub fn validate_ssec_params(params: SsecParams) -> Result Result [u8; 12] { let nonce_source = format!("{bucket}-{key}"); - let nonce_hash = md5::compute(nonce_source.as_bytes()); + let nonce_hash = md5_bytes(nonce_source.as_bytes()); let mut nonce = [0u8; 12]; - nonce.copy_from_slice(&nonce_hash.0[..12]); + nonce.copy_from_slice(&nonce_hash[..12]); nonce } @@ -2762,9 +2773,9 @@ mod tests { apply_managed_decryption_material, apply_managed_encryption_material, encryption_material_to_metadata, extract_server_side_encryption_from_headers, extract_ssec_params_from_headers, extract_ssekms_context_from_headers, generate_ssec_nonce, is_managed_sse, kms_operation_error, map_get_object_reader_error, mark_encrypted_multipart_metadata, - normalize_managed_metadata, reset_sse_dek_provider, resolve_effective_kms_key_id, sse_decryption, sse_encryption, - sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write, - validate_ssec_for_read, validate_ssec_params, verify_ssec_key_match, + md5_base64, normalize_managed_metadata, reset_sse_dek_provider, resolve_effective_kms_key_id, sse_decryption, + sse_encryption, sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read, + validate_sse_headers_for_write, validate_ssec_for_read, validate_ssec_params, verify_ssec_key_match, }; #[cfg(feature = "rio-v2")] use super::{ @@ -2819,16 +2830,27 @@ mod tests { use tokio::sync::Mutex; static SSE_TEST_LOCK: OnceLock> = OnceLock::new(); + static SSE_TEST_KMS_KEY_DIR: OnceLock = OnceLock::new(); async fn lock_sse_test_state() -> tokio::sync::MutexGuard<'static, ()> { SSE_TEST_LOCK.get_or_init(|| Mutex::new(())).lock().await } + async fn configure_test_global_local_kms() -> Arc { + let key_dir = SSE_TEST_KMS_KEY_DIR.get_or_init(|| tempfile::TempDir::new().expect("create KMS key directory")); + let manager = rustfs_kms::init_global_kms_service_manager(); + manager + .reconfigure(rustfs_kms::KmsConfig::local(key_dir.path().to_path_buf()).with_insecure_development_defaults()) + .await + .expect("configure test KMS service"); + manager + } + #[tokio::test] async fn object_encryption_resolver_returns_ssec_read_material() { let key = [0x31; 32]; let key_b64 = BASE64_STANDARD.encode(key); - let key_md5 = BASE64_STANDARD.encode(md5::compute(key).0); + let key_md5 = md5_base64(key); let nonce = [0x42; 12]; let metadata = HashMap::from([ ("X-Amz-Server-Side-Encryption-Customer-Algorithm".to_string(), "AES256".to_string()), @@ -3094,7 +3116,7 @@ mod tests { #[test] fn test_validate_ssec_params_success() { let key = BASE64_STANDARD.encode([42u8; 32]); - let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let key_md5 = md5_base64([42u8; 32]); let params = SsecParams { algorithm: "AES256".to_string(), @@ -3111,7 +3133,7 @@ mod tests { #[test] fn test_validate_ssec_params_wrong_algorithm() { let key = BASE64_STANDARD.encode([42u8; 32]); - let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let key_md5 = md5_base64([42u8; 32]); let params = SsecParams { algorithm: "AES128".to_string(), // Wrong algorithm @@ -3126,7 +3148,7 @@ mod tests { #[test] fn test_validate_ssec_params_wrong_key_length() { let key = BASE64_STANDARD.encode([42u8; 16]); // Only 16 bytes - let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 16]).0); + let key_md5 = md5_base64([42u8; 16]); let params = SsecParams { algorithm: "AES256".to_string(), @@ -3158,7 +3180,7 @@ mod tests { let bucket = "test-bucket"; let key = "test-key"; let sse_key = BASE64_STANDARD.encode([42u8; 32]); - let sse_key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let sse_key_md5 = md5_base64([42u8; 32]); let content_size = 1024; let request_missing_md5 = EncryptionRequest { @@ -3211,7 +3233,7 @@ mod tests { async fn test_sse_prepare_encryption_rejects_partial_ssec_headers() { let bucket = "test-bucket"; let key = "test-key"; - let sse_key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let sse_key_md5 = md5_base64([42u8; 32]); let request_missing_algorithm = PrepareEncryptionRequest { bucket, @@ -3241,7 +3263,7 @@ mod tests { async fn test_sse_prepare_encryption_rejects_ssec_headers_without_customer_key() { let bucket = "test-bucket"; let key = "test-key"; - let sse_key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let sse_key_md5 = md5_base64([42u8; 32]); let request = PrepareEncryptionRequest { bucket, @@ -3299,7 +3321,7 @@ mod tests { let key = "object"; let customer_key_bytes = [0x24u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let customer_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let customer_key_md5 = md5_base64(customer_key_bytes); let metadata_one = ssec_direct_put_metadata(bucket, key, &customer_key, &customer_key_md5).await; let metadata_two = ssec_direct_put_metadata(bucket, key, &customer_key, &customer_key_md5).await; @@ -3340,7 +3362,7 @@ mod tests { let key = "object"; let customer_key_bytes = [0x24u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let customer_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let customer_key_md5 = md5_base64(customer_key_bytes); let mut metadata = HashMap::new(); metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()); @@ -3372,7 +3394,7 @@ mod tests { let key = "object"; let customer_key_bytes = [0x51u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let customer_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let customer_key_md5 = md5_base64(customer_key_bytes); let plaintext = b"attack at dawn - sse-c round trip".to_vec(); let metadata = ssec_direct_put_metadata(bucket, key, &customer_key, &customer_key_md5).await; @@ -3411,7 +3433,7 @@ mod tests { let key = "object"; let customer_key_bytes = [0x33u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let customer_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let customer_key_md5 = md5_base64(customer_key_bytes); let material = sse_prepare_encryption(PrepareEncryptionRequest { bucket, @@ -3473,7 +3495,7 @@ mod tests { let key = "test-key"; let customer_key_bytes = [0x24u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let sse_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let sse_key_md5 = md5_base64(customer_key_bytes); let request = PrepareEncryptionRequest { bucket, @@ -3568,7 +3590,7 @@ mod tests { let key = "test-key"; let content_size = 1024; let sse_key = BASE64_STANDARD.encode([42u8; 32]); - let sse_key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let sse_key_md5 = md5_base64([42u8; 32]); let request = EncryptionRequest { bucket, @@ -3634,18 +3656,11 @@ mod tests { #[cfg(feature = "rio-v2")] #[tokio::test] async fn test_sse_kms_roundtrip_persists_and_uses_minio_context() { - use rustfs_kms::config::KmsConfig; use rustfs_kms::types::{CreateKeyRequest, KeyUsage}; - use tempfile::TempDir; let _guard = lock_sse_test_state().await; reset_sse_dek_provider(); - let manager = rustfs_kms::init_global_kms_service_manager(); - let temp_dir = TempDir::new().expect("temp dir"); - manager - .reconfigure(KmsConfig::local(temp_dir.path().to_path_buf()).with_insecure_development_defaults()) - .await - .expect("kms reconfigure should succeed"); + let manager = configure_test_global_local_kms().await; manager .get_encryption_service() .await @@ -3970,7 +3985,7 @@ mod tests { async fn test_ssec_rio_v2_uses_sealed_object_key_metadata_roundtrip() { let customer_key_bytes = [0x42u8; 32]; let customer_key = BASE64_STANDARD.encode(customer_key_bytes); - let customer_key_md5 = BASE64_STANDARD.encode(md5::compute(customer_key_bytes).0); + let customer_key_md5 = md5_base64(customer_key_bytes); let material = sse_encryption(EncryptionRequest { bucket: "bucket", @@ -4073,7 +4088,7 @@ mod tests { ssekms_context: None, sse_customer_algorithm: Some("AES256".to_string()), sse_customer_key: Some(BASE64_STANDARD.encode(key_bytes)), - sse_customer_key_md5: Some(BASE64_STANDARD.encode(md5::compute(key_bytes).0)), + sse_customer_key_md5: Some(md5_base64(key_bytes)), content_size: 1, } } @@ -4082,10 +4097,7 @@ mod tests { let key_bytes = [key_byte; 32]; HashMap::from([ ("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()), - ( - "x-amz-server-side-encryption-customer-key-md5".to_string(), - BASE64_STANDARD.encode(md5::compute(key_bytes).0), - ), + ("x-amz-server-side-encryption-customer-key-md5".to_string(), md5_base64(key_bytes)), ]) } @@ -4510,17 +4522,9 @@ mod tests { #[tokio::test] async fn test_managed_decryption_selects_provider_from_persisted_dek() { - use rustfs_kms::config::KmsConfig; - use tempfile::TempDir; - let _guard = lock_sse_test_state().await; reset_sse_dek_provider(); - let manager = rustfs_kms::init_global_kms_service_manager(); - let key_dir = TempDir::new().expect("create KMS key directory"); - manager - .reconfigure(KmsConfig::local(key_dir.path().to_path_buf()).with_insecure_development_defaults()) - .await - .expect("start test KMS service"); + let manager = configure_test_global_local_kms().await; let local_master_key = [7u8; 32]; let local_provider = TestSseDekProvider::new_with_key(local_master_key); @@ -4588,9 +4592,6 @@ mod tests { /// the local-provider cache. #[tokio::test] async fn test_kms_envelope_never_routes_to_cached_local_provider() { - use rustfs_kms::config::KmsConfig; - use tempfile::TempDir; - let _guard = lock_sse_test_state().await; reset_sse_dek_provider(); @@ -4602,12 +4603,7 @@ mod tests { .expect("write local provider into local cache") = Some(Arc::new(TestSseDekProvider::new_with_key(local_master_key))); // 2. Start a KMS service (dynamic enable). - let manager = rustfs_kms::init_global_kms_service_manager(); - let key_dir = TempDir::new().expect("create KMS key directory"); - manager - .reconfigure(KmsConfig::local(key_dir.path().to_path_buf()).with_insecure_development_defaults()) - .await - .expect("start test KMS service"); + let manager = configure_test_global_local_kms().await; // 3. Construct a KMS JSON envelope — the persisted format of a KMS-wrapped DEK. // is_data_key_envelope() will return true for this payload. @@ -4698,7 +4694,7 @@ mod tests { use rustfs_kms::config::KmsConfig; let _guard = lock_sse_test_state().await; - let manager = rustfs_kms::init_global_kms_service_manager(); + let manager = Arc::new(rustfs_kms::KmsServiceManager::new()); manager .reconfigure(KmsConfig::static_kms("first-key".to_string(), BASE64_STANDARD.encode([0x11; 32]))) @@ -4795,7 +4791,7 @@ mod tests { fn test_validate_ssec_for_read_wrong_key() { // Key A is used to "encrypt" the object (stored MD5 is from key A). let key_a = [42u8; 32]; - let stored_md5 = BASE64_STANDARD.encode(md5::compute(key_a).0); + let stored_md5 = md5_base64(key_a); let mut metadata = HashMap::new(); metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()); @@ -4804,7 +4800,7 @@ mod tests { // Key B is a different key; its MD5 won't match stored MD5. let key_b = [99u8; 32]; let key_b_b64 = BASE64_STANDARD.encode(key_b); - let key_b_md5 = BASE64_STANDARD.encode(md5::compute(key_b).0); + let key_b_md5 = md5_base64(key_b); let err = validate_ssec_for_read(&metadata, Some(&key_b_b64), Some(&key_b_md5)).unwrap_err(); assert_eq!(err.code, S3ErrorCode::InvalidRequest); @@ -4814,7 +4810,7 @@ mod tests { fn test_validate_ssec_for_read_correct_key() { let key_bytes = [42u8; 32]; let key_b64 = BASE64_STANDARD.encode(key_bytes); - let key_md5 = BASE64_STANDARD.encode(md5::compute(key_bytes).0); + let key_md5 = md5_base64(key_bytes); let mut metadata = HashMap::new(); metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()); @@ -4830,7 +4826,7 @@ mod tests { // DIFFERENT key. The server must recompute MD5 from the key bytes and // reject the request because the recomputed MD5 won't match the header. let real_key = [42u8; 32]; - let stored_md5 = BASE64_STANDARD.encode(md5::compute(real_key).0); + let stored_md5 = md5_base64(real_key); let mut metadata = HashMap::new(); metadata.insert("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()); @@ -4951,7 +4947,7 @@ mod tests { #[test] fn test_validate_ssec_params_returns_invalid_request_on_bad_algorithm() { let key = BASE64_STANDARD.encode([42u8; 32]); - let key_md5 = BASE64_STANDARD.encode(md5::compute([42u8; 32]).0); + let key_md5 = md5_base64([42u8; 32]); let params = SsecParams { algorithm: "AES128".to_string(), key, @@ -5037,7 +5033,7 @@ mod tests { ssekms_context: None, sse_customer_algorithm: Some("unsupported-algo".to_string()), sse_customer_key: Some(sse_key), - sse_customer_key_md5: Some(BASE64_STANDARD.encode(md5::compute([42u8; 32]).0)), + sse_customer_key_md5: Some(md5_base64([42u8; 32])), content_size: 1024, }; let err = sse_encryption(request_unsupported_algorithm).await.unwrap_err();