mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
feat(ecstore): closed-form range seek for single-part v2 encrypted objects (#6601)
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.
This commit is contained in:
@@ -394,6 +394,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt a stream that starts at an arbitrary frame boundary of a
|
||||
/// single-part v2 object.
|
||||
///
|
||||
/// `starting_block_index` is the absolute index of the first frame in the
|
||||
/// stream: both the per-block nonce derivation and the v2 AEAD associated
|
||||
/// data bind absolute indices, so a frame served from the middle of an
|
||||
/// object only authenticates when the caller positions the read at a true
|
||||
/// frame boundary and names that frame's index. v1 streams are never
|
||||
/// planned with a non-zero start (their frames have no closed-form
|
||||
/// positions), so this constructor is v2-only by construction.
|
||||
pub fn new_at_block(inner: R, key: [u8; 32], nonce: [u8; 12], starting_block_index: usize) -> Self {
|
||||
let mut reader = Self::new(inner, key, nonce);
|
||||
reader.block_index = starting_block_index;
|
||||
reader
|
||||
}
|
||||
|
||||
pub fn new_multipart(inner: R, key: [u8; 32], base_nonce: [u8; 12], multipart_parts: Vec<usize>) -> Self {
|
||||
let first_part = multipart_parts.first().copied().unwrap_or(1);
|
||||
let initial_nonce = derive_part_nonce(&base_nonce, first_part);
|
||||
@@ -1548,4 +1564,35 @@ mod tests {
|
||||
assert_eq!(decrypted.len(), expected.len(), "no segment may be dropped after an empty final frame");
|
||||
assert_eq!(decrypted, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn v2_decrypts_from_an_arbitrary_frame_boundary() {
|
||||
let mut key = [0u8; 32];
|
||||
let mut nonce = [0u8; 12];
|
||||
rand::rng().fill_bytes(&mut key);
|
||||
rand::rng().fill_bytes(&mut nonce);
|
||||
|
||||
let block = super::ENCRYPTION_BLOCK_SIZE;
|
||||
let data: Vec<u8> = (0..4 * block + 321).map(|i| (i % 247) as u8).collect();
|
||||
let encrypted = v2_encrypt(&data, key, nonce).await;
|
||||
|
||||
// Serve the ciphertext window starting at frame 2 and decrypt with the
|
||||
// matching absolute frame index: nonce and AAD both line up.
|
||||
let window = encrypted[2 * V2_FULL_FRAME_LEN..].to_vec();
|
||||
let mut decrypt_reader = DecryptReader::new_at_block(Cursor::new(window.clone()), key, nonce, 2);
|
||||
let mut decrypted = Vec::new();
|
||||
decrypt_reader
|
||||
.read_to_end(&mut decrypted)
|
||||
.await
|
||||
.expect("mid-stream decrypt at a true frame boundary succeeds");
|
||||
assert_eq!(decrypted, &data[2 * block..]);
|
||||
|
||||
// The same window with a wrong starting index fails authentication.
|
||||
let mut decrypt_reader = DecryptReader::new_at_block(Cursor::new(window), key, nonce, 1);
|
||||
let mut decrypted = Vec::new();
|
||||
decrypt_reader
|
||||
.read_to_end(&mut decrypted)
|
||||
.await
|
||||
.expect_err("a wrong absolute frame index must fail authentication");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user