use crate::aead::{NONCE_LEN, TAG_LEN, seal_chunk_hedged};
use crate::merkle::{Leaf, MerkleTree};
use crate::secret::{PayloadKey, SecretBuf};
use crate::{AeadAlg, CryptoError};
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sealed {
pub total_len: u64,
pub chunk_count: u32,
pub tree_root: [u8; 32],
}
#[derive(Debug)]
pub enum StreamError<E> {
Crypto(CryptoError),
Host(E),
TooLarge,
}
impl<E> From<CryptoError> for StreamError<E> {
fn from(err: CryptoError) -> Self {
Self::Crypto(err)
}
}
impl<E: core::fmt::Display> core::fmt::Display for StreamError<E> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Crypto(err) => write!(f, "{err}"),
Self::Host(err) => write!(f, "{err}"),
Self::TooLarge => f.write_str("файл больше, чем адресуемо форматом"),
}
}
}
impl<E: core::fmt::Debug + core::fmt::Display> core::error::Error for StreamError<E> {}
pub fn seal_chunks<G, E>(
key: &PayloadKey,
alg: AeadAlg,
file_id: &[u8; 16],
chunk_size: u32,
rng: &mut G,
mut source: impl FnMut(&mut [u8]) -> Result<usize, E>,
mut sink: impl FnMut(&[u8]) -> Result<(), E>,
) -> Result<Sealed, StreamError<E>>
where
G: rand_core::CryptoRng + ?Sized,
{
let capacity = usize::try_from(chunk_size).map_err(|_| StreamError::TooLarge)?;
if capacity == 0 {
return Err(CryptoError::BadLength.into());
}
let mut plaintext = SecretBuf::with_capacity(capacity);
let mut framed = Vec::with_capacity(capacity.saturating_add(TAG_LEN));
let mut leaves: Vec<Leaf> = Vec::new();
let mut total_len = 0u64;
let mut index = 0u32;
loop {
let filled = fill(&mut source, plaintext.as_capacity_mut())?;
plaintext.declare_len(filled)?;
if filled == 0 && index > 0 {
break;
}
let piece = plaintext.as_slice();
let mut nonce_seed = Zeroizing::new([0u8; NONCE_LEN]);
rand_core::Rng::fill_bytes(rng, nonce_seed.as_mut_slice());
let (nonce, leaf) =
seal_chunk_hedged(key, alg, file_id, index, &nonce_seed, piece, &mut framed)?;
sink(&nonce).map_err(StreamError::Host)?;
sink(&framed).map_err(StreamError::Host)?;
leaves.push(leaf);
total_len =
total_len.checked_add(filled as u64).ok_or(StreamError::TooLarge)?;
index = index.checked_add(1).ok_or(StreamError::TooLarge)?;
if filled < capacity {
break;
}
}
let tree = MerkleTree::build(&leaves)?;
Ok(Sealed { total_len, chunk_count: index, tree_root: tree.root() })
}
fn fill<E>(
source: &mut impl FnMut(&mut [u8]) -> Result<usize, E>,
buf: &mut [u8],
) -> Result<usize, StreamError<E>> {
let mut filled = 0usize;
while filled < buf.len() {
let Some(rest) = buf.get_mut(filled..) else {
break;
};
let got = source(rest).map_err(StreamError::Host)?;
if got == 0 {
break;
}
filled = filled.saturating_add(got);
}
Ok(filled)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
mod tests {
use super::*;
struct Fixed(u8);
impl rand_core::TryRng for Fixed {
type Error = core::convert::Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
Ok(u32::from(self.0))
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
Ok(u64::from(self.0))
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
dst.fill(self.0);
Ok(())
}
}
impl rand_core::TryCryptoRng for Fixed {}
fn key() -> PayloadKey {
PayloadKey::from_bytes([7u8; 32])
}
fn run(input: &[u8], chunk_size: u32, step: usize) -> (Sealed, Vec<u8>) {
let mut left = input;
let mut out = Vec::new();
let sealed = seal_chunks(
&key(),
AeadAlg::XChaCha20Poly1305,
&[1u8; 16],
chunk_size,
&mut Fixed(0x5a),
|buf| -> Result<usize, core::convert::Infallible> {
let take = left.len().min(buf.len()).min(step);
buf.get_mut(..take).unwrap_or_default().copy_from_slice(&left[..take]);
left = &left[take..];
Ok(take)
},
|bytes| -> Result<(), core::convert::Infallible> {
out.extend_from_slice(bytes);
Ok(())
},
)
.expect("прогон не удался");
(sealed, out)
}
#[test]
fn an_empty_input_still_gets_exactly_one_chunk() {
let (sealed, _) = run(&[], 64, 64);
assert_eq!(sealed.chunk_count, 1, "у пустого входа не один чанк");
assert_eq!(sealed.total_len, 0);
}
#[test]
fn a_short_read_changes_nothing() {
let input: Vec<u8> = (0..300u32).map(|i| u8::try_from(i % 251).unwrap_or(0)).collect();
let (whole, bytes_whole) = run(&input, 64, usize::MAX);
for step in [1usize, 7, 63, 64, 65, 128] {
let (piecemeal, bytes_piecemeal) = run(&input, 64, step);
assert_eq!(piecemeal, whole, "шаг {step}: нарезка разошлась");
assert_eq!(bytes_piecemeal, bytes_whole, "шаг {step}: байты разошлись");
}
}
#[test]
fn the_chunk_count_follows_the_size_including_the_edges() {
for (len, expected) in [(0usize, 1u32), (1, 1), (63, 1), (64, 1), (65, 2), (128, 2), (129, 3)] {
let input = vec![0xa5u8; len];
let (sealed, _) = run(&input, 64, usize::MAX);
assert_eq!(sealed.chunk_count, expected, "длина {len}");
assert_eq!(sealed.total_len, len as u64, "длина {len}");
}
}
#[test]
fn a_host_failure_arrives_as_itself() {
let err = seal_chunks(
&key(),
AeadAlg::XChaCha20Poly1305,
&[1u8; 16],
64,
&mut Fixed(1),
|_buf| Err("источник отвалился"),
|_bytes| Ok(()),
)
.expect_err("отказ источника не заметили");
match err {
StreamError::Host(said) => assert_eq!(said, "источник отвалился"),
other => panic!("отказ хоста подменён нашим: {other:?}"),
}
}
}