1use crate::aead::{NONCE_LEN, TAG_LEN, seal_chunk_hedged};
10use crate::merkle::{Leaf, MerkleTree};
11use crate::secret::{PayloadKey, SecretBuf};
12use crate::{AeadAlg, CryptoError};
13use zeroize::Zeroizing;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Sealed {
18 pub total_len: u64,
19 pub chunk_count: u32,
20 pub tree_root: [u8; 32],
21}
22
23#[derive(Debug)]
29pub enum StreamError<E> {
30 Crypto(CryptoError),
32 Host(E),
34 TooLarge,
36}
37
38impl<E> From<CryptoError> for StreamError<E> {
39 fn from(err: CryptoError) -> Self {
40 Self::Crypto(err)
41 }
42}
43
44impl<E: core::fmt::Display> core::fmt::Display for StreamError<E> {
45 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46 match self {
47 Self::Crypto(err) => write!(f, "{err}"),
48 Self::Host(err) => write!(f, "{err}"),
49 Self::TooLarge => f.write_str("файл больше, чем адресуемо форматом"),
50 }
51 }
52}
53
54impl<E: core::fmt::Debug + core::fmt::Display> core::error::Error for StreamError<E> {}
55
56pub fn seal_chunks<G, E>(
68 key: &PayloadKey,
69 alg: AeadAlg,
70 file_id: &[u8; 16],
71 chunk_size: u32,
72 rng: &mut G,
73 mut source: impl FnMut(&mut [u8]) -> Result<usize, E>,
74 mut sink: impl FnMut(&[u8]) -> Result<(), E>,
75) -> Result<Sealed, StreamError<E>>
76where
77 G: rand_core::CryptoRng + ?Sized,
78{
79 let capacity = usize::try_from(chunk_size).map_err(|_| StreamError::TooLarge)?;
80 let mut plaintext = SecretBuf::with_capacity(capacity);
84 let mut framed = Vec::with_capacity(capacity.saturating_add(TAG_LEN));
85 let mut leaves: Vec<Leaf> = Vec::new();
86 let mut total_len = 0u64;
87 let mut index = 0u32;
88
89 loop {
90 let filled = fill(&mut source, plaintext.as_capacity_mut())?;
91 plaintext.declare_len(filled)?;
92 if filled == 0 && index > 0 {
95 break;
96 }
97
98 let piece = plaintext.as_slice();
99 let mut nonce_seed = Zeroizing::new([0u8; NONCE_LEN]);
106 rand_core::Rng::fill_bytes(rng, nonce_seed.as_mut_slice());
107 let (nonce, leaf) =
108 seal_chunk_hedged(key, alg, file_id, index, &nonce_seed, piece, &mut framed)?;
109
110 sink(&nonce).map_err(StreamError::Host)?;
111 sink(&framed).map_err(StreamError::Host)?;
112
113 leaves.push(leaf);
114 total_len =
115 total_len.checked_add(filled as u64).ok_or(StreamError::TooLarge)?;
116 index = index.checked_add(1).ok_or(StreamError::TooLarge)?;
117
118 if filled < capacity {
119 break;
120 }
121 }
122
123 let tree = MerkleTree::build(&leaves)?;
124 Ok(Sealed { total_len, chunk_count: index, tree_root: tree.root() })
125}
126
127fn fill<E>(
134 source: &mut impl FnMut(&mut [u8]) -> Result<usize, E>,
135 buf: &mut [u8],
136) -> Result<usize, StreamError<E>> {
137 let mut filled = 0usize;
138 while filled < buf.len() {
139 let Some(rest) = buf.get_mut(filled..) else {
140 break;
141 };
142 let got = source(rest).map_err(StreamError::Host)?;
143 if got == 0 {
144 break;
145 }
146 filled = filled.saturating_add(got);
147 }
148 Ok(filled)
149}
150
151#[cfg(test)]
152#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
153mod tests {
154 use super::*;
155
156 struct Fixed(u8);
160
161 impl rand_core::TryRng for Fixed {
162 type Error = core::convert::Infallible;
163 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
164 Ok(u32::from(self.0))
165 }
166 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
167 Ok(u64::from(self.0))
168 }
169 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
170 dst.fill(self.0);
171 Ok(())
172 }
173 }
174 impl rand_core::TryCryptoRng for Fixed {}
175
176 fn key() -> PayloadKey {
177 PayloadKey::from_bytes([7u8; 32])
178 }
179
180 fn run(input: &[u8], chunk_size: u32, step: usize) -> (Sealed, Vec<u8>) {
185 let mut left = input;
186 let mut out = Vec::new();
187 let sealed = seal_chunks(
188 &key(),
189 AeadAlg::XChaCha20Poly1305,
190 &[1u8; 16],
191 chunk_size,
192 &mut Fixed(0x5a),
193 |buf| -> Result<usize, core::convert::Infallible> {
194 let take = left.len().min(buf.len()).min(step);
195 buf.get_mut(..take).unwrap_or_default().copy_from_slice(&left[..take]);
196 left = &left[take..];
197 Ok(take)
198 },
199 |bytes| -> Result<(), core::convert::Infallible> {
200 out.extend_from_slice(bytes);
201 Ok(())
202 },
203 )
204 .expect("прогон не удался");
205 (sealed, out)
206 }
207
208 #[test]
214 fn an_empty_input_still_gets_exactly_one_chunk() {
215 let (sealed, _) = run(&[], 64, 64);
216 assert_eq!(sealed.chunk_count, 1, "у пустого входа не один чанк");
217 assert_eq!(sealed.total_len, 0);
218 }
219
220 #[test]
228 fn a_short_read_changes_nothing() {
229 let input: Vec<u8> = (0..300u32).map(|i| u8::try_from(i % 251).unwrap_or(0)).collect();
230 let (whole, bytes_whole) = run(&input, 64, usize::MAX);
231 for step in [1usize, 7, 63, 64, 65, 128] {
232 let (piecemeal, bytes_piecemeal) = run(&input, 64, step);
233 assert_eq!(piecemeal, whole, "шаг {step}: нарезка разошлась");
234 assert_eq!(bytes_piecemeal, bytes_whole, "шаг {step}: байты разошлись");
235 }
236 }
237
238 #[test]
243 fn the_chunk_count_follows_the_size_including_the_edges() {
244 for (len, expected) in [(0usize, 1u32), (1, 1), (63, 1), (64, 1), (65, 2), (128, 2), (129, 3)] {
245 let input = vec![0xa5u8; len];
246 let (sealed, _) = run(&input, 64, usize::MAX);
247 assert_eq!(sealed.chunk_count, expected, "длина {len}");
248 assert_eq!(sealed.total_len, len as u64, "длина {len}");
249 }
250 }
251
252 #[test]
257 fn a_host_failure_arrives_as_itself() {
258 let err = seal_chunks(
259 &key(),
260 AeadAlg::XChaCha20Poly1305,
261 &[1u8; 16],
262 64,
263 &mut Fixed(1),
264 |_buf| Err("источник отвалился"),
265 |_bytes| Ok(()),
266 )
267 .expect_err("отказ источника не заметили");
268 match err {
269 StreamError::Host(said) => assert_eq!(said, "источник отвалился"),
270 other => panic!("отказ хоста подменён нашим: {other:?}"),
271 }
272 }
273}