mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 16:37:07 +00:00
9118a6e344
Single-part encrypted objects in the legacy format could not serve range reads without decrypting from byte 0: v1 frames are emitted per upstream read, so no closed-form plaintext-to-ciphertext mapping exists. The v2 layout fixed the frame length (8218 ciphertext bytes per 8 KiB plaintext frame), making the mapping closed-form. Consume it: - Single-part PUTs that encrypt locally under the v2 write switch stamp the frame-layout marker, valued with the object's data_dir token - ciphertext passthrough, data movement and copies mint a new data_dir or strip the marker, so a re-homed marker disqualifies itself. - The encrypted read plan seeks marked, uncompressed single-part objects to frame_index * 8218 and decrypts from that frame: the frame index rides the plan's sequence-number slot into DecryptReader::new_at_block, whose nonce and AAD bind absolute indices. New metric path label frame_seek. - A lying marker fails closed: v2 authentication rejects bytes at a fake frame boundary; plaintext is never served from the wrong offset. Compressed objects and multipart sub-part seeks keep the conservative paths (follow-up work); reading needs no switch - seekability follows the marker.
141 lines
5.2 KiB
Rust
141 lines
5.2 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Ranged GETs over encrypted single-part objects.
|
|
//!
|
|
//! Byte-exactness must hold on every frame layout the server can write:
|
|
//! legacy v1 (variable frames, conservative full read) and, when
|
|
//! `RUSTFS_ENCRYPTION_FRAME_V2=true` reaches the server under test, the
|
|
//! fixed-frame v2 layout whose marker enables the closed-form frame seek.
|
|
//! The matrix crosses frame boundaries, starts mid-frame, and ends inside
|
|
//! the final short frame, so a mispositioned seek cannot pass.
|
|
|
|
use super::common::LocalKMSTestEnvironment;
|
|
use crate::common::{TEST_BUCKET, init_logging};
|
|
use aws_sdk_s3::primitives::ByteStream;
|
|
use aws_sdk_s3::types::ServerSideEncryption;
|
|
use tracing::info;
|
|
|
|
const FRAME_PLAINTEXT: usize = 8 * 1024;
|
|
|
|
#[tokio::test]
|
|
async fn sse_s3_single_part_ranged_gets_are_byte_exact() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
init_logging();
|
|
info!("Testing ranged GETs over an SSE-S3 single-part object");
|
|
|
|
let mut kms_env = LocalKMSTestEnvironment::new().await?;
|
|
let _default_key_id = kms_env.start_rustfs_for_local_kms().await?;
|
|
kms_env.wait_for_kms_ready().await?;
|
|
|
|
let s3_client = kms_env.base_env.create_s3_client();
|
|
kms_env.base_env.create_test_bucket(TEST_BUCKET).await?;
|
|
|
|
let test_key = "encrypted-range-get";
|
|
let body: Vec<u8> = (0..3 * FRAME_PLAINTEXT + 500).map(|i| (i % 251) as u8).collect();
|
|
|
|
let put = s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(test_key)
|
|
.server_side_encryption(ServerSideEncryption::Aes256)
|
|
.body(ByteStream::from(body.clone()))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
put.server_side_encryption(),
|
|
Some(&ServerSideEncryption::Aes256),
|
|
"the object under test must actually be encrypted"
|
|
);
|
|
|
|
let cases: &[(usize, usize)] = &[
|
|
// Head range inside frame 0.
|
|
(0, 99),
|
|
// Crossing the first frame boundary.
|
|
(FRAME_PLAINTEXT - 1, FRAME_PLAINTEXT),
|
|
// Starting exactly on a frame boundary.
|
|
(FRAME_PLAINTEXT, FRAME_PLAINTEXT + 9),
|
|
// Mid-object, mid-frame on both ends.
|
|
(2 * FRAME_PLAINTEXT + 5, 3 * FRAME_PLAINTEXT + 100),
|
|
// Tail range ending inside the final short frame.
|
|
(3 * FRAME_PLAINTEXT + 100, 3 * FRAME_PLAINTEXT + 499),
|
|
];
|
|
|
|
for &(start, end) in cases {
|
|
let response = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(test_key)
|
|
.range(format!("bytes={start}-{end}"))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
response.content_length(),
|
|
Some((end - start + 1) as i64),
|
|
"range {start}-{end} content length"
|
|
);
|
|
let data = response.body.collect().await?.into_bytes();
|
|
assert_eq!(data.as_ref(), &body[start..=end], "range {start}-{end} must be byte-exact");
|
|
}
|
|
|
|
// A suffix range exercises the offset resolution path as well.
|
|
let response = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(test_key)
|
|
.range("bytes=-123")
|
|
.send()
|
|
.await?;
|
|
let data = response.body.collect().await?.into_bytes();
|
|
assert_eq!(data.as_ref(), &body[body.len() - 123..], "suffix range must be byte-exact");
|
|
|
|
// The unranged body still round-trips.
|
|
let response = s3_client.get_object().bucket(TEST_BUCKET).key(test_key).send().await?;
|
|
let data = response.body.collect().await?.into_bytes();
|
|
assert_eq!(data.as_ref(), body.as_slice(), "full body must round-trip");
|
|
|
|
// A block-aligned object ends in an empty authenticated final frame under
|
|
// the v2 layout; tail ranges touching the last plaintext byte must not be
|
|
// misread as truncation.
|
|
let aligned_key = "encrypted-range-get-aligned";
|
|
let aligned_body: Vec<u8> = (0..3 * FRAME_PLAINTEXT).map(|i| ((i + 3) % 251) as u8).collect();
|
|
s3_client
|
|
.put_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(aligned_key)
|
|
.server_side_encryption(ServerSideEncryption::Aes256)
|
|
.body(ByteStream::from(aligned_body.clone()))
|
|
.send()
|
|
.await?;
|
|
for (start, end) in [
|
|
(2 * FRAME_PLAINTEXT + 10, 3 * FRAME_PLAINTEXT - 1),
|
|
(3 * FRAME_PLAINTEXT - 1, 3 * FRAME_PLAINTEXT - 1),
|
|
] {
|
|
let response = s3_client
|
|
.get_object()
|
|
.bucket(TEST_BUCKET)
|
|
.key(aligned_key)
|
|
.range(format!("bytes={start}-{end}"))
|
|
.send()
|
|
.await?;
|
|
let data = response.body.collect().await?.into_bytes();
|
|
assert_eq!(
|
|
data.as_ref(),
|
|
&aligned_body[start..=end],
|
|
"aligned range {start}-{end} must be byte-exact"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|