1use hmac::Mac;
7use sha2::digest::typenum::Unsigned;
8use sha2::digest::{FixedOutput, MacError, Output};
9
10#[derive(Clone)]
11pub struct Incremental<M: Mac + Clone> {
12 mac: M,
13 chunk_size: usize,
14 unused_length: usize,
15}
16
17#[derive(Clone)]
18pub struct Validating<M: Mac + Clone> {
19 incremental: Incremental<M>,
20 expected: Vec<Output<M>>,
22}
23
24const MINIMUM_CHUNK_SIZE: usize = 64 * 1024;
25const MAXIMUM_CHUNK_SIZE: usize = 2 * 1024 * 1024;
26const TARGET_TOTAL_DIGEST_SIZE: usize = 8 * 1024;
27
28pub const fn calculate_chunk_size<D>(data_size: usize) -> usize
29where
30 D: FixedOutput,
31{
32 assert!(
33 0 == TARGET_TOTAL_DIGEST_SIZE % D::OutputSize::USIZE,
34 "Target digest size should be a multiple of digest size"
35 );
36 let target_chunk_count = TARGET_TOTAL_DIGEST_SIZE / D::OutputSize::USIZE;
37 if data_size < target_chunk_count * MINIMUM_CHUNK_SIZE {
38 return MINIMUM_CHUNK_SIZE;
39 }
40 if data_size < target_chunk_count * MAXIMUM_CHUNK_SIZE {
41 return data_size.div_ceil(target_chunk_count);
42 }
43 MAXIMUM_CHUNK_SIZE
44}
45
46impl<M: Mac + Clone> Incremental<M> {
47 pub fn new(mac: M, chunk_size: usize) -> Self {
48 assert!(chunk_size > 0, "chunk size must be positive");
49 Self {
50 mac,
51 chunk_size,
52 unused_length: chunk_size,
53 }
54 }
55
56 pub fn validating<'a, A, I>(self, macs: I) -> Validating<M>
57 where
58 A: Into<&'a sha2::digest::array::Array<u8, M::OutputSize>>,
61 I: IntoIterator<Item = A, IntoIter: DoubleEndedIterator>,
62 {
63 let expected = macs
64 .into_iter()
65 .map(|mac| mac.into().to_owned())
66 .rev()
67 .collect();
68 Validating {
69 incremental: self,
70 expected,
71 }
72 }
73
74 pub fn update<'a>(&'a mut self, bytes: &'a [u8]) -> impl Iterator<Item = Output<M>> + 'a {
75 let split_point = std::cmp::min(bytes.len(), self.unused_length);
76 let (to_write, overflow) = bytes.split_at(split_point);
77
78 std::iter::once(to_write)
79 .chain(overflow.chunks(self.chunk_size))
80 .flat_map(move |chunk| self.update_chunk(chunk))
81 }
82
83 pub fn finalize(self) -> Output<M> {
84 self.mac.finalize().into_bytes()
85 }
86
87 fn update_chunk(&mut self, bytes: &[u8]) -> Option<Output<M>> {
88 assert!(bytes.len() <= self.unused_length);
89 self.mac.update(bytes);
90 self.unused_length -= bytes.len();
91 if self.unused_length == 0 {
92 self.unused_length = self.chunk_size;
93 let mac = self.mac.clone();
94 Some(mac.finalize().into_bytes())
95 } else {
96 None
97 }
98 }
99
100 fn pending_bytes_size(&self) -> usize {
101 self.chunk_size - self.unused_length
102 }
103}
104
105impl<M: Mac + Clone> Validating<M> {
106 pub fn update(&mut self, bytes: &[u8]) -> Result<usize, MacError> {
107 let mut result = Ok(0);
108 let macs = self.incremental.update(bytes);
109
110 let mut whole_chunks = 0;
111 for mac in macs {
112 match self.expected.last() {
113 Some(expected) if expected == &mac => {
114 whole_chunks += 1;
115 self.expected.pop();
116 }
117 _ => {
118 result = Err(MacError);
119 }
120 }
121 }
122 let validated_bytes = whole_chunks * self.incremental.chunk_size;
123 result.map(|_| validated_bytes)
124 }
125
126 pub fn finalize(self) -> Result<usize, MacError> {
127 let pending_bytes_size = self.incremental.pending_bytes_size();
128 let mac = self.incremental.finalize();
129 match &self.expected[..] {
130 [expected] if expected == &mac => Ok(pending_bytes_size),
131 _ => Err(MacError),
132 }
133 }
134}
135
136#[cfg(test)]
137mod test {
138 use const_str::hex;
139 use hmac::{Hmac, KeyInit as _};
140 use proptest::prelude::*;
141 use rand::distr::uniform::{UniformSampler as _, UniformUsize};
142 use rand::prelude::{Rng, ThreadRng};
143 use sha2::Sha256;
144 use sha2::digest::OutputSizeUser;
145
146 use super::*;
147 use crate::crypto::hmac_sha256;
148
149 const TEST_HMAC_KEY: &[u8] =
150 &hex!("a83481457efecc69ad1342e21d9c0297f71debbf5c9304b4c1b2e433c1a78f98");
151
152 const TEST_CHUNK_SIZE: usize = 32;
153
154 fn new_incremental(key: &[u8], chunk_size: usize) -> Incremental<Hmac<Sha256>> {
155 let hmac = Hmac::<Sha256>::new_from_slice(key)
156 .expect("Should be able to create a new HMAC instance");
157 Incremental::new(hmac, chunk_size)
158 }
159
160 #[test]
161 #[should_panic]
162 fn chunk_size_zero() {
163 new_incremental(&[], 0);
164 }
165
166 #[test]
167 fn simple_test() {
168 let key = TEST_HMAC_KEY;
169 let input = "this is a simple test input string which is longer than the chunk";
170
171 let bytes = input.as_bytes();
172 let expected = hmac_sha256(key, bytes);
173 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
174 let _ = incremental.update(bytes).collect::<Vec<_>>();
175 let digest = incremental.finalize();
176 let actual: [u8; 32] = digest.into();
177 assert_eq!(actual, expected);
178 }
179
180 #[test]
181 fn final_result_should_be_equal_to_non_incremental_hmac() {
182 let key = TEST_HMAC_KEY;
183 proptest!(|(input in ".{0,100}")| {
184 let bytes = input.as_bytes();
185 let expected = hmac_sha256(key, bytes);
186 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
187 let _ = incremental.update(bytes).collect::<Vec<_>>();
188 let actual: [u8; 32] = incremental.finalize().into();
189 assert_eq!(actual, expected);
190 });
191 }
192
193 #[test]
194 fn incremental_macs_are_valid() {
195 let key = TEST_HMAC_KEY;
196
197 proptest!(|(input in ".{50,100}")| {
198 let bytes = input.as_bytes();
199 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
200
201 let expected: Vec<_> = bytes
204 .chunks(incremental.chunk_size)
205 .scan(Vec::new(), |acc, chunk| {
206 acc.extend(chunk.iter());
207 Some(hmac_sha256(key, acc).to_vec())
208 })
209 .collect();
210
211 let mut actual: Vec<Vec<u8>> = bytes
212 .random_chunks(incremental.chunk_size)
213 .flat_map(|chunk| incremental.update(chunk).collect::<Vec<_>>())
214 .map(|out| out.into())
215 .map(|bs: [u8; 32]| bs.to_vec())
216 .collect();
217 if bytes.len() % incremental.chunk_size != 0 {
220 let last_hmac: [u8; 32] = incremental.finalize().into();
221 actual.push(last_hmac.to_vec());
222 }
223 assert_eq!(actual, expected);
224 });
225 }
226
227 #[test]
228 fn validating_simple_test() {
229 let key = TEST_HMAC_KEY;
230 let input = "this is a simple test input string";
231
232 let bytes = input.as_bytes();
233 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
234 let mut expected_macs: Vec<_> = incremental.update(bytes).collect();
235 expected_macs.push(incremental.finalize());
236
237 let expected_bytes: Vec<[u8; 32]> =
238 expected_macs.into_iter().map(|mac| mac.into()).collect();
239
240 {
241 let mut validating = new_incremental(key, TEST_CHUNK_SIZE).validating(&expected_bytes);
242 validating
243 .update(bytes)
244 .expect("update: validation should succeed");
245 validating
246 .finalize()
247 .expect("finalize: validation should succeed");
248 }
249
250 {
251 let mut failing_first_update = expected_bytes.clone();
252 failing_first_update
253 .first_mut()
254 .expect("there must be at least one mac")[0] ^= 0xff;
255 let mut validating =
256 new_incremental(key, TEST_CHUNK_SIZE).validating(&failing_first_update);
257 validating.update(bytes).expect_err("MacError");
258 }
259
260 {
261 let mut failing_finalize = expected_bytes.clone();
262 failing_finalize
263 .last_mut()
264 .expect("there must be at least one mac")[0] ^= 0xff;
265 let mut validating =
266 new_incremental(key, TEST_CHUNK_SIZE).validating(&failing_finalize);
267 validating.update(bytes).expect("update should succeed");
268 validating.finalize().expect_err("MacError");
269 }
270
271 {
272 let missing_last_mac = &expected_bytes[0..expected_bytes.len() - 1];
273 let mut validating = new_incremental(key, TEST_CHUNK_SIZE).validating(missing_last_mac);
274 validating.update(bytes).expect("update should succeed");
275 validating.finalize().expect_err("MacError");
276 }
277
278 {
279 let missing_first_mac: Vec<_> = expected_bytes.clone().into_iter().skip(1).collect();
280 let mut validating =
281 new_incremental(key, TEST_CHUNK_SIZE).validating(&missing_first_mac);
282 validating.update(bytes).expect_err("MacError");
283 }
284 std::hint::black_box(expected_bytes);
286 }
287
288 #[test]
289 fn validating_returns_right_size() {
290 let key = TEST_HMAC_KEY;
291 let input = "this is a simple test input string";
292
293 let bytes = input.as_bytes();
294 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
295 let mut expected_macs: Vec<_> = incremental.update(bytes).collect();
296 expected_macs.push(incremental.finalize());
297
298 let expected_bytes: Vec<[u8; 32]> =
299 expected_macs.into_iter().map(|mac| mac.into()).collect();
300
301 let mut validating = new_incremental(key, TEST_CHUNK_SIZE).validating(&expected_bytes);
302
303 let input_chunks = bytes.chunks(16).collect::<Vec<_>>();
306 assert_eq!(3, input_chunks.len());
307 let expected_remainder = bytes.len() - TEST_CHUNK_SIZE;
308
309 for (expected_size, input) in std::iter::zip([0, TEST_CHUNK_SIZE, 0], input_chunks) {
310 assert_eq!(
311 expected_size,
312 validating
313 .update(input)
314 .expect("update: validation should succeed")
315 );
316 }
317 assert_eq!(
318 expected_remainder,
319 validating
320 .finalize()
321 .expect("finalize: validation should succeed")
322 );
323 }
324
325 #[test]
326 fn produce_and_validate() {
327 let key = TEST_HMAC_KEY;
328
329 proptest!(|(input in ".{0,100}")| {
330 let bytes = input.as_bytes();
331 let mut incremental = new_incremental(key, TEST_CHUNK_SIZE);
332 let input_chunks = bytes.random_chunks(incremental.chunk_size*2);
333
334 let mut produced: Vec<[u8; 32]> = input_chunks.clone()
335 .flat_map(|chunk| incremental.update(chunk).collect::<Vec<_>>())
336 .map(|out| out.into())
337 .collect();
338 produced.push(incremental.finalize().into());
339
340 let mut validating = new_incremental(key, TEST_CHUNK_SIZE).validating(&produced);
341 for chunk in input_chunks.clone() {
342 validating.update(chunk).expect("update: validation should succeed");
343 }
344 validating.finalize().expect("finalize: validation should succeed");
345 });
346 }
347
348 const KIBIBYTES: usize = 1024;
349 const MEBIBYTES: usize = 1024 * KIBIBYTES;
350 const GIBIBYTES: usize = 1024 * MEBIBYTES;
351
352 #[test]
353 fn chunk_sizes_sha256() {
354 for (data_size, expected) in [
355 (0, MINIMUM_CHUNK_SIZE),
356 (KIBIBYTES, MINIMUM_CHUNK_SIZE),
357 (10 * KIBIBYTES, MINIMUM_CHUNK_SIZE),
358 (100 * KIBIBYTES, MINIMUM_CHUNK_SIZE),
359 (MEBIBYTES, MINIMUM_CHUNK_SIZE),
360 (10 * MEBIBYTES, MINIMUM_CHUNK_SIZE),
361 (20 * MEBIBYTES, 80 * KIBIBYTES),
362 (100 * MEBIBYTES, 400 * KIBIBYTES),
363 (200 * MEBIBYTES, 800 * KIBIBYTES),
364 (256 * MEBIBYTES, MEBIBYTES),
365 (512 * MEBIBYTES, 2 * MEBIBYTES),
366 (GIBIBYTES, 2 * MEBIBYTES),
367 (2 * GIBIBYTES, 2 * MEBIBYTES),
368 ] {
369 let actual = calculate_chunk_size::<Sha256>(data_size);
370 assert_eq!(actual, expected);
371 }
372 }
373
374 #[test]
375 fn chunk_sizes_sha512() {
376 for (data_size, expected) in [
377 (0, MINIMUM_CHUNK_SIZE),
378 (KIBIBYTES, MINIMUM_CHUNK_SIZE),
379 (10 * KIBIBYTES, MINIMUM_CHUNK_SIZE),
380 (100 * KIBIBYTES, MINIMUM_CHUNK_SIZE),
381 (MEBIBYTES, MINIMUM_CHUNK_SIZE),
382 (10 * MEBIBYTES, 80 * KIBIBYTES),
383 (20 * MEBIBYTES, 160 * KIBIBYTES),
384 (100 * MEBIBYTES, 800 * KIBIBYTES),
385 (200 * MEBIBYTES, 1600 * KIBIBYTES),
386 (256 * MEBIBYTES, 2 * MEBIBYTES),
387 (512 * MEBIBYTES, 2 * MEBIBYTES),
388 (GIBIBYTES, 2 * MEBIBYTES),
389 ] {
390 let actual = calculate_chunk_size::<sha2::Sha512>(data_size);
391 assert_eq!(actual, expected);
392 }
393 }
394
395 #[test]
396 fn total_digest_size_is_never_too_big() {
397 fn total_digest_size(data_size: usize) -> usize {
398 let chunk_size = calculate_chunk_size::<Sha256>(data_size);
399 let num_chunks = std::cmp::max(1, data_size.div_ceil(chunk_size));
400 num_chunks * <Sha256 as OutputSizeUser>::OutputSize::USIZE
401 }
402 let config = ProptestConfig::with_cases(10_000);
403 proptest!(config, |(data_size in 256..256*MEBIBYTES)| {
404 assert!(total_digest_size(data_size) <= 8*KIBIBYTES)
405 });
406 proptest!(|(data_size_mib in 256_usize..2048)| {
407 assert!(total_digest_size(data_size_mib*MEBIBYTES) <= 32*KIBIBYTES)
408 });
409 }
410
411 #[derive(Clone)]
412 struct RandomChunks<'a, T, R: Rng> {
413 base: &'a [T],
414 distribution: UniformUsize,
415 rng: R,
416 }
417
418 impl<'a, T, R: Rng> Iterator for RandomChunks<'a, T, R> {
419 type Item = &'a [T];
420
421 fn next(&mut self) -> Option<Self::Item> {
422 if self.base.is_empty() {
423 None
424 } else {
425 let candidate = self.distribution.sample(&mut self.rng);
426 let chunk_size = std::cmp::min(candidate, self.base.len());
427 let (before, after) = self.base.split_at(chunk_size);
428 self.base = after;
429 Some(before)
430 }
431 }
432 }
433
434 trait RandomChunksIterator<T> {
435 fn random_chunks(&self, max_size: usize) -> RandomChunks<'_, T, ThreadRng>;
436 }
437
438 impl<T> RandomChunksIterator<T> for [T] {
439 fn random_chunks(&self, max_size: usize) -> RandomChunks<'_, T, ThreadRng> {
440 assert!(max_size > 0, "Maximal chunk size should be positive");
441 RandomChunks {
442 base: self,
443 distribution: UniformUsize::new_inclusive(0, max_size).expect("valid range"),
444 rng: Default::default(),
445 }
446 }
447 }
448}