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 if capacity == 0 {
82 return Err(CryptoError::BadLength.into());
83 }
84 let mut plaintext = SecretBuf::with_capacity(capacity);
88 let mut framed = Vec::with_capacity(capacity.saturating_add(TAG_LEN));
89 let mut leaves: Vec<Leaf> = Vec::new();
90 let mut total_len = 0u64;
91 let mut index = 0u32;
92
93 loop {
94 let filled = fill(&mut source, plaintext.as_capacity_mut())?;
95 plaintext.declare_len(filled)?;
96 if filled == 0 && index > 0 {
99 break;
100 }
101
102 let piece = plaintext.as_slice();
103 let mut nonce_seed = Zeroizing::new([0u8; NONCE_LEN]);
110 rand_core::Rng::fill_bytes(rng, nonce_seed.as_mut_slice());
111 let (nonce, leaf) =
112 seal_chunk_hedged(key, alg, file_id, index, &nonce_seed, piece, &mut framed)?;
113
114 sink(&nonce).map_err(StreamError::Host)?;
115 sink(&framed).map_err(StreamError::Host)?;
116
117 leaves.push(leaf);
118 total_len =
119 total_len.checked_add(filled as u64).ok_or(StreamError::TooLarge)?;
120 index = index.checked_add(1).ok_or(StreamError::TooLarge)?;
121
122 if filled < capacity {
123 break;
124 }
125 }
126
127 let tree = MerkleTree::build(&leaves)?;
128 Ok(Sealed { total_len, chunk_count: index, tree_root: tree.root() })
129}
130
131fn fill<E>(
138 source: &mut impl FnMut(&mut [u8]) -> Result<usize, E>,
139 buf: &mut [u8],
140) -> Result<usize, StreamError<E>> {
141 let mut filled = 0usize;
142 while filled < buf.len() {
143 let Some(rest) = buf.get_mut(filled..) else {
144 break;
145 };
146 let got = source(rest).map_err(StreamError::Host)?;
147 if got == 0 {
148 break;
149 }
150 filled = filled.saturating_add(got);
151 }
152 Ok(filled)
153}
154
155#[cfg(test)]
156#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing)]
157mod tests {
158 use super::*;
159
160 struct Fixed(u8);
164
165 impl rand_core::TryRng for Fixed {
166 type Error = core::convert::Infallible;
167 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
168 Ok(u32::from(self.0))
169 }
170 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
171 Ok(u64::from(self.0))
172 }
173 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
174 dst.fill(self.0);
175 Ok(())
176 }
177 }
178 impl rand_core::TryCryptoRng for Fixed {}
179
180 fn key() -> PayloadKey {
181 PayloadKey::from_bytes([7u8; 32])
182 }
183
184 fn run(input: &[u8], chunk_size: u32, step: usize) -> (Sealed, Vec<u8>) {
189 let mut left = input;
190 let mut out = Vec::new();
191 let sealed = seal_chunks(
192 &key(),
193 AeadAlg::XChaCha20Poly1305,
194 &[1u8; 16],
195 chunk_size,
196 &mut Fixed(0x5a),
197 |buf| -> Result<usize, core::convert::Infallible> {
198 let take = left.len().min(buf.len()).min(step);
199 buf.get_mut(..take).unwrap_or_default().copy_from_slice(&left[..take]);
200 left = &left[take..];
201 Ok(take)
202 },
203 |bytes| -> Result<(), core::convert::Infallible> {
204 out.extend_from_slice(bytes);
205 Ok(())
206 },
207 )
208 .expect("прогон не удался");
209 (sealed, out)
210 }
211
212 #[test]
218 fn an_empty_input_still_gets_exactly_one_chunk() {
219 let (sealed, _) = run(&[], 64, 64);
220 assert_eq!(sealed.chunk_count, 1, "у пустого входа не один чанк");
221 assert_eq!(sealed.total_len, 0);
222 }
223
224 #[test]
232 fn a_short_read_changes_nothing() {
233 let input: Vec<u8> = (0..300u32).map(|i| u8::try_from(i % 251).unwrap_or(0)).collect();
234 let (whole, bytes_whole) = run(&input, 64, usize::MAX);
235 for step in [1usize, 7, 63, 64, 65, 128] {
236 let (piecemeal, bytes_piecemeal) = run(&input, 64, step);
237 assert_eq!(piecemeal, whole, "шаг {step}: нарезка разошлась");
238 assert_eq!(bytes_piecemeal, bytes_whole, "шаг {step}: байты разошлись");
239 }
240 }
241
242 #[test]
247 fn the_chunk_count_follows_the_size_including_the_edges() {
248 for (len, expected) in [(0usize, 1u32), (1, 1), (63, 1), (64, 1), (65, 2), (128, 2), (129, 3)] {
249 let input = vec![0xa5u8; len];
250 let (sealed, _) = run(&input, 64, usize::MAX);
251 assert_eq!(sealed.chunk_count, expected, "длина {len}");
252 assert_eq!(sealed.total_len, len as u64, "длина {len}");
253 }
254 }
255
256 #[test]
261 fn a_host_failure_arrives_as_itself() {
262 let err = seal_chunks(
263 &key(),
264 AeadAlg::XChaCha20Poly1305,
265 &[1u8; 16],
266 64,
267 &mut Fixed(1),
268 |_buf| Err("источник отвалился"),
269 |_bytes| Ok(()),
270 )
271 .expect_err("отказ источника не заметили");
272 match err {
273 StreamError::Host(said) => assert_eq!(said, "источник отвалился"),
274 other => panic!("отказ хоста подменён нашим: {other:?}"),
275 }
276 }
277}