Skip to main content

oxicrypto_hash/
hash_builder.rs

1// ── HashBuilder: fluent construction of hash instances ──────────────────────
2//
3//! Ergonomic builder API for constructing boxed hash instances.
4//!
5//! Instead of naming concrete types directly, callers can fluently select an
6//! algorithm and (optionally) switch to streaming mode:
7//!
8//! ```
9//! use oxicrypto_hash::HashBuilder;
10//! use oxicrypto_core::{Hash, StreamingHash};
11//!
12//! // One-shot: returns `Box<dyn Hash>`.
13//! let hasher = HashBuilder::sha256().build();
14//! let digest = hasher.hash_to_vec(b"abc").unwrap();
15//! assert_eq!(digest.len(), 32);
16//!
17//! // Streaming: returns a sized [`DynStreamingHash`] enum that implements
18//! // `StreamingHash` (a boxed `dyn StreamingHash` could not be `finalize`d
19//! // because `finalize` consumes `self`).
20//! let mut streaming = HashBuilder::sha256().streaming().build();
21//! streaming.update(b"a");
22//! streaming.update(b"bc");
23//! let mut out = [0u8; 32];
24//! streaming.finalize(out.as_mut_slice()).unwrap();
25//! assert_eq!(&out[..], digest.as_slice());
26//! ```
27//!
28//! The set of algorithms covers the SHA-2 family (SHA-256/384/512, SHA-512/256),
29//! the SHA-3 family (SHA3-256/384/512), and BLAKE3.
30
31use alloc::boxed::Box;
32
33use oxicrypto_core::{Hash, StreamingHash};
34
35use crate::{
36    Blake3, Blake3Streaming, Sha256, Sha256Streaming, Sha384, Sha384Streaming, Sha3_256,
37    Sha3_256Streaming, Sha3_384, Sha3_384Streaming, Sha3_512, Sha3_512Streaming, Sha512,
38    Sha512Streaming, Sha512_256, Sha512_256Streaming,
39};
40
41/// Hash algorithm selector used by [`HashBuilder`].
42///
43/// Covers every algorithm the builder can construct.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum HashAlgorithm {
46    /// SHA-256 (FIPS 180-4).
47    Sha256,
48    /// SHA-384 (FIPS 180-4).
49    Sha384,
50    /// SHA-512 (FIPS 180-4).
51    Sha512,
52    /// SHA-512/256 truncated (FIPS 180-4 §6.7).
53    Sha512_256,
54    /// SHA3-256 (FIPS 202).
55    Sha3_256,
56    /// SHA3-384 (FIPS 202).
57    Sha3_384,
58    /// SHA3-512 (FIPS 202).
59    Sha3_512,
60    /// BLAKE3 (32-byte output).
61    Blake3,
62}
63
64impl HashAlgorithm {
65    /// Digest output length in bytes for this algorithm.
66    #[must_use]
67    pub const fn output_len(self) -> usize {
68        match self {
69            HashAlgorithm::Sha256
70            | HashAlgorithm::Sha512_256
71            | HashAlgorithm::Sha3_256
72            | HashAlgorithm::Blake3 => 32,
73            HashAlgorithm::Sha384 | HashAlgorithm::Sha3_384 => 48,
74            HashAlgorithm::Sha512 | HashAlgorithm::Sha3_512 => 64,
75        }
76    }
77}
78
79/// Fluent builder producing a boxed one-shot [`Hash`] instance.
80///
81/// Construct via one of the algorithm constructors (e.g. [`HashBuilder::sha256`]),
82/// then call [`build`](HashBuilder::build) for a `Box<dyn Hash>`, or switch to
83/// streaming mode with [`streaming`](HashBuilder::streaming).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct HashBuilder {
86    algorithm: HashAlgorithm,
87}
88
89impl HashBuilder {
90    /// Begin building with an explicit [`HashAlgorithm`].
91    #[must_use]
92    pub const fn new(algorithm: HashAlgorithm) -> Self {
93        Self { algorithm }
94    }
95
96    /// Select SHA-256.
97    #[must_use]
98    pub const fn sha256() -> Self {
99        Self::new(HashAlgorithm::Sha256)
100    }
101
102    /// Select SHA-384.
103    #[must_use]
104    pub const fn sha384() -> Self {
105        Self::new(HashAlgorithm::Sha384)
106    }
107
108    /// Select SHA-512.
109    #[must_use]
110    pub const fn sha512() -> Self {
111        Self::new(HashAlgorithm::Sha512)
112    }
113
114    /// Select SHA-512/256.
115    #[must_use]
116    pub const fn sha512_256() -> Self {
117        Self::new(HashAlgorithm::Sha512_256)
118    }
119
120    /// Select SHA3-256.
121    #[must_use]
122    pub const fn sha3_256() -> Self {
123        Self::new(HashAlgorithm::Sha3_256)
124    }
125
126    /// Select SHA3-384.
127    #[must_use]
128    pub const fn sha3_384() -> Self {
129        Self::new(HashAlgorithm::Sha3_384)
130    }
131
132    /// Select SHA3-512.
133    #[must_use]
134    pub const fn sha3_512() -> Self {
135        Self::new(HashAlgorithm::Sha3_512)
136    }
137
138    /// Select BLAKE3.
139    #[must_use]
140    pub const fn blake3() -> Self {
141        Self::new(HashAlgorithm::Blake3)
142    }
143
144    /// The algorithm currently selected.
145    #[must_use]
146    pub const fn algorithm(&self) -> HashAlgorithm {
147        self.algorithm
148    }
149
150    /// Switch to streaming mode, returning a [`StreamingHashBuilder`].
151    #[must_use]
152    pub const fn streaming(self) -> StreamingHashBuilder {
153        StreamingHashBuilder {
154            algorithm: self.algorithm,
155        }
156    }
157
158    /// Build a boxed one-shot [`Hash`] for the selected algorithm.
159    #[must_use]
160    pub fn build(self) -> Box<dyn Hash> {
161        match self.algorithm {
162            HashAlgorithm::Sha256 => Box::new(Sha256),
163            HashAlgorithm::Sha384 => Box::new(Sha384),
164            HashAlgorithm::Sha512 => Box::new(Sha512),
165            HashAlgorithm::Sha512_256 => Box::new(Sha512_256),
166            HashAlgorithm::Sha3_256 => Box::new(Sha3_256),
167            HashAlgorithm::Sha3_384 => Box::new(Sha3_384),
168            HashAlgorithm::Sha3_512 => Box::new(Sha3_512),
169            HashAlgorithm::Blake3 => Box::new(Blake3),
170        }
171    }
172}
173
174/// Fluent builder producing a [`DynStreamingHash`] instance.
175///
176/// Obtained from [`HashBuilder::streaming`]. Call
177/// [`build`](StreamingHashBuilder::build) for a [`DynStreamingHash`].
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct StreamingHashBuilder {
180    algorithm: HashAlgorithm,
181}
182
183impl StreamingHashBuilder {
184    /// Begin building a streaming hasher with an explicit [`HashAlgorithm`].
185    #[must_use]
186    pub const fn new(algorithm: HashAlgorithm) -> Self {
187        Self { algorithm }
188    }
189
190    /// The algorithm currently selected.
191    #[must_use]
192    pub const fn algorithm(&self) -> HashAlgorithm {
193        self.algorithm
194    }
195
196    /// Build a [`DynStreamingHash`] for the selected algorithm.
197    #[must_use]
198    pub fn build(self) -> DynStreamingHash {
199        match self.algorithm {
200            HashAlgorithm::Sha256 => DynStreamingHash::Sha256(Sha256Streaming::new()),
201            HashAlgorithm::Sha384 => DynStreamingHash::Sha384(Sha384Streaming::new()),
202            HashAlgorithm::Sha512 => DynStreamingHash::Sha512(Sha512Streaming::new()),
203            HashAlgorithm::Sha512_256 => DynStreamingHash::Sha512_256(Sha512_256Streaming::new()),
204            HashAlgorithm::Sha3_256 => DynStreamingHash::Sha3_256(Sha3_256Streaming::new()),
205            HashAlgorithm::Sha3_384 => DynStreamingHash::Sha3_384(Sha3_384Streaming::new()),
206            HashAlgorithm::Sha3_512 => DynStreamingHash::Sha3_512(Sha3_512Streaming::new()),
207            HashAlgorithm::Blake3 => DynStreamingHash::Blake3(Box::default()),
208        }
209    }
210}
211
212/// Runtime-dispatched streaming hasher returned by [`StreamingHashBuilder::build`].
213///
214/// This is a *sized* enum (not a `Box<dyn StreamingHash>`): a boxed trait object
215/// could not be passed to [`StreamingHash::finalize`], which consumes `self` by
216/// value and therefore requires a `Sized` receiver. `DynStreamingHash` itself
217/// implements [`StreamingHash`], dispatching to the wrapped concrete hasher.
218pub enum DynStreamingHash {
219    /// Streaming SHA-256.
220    Sha256(Sha256Streaming),
221    /// Streaming SHA-384.
222    Sha384(Sha384Streaming),
223    /// Streaming SHA-512.
224    Sha512(Sha512Streaming),
225    /// Streaming SHA-512/256.
226    Sha512_256(Sha512_256Streaming),
227    /// Streaming SHA3-256.
228    Sha3_256(Sha3_256Streaming),
229    /// Streaming SHA3-384.
230    Sha3_384(Sha3_384Streaming),
231    /// Streaming SHA3-512.
232    Sha3_512(Sha3_512Streaming),
233    /// Streaming BLAKE3.
234    ///
235    /// Boxed because `blake3::Hasher` is far larger than the digest-based
236    /// streaming states, which would otherwise bloat every enum value.
237    Blake3(Box<Blake3Streaming>),
238}
239
240impl StreamingHash for DynStreamingHash {
241    fn update(&mut self, data: &[u8]) {
242        match self {
243            DynStreamingHash::Sha256(h) => h.update(data),
244            DynStreamingHash::Sha384(h) => h.update(data),
245            DynStreamingHash::Sha512(h) => h.update(data),
246            DynStreamingHash::Sha512_256(h) => h.update(data),
247            DynStreamingHash::Sha3_256(h) => h.update(data),
248            DynStreamingHash::Sha3_384(h) => h.update(data),
249            DynStreamingHash::Sha3_512(h) => h.update(data),
250            DynStreamingHash::Blake3(h) => h.update(data),
251        }
252    }
253
254    fn finalize(self, out: &mut [u8]) -> Result<(), oxicrypto_core::CryptoError> {
255        match self {
256            DynStreamingHash::Sha256(h) => h.finalize(out),
257            DynStreamingHash::Sha384(h) => h.finalize(out),
258            DynStreamingHash::Sha512(h) => h.finalize(out),
259            DynStreamingHash::Sha512_256(h) => h.finalize(out),
260            DynStreamingHash::Sha3_256(h) => h.finalize(out),
261            DynStreamingHash::Sha3_384(h) => h.finalize(out),
262            DynStreamingHash::Sha3_512(h) => h.finalize(out),
263            DynStreamingHash::Blake3(h) => (*h).finalize(out),
264        }
265    }
266
267    fn reset(&mut self) {
268        match self {
269            DynStreamingHash::Sha256(h) => h.reset(),
270            DynStreamingHash::Sha384(h) => h.reset(),
271            DynStreamingHash::Sha512(h) => h.reset(),
272            DynStreamingHash::Sha512_256(h) => h.reset(),
273            DynStreamingHash::Sha3_256(h) => h.reset(),
274            DynStreamingHash::Sha3_384(h) => h.reset(),
275            DynStreamingHash::Sha3_512(h) => h.reset(),
276            DynStreamingHash::Blake3(h) => h.reset(),
277        }
278    }
279}
280
281impl DynStreamingHash {
282    /// The [`HashAlgorithm`] this streaming hasher computes.
283    #[must_use]
284    pub const fn algorithm(&self) -> HashAlgorithm {
285        match self {
286            DynStreamingHash::Sha256(_) => HashAlgorithm::Sha256,
287            DynStreamingHash::Sha384(_) => HashAlgorithm::Sha384,
288            DynStreamingHash::Sha512(_) => HashAlgorithm::Sha512,
289            DynStreamingHash::Sha512_256(_) => HashAlgorithm::Sha512_256,
290            DynStreamingHash::Sha3_256(_) => HashAlgorithm::Sha3_256,
291            DynStreamingHash::Sha3_384(_) => HashAlgorithm::Sha3_384,
292            DynStreamingHash::Sha3_512(_) => HashAlgorithm::Sha3_512,
293            DynStreamingHash::Blake3(_) => HashAlgorithm::Blake3,
294        }
295    }
296}
297
298impl core::fmt::Debug for DynStreamingHash {
299    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
300        f.debug_tuple("DynStreamingHash")
301            .field(&self.algorithm())
302            .finish()
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::{Blake3, Sha256, Sha384, Sha3_256, Sha3_384, Sha3_512, Sha512, Sha512_256};
310
311    /// Every algorithm the builder supports, paired with its direct one-shot type.
312    fn direct_hash(algo: HashAlgorithm, msg: &[u8]) -> alloc::vec::Vec<u8> {
313        match algo {
314            HashAlgorithm::Sha256 => Sha256.hash_to_vec(msg).unwrap(),
315            HashAlgorithm::Sha384 => Sha384.hash_to_vec(msg).unwrap(),
316            HashAlgorithm::Sha512 => Sha512.hash_to_vec(msg).unwrap(),
317            HashAlgorithm::Sha512_256 => Sha512_256.hash_to_vec(msg).unwrap(),
318            HashAlgorithm::Sha3_256 => Sha3_256.hash_to_vec(msg).unwrap(),
319            HashAlgorithm::Sha3_384 => Sha3_384.hash_to_vec(msg).unwrap(),
320            HashAlgorithm::Sha3_512 => Sha3_512.hash_to_vec(msg).unwrap(),
321            HashAlgorithm::Blake3 => Blake3.hash_to_vec(msg).unwrap(),
322        }
323    }
324
325    const ALL: [HashAlgorithm; 8] = [
326        HashAlgorithm::Sha256,
327        HashAlgorithm::Sha384,
328        HashAlgorithm::Sha512,
329        HashAlgorithm::Sha512_256,
330        HashAlgorithm::Sha3_256,
331        HashAlgorithm::Sha3_384,
332        HashAlgorithm::Sha3_512,
333        HashAlgorithm::Blake3,
334    ];
335
336    #[test]
337    fn builder_one_shot_matches_direct_api() {
338        let msg = b"The quick brown fox jumps over the lazy dog";
339        for algo in ALL {
340            let built = HashBuilder::new(algo).build();
341            let via_builder = built.hash_to_vec(msg).unwrap();
342            let direct = direct_hash(algo, msg);
343            assert_eq!(
344                via_builder, direct,
345                "builder one-shot must equal direct API for {algo:?}"
346            );
347        }
348    }
349
350    #[test]
351    fn builder_output_len_matches_trait() {
352        for algo in ALL {
353            let built = HashBuilder::new(algo).build();
354            assert_eq!(
355                built.output_len(),
356                algo.output_len(),
357                "HashAlgorithm::output_len must match trait output_len for {algo:?}"
358            );
359        }
360    }
361
362    #[test]
363    fn builder_streaming_matches_one_shot() {
364        let msg = b"streaming-vs-one-shot equivalence payload";
365        for algo in ALL {
366            let direct = direct_hash(algo, msg);
367
368            let mut streaming = HashBuilder::new(algo).streaming().build();
369            // Feed in three uneven chunks.
370            streaming.update(&msg[..7]);
371            streaming.update(&msg[7..20]);
372            streaming.update(&msg[20..]);
373
374            let mut out = alloc::vec![0u8; algo.output_len()];
375            streaming.finalize(out.as_mut_slice()).unwrap();
376
377            assert_eq!(
378                out, direct,
379                "builder streaming must equal one-shot for {algo:?}"
380            );
381        }
382    }
383
384    #[test]
385    fn builder_streaming_byte_at_a_time() {
386        let msg = b"abc";
387        for algo in ALL {
388            let direct = direct_hash(algo, msg);
389
390            let mut streaming = HashBuilder::new(algo).streaming().build();
391            for byte in msg {
392                streaming.update(core::slice::from_ref(byte));
393            }
394            let mut out = alloc::vec![0u8; algo.output_len()];
395            streaming.finalize(out.as_mut_slice()).unwrap();
396
397            assert_eq!(
398                out, direct,
399                "byte-at-a-time streaming must equal one-shot for {algo:?}"
400            );
401        }
402    }
403
404    #[test]
405    fn named_constructors_select_expected_algorithm() {
406        assert_eq!(HashBuilder::sha256().algorithm(), HashAlgorithm::Sha256);
407        assert_eq!(HashBuilder::sha384().algorithm(), HashAlgorithm::Sha384);
408        assert_eq!(HashBuilder::sha512().algorithm(), HashAlgorithm::Sha512);
409        assert_eq!(
410            HashBuilder::sha512_256().algorithm(),
411            HashAlgorithm::Sha512_256
412        );
413        assert_eq!(HashBuilder::sha3_256().algorithm(), HashAlgorithm::Sha3_256);
414        assert_eq!(HashBuilder::sha3_384().algorithm(), HashAlgorithm::Sha3_384);
415        assert_eq!(HashBuilder::sha3_512().algorithm(), HashAlgorithm::Sha3_512);
416        assert_eq!(HashBuilder::blake3().algorithm(), HashAlgorithm::Blake3);
417    }
418
419    #[test]
420    fn streaming_preserves_algorithm() {
421        for algo in ALL {
422            let b = HashBuilder::new(algo).streaming();
423            assert_eq!(b.algorithm(), algo);
424        }
425    }
426
427    #[test]
428    fn fluent_sha256_example_round_trips() {
429        // Mirror the doc-comment example end to end.
430        let hasher = HashBuilder::sha256().build();
431        let digest = hasher.hash_to_vec(b"abc").unwrap();
432
433        let mut streaming = HashBuilder::sha256().streaming().build();
434        streaming.update(b"a");
435        streaming.update(b"bc");
436        let mut out = [0u8; 32];
437        streaming.finalize(out.as_mut_slice()).unwrap();
438
439        assert_eq!(&out[..], digest.as_slice());
440    }
441}