Skip to main content

oxicrypto_mac/
hmac_streaming_hash.rs

1//! Generic HMAC adapter that accepts any [`StreamingHash`] implementation.
2//!
3//! This module provides [`StreamingHashHmac`] — a structurally correct HMAC
4//! (RFC 2104) implementation that derives its inner hash from the
5//! [`oxicrypto_core::StreamingHash`] trait rather than the `digest` crate's
6//! `Digest` trait.  This allows callers to use any `oxicrypto-hash` streaming
7//! hasher (SHA-256, SHA-512, BLAKE3, BLAKE2b-512, …) as the underlying PRF
8//! without coupling `oxicrypto-mac` to the concrete hash crate.
9//!
10//! # Design
11//!
12//! HMAC requires two things beyond what `StreamingHash` exposes:
13//!
14//! 1. **Block size** — the internal compression block width (64 bytes for
15//!    SHA-256, 128 bytes for SHA-512/SHA-384, 64 bytes for BLAKE3).
16//! 2. **Fresh instances** — the RFC 2104 construction hashes three independent
17//!    sub-messages (key-hash if key > block, inner, outer), so three separate
18//!    hasher instances are needed in the general case.
19//!
20//! Both are supplied by the caller through the `block_size` parameter and a
21//! factory closure `F: Fn() -> H`.  The resulting [`StreamingHashHmac`] is
22//! independent of any specific hash algorithm.
23//!
24//! # HMAC construction (RFC 2104)
25//!
26//! ```text
27//! K'  = K if |K| ≤ B, else H(K)
28//! K'' = K' ‖ 0x00^(B - |K'|)        // zero-pad to B bytes
29//! ipad = 0x36^B
30//! opad = 0x5c^B
31//! HMAC = H(K'' ⊕ opad ‖ H(K'' ⊕ ipad ‖ message))
32//! ```
33//!
34//! where `B` = `block_size` and `H` = the streaming hasher created by the
35//! factory.
36
37extern crate alloc;
38use alloc::vec;
39use alloc::vec::Vec;
40
41use oxicrypto_core::{CryptoError, StreamingHash};
42use subtle::ConstantTimeEq;
43
44/// Generic HMAC over any [`StreamingHash`] implementation.
45///
46/// The type parameter `H` is the underlying hash; `F` is the factory that
47/// creates fresh instances of `H`.  Both `H` and `F` must be `Send` to allow
48/// the MAC to cross thread boundaries.
49///
50/// # Construction
51///
52/// Use [`StreamingHashHmac::new`] to provide a key, block size, and hash
53/// factory.  The resulting value implements one-shot [`StreamingHashHmac::mac_oneshot`]
54/// and incremental [`StreamingHashHmac::streaming_session`].
55///
56/// # Example
57///
58/// ```rust,ignore
59/// use oxicrypto_hash::Sha256Streaming;
60/// use oxicrypto_mac::hmac_streaming_hash::StreamingHashHmac;
61///
62/// let key = b"secret-key-for-hmac";
63/// let msg = b"hello, world";
64/// let mut tag = [0u8; 32];
65/// let mut hmac = StreamingHashHmac::new(key, 64, || Sha256Streaming::new())?;
66/// hmac.mac_oneshot(msg, &mut tag)?;
67/// ```
68pub struct StreamingHashHmac<H, F>
69where
70    H: StreamingHash,
71    F: Fn() -> H + Send,
72{
73    /// Zero-padded, optionally pre-hashed key (length == block_size).
74    padded_key: Vec<u8>,
75    /// Hash block size (bytes).
76    block_size: usize,
77    /// Output length of the underlying hash (bytes).
78    output_len: usize,
79    /// Factory for creating fresh hasher instances.
80    factory: F,
81}
82
83impl<H, F> StreamingHashHmac<H, F>
84where
85    H: StreamingHash,
86    F: Fn() -> H + Send,
87{
88    /// Construct an HMAC instance with the given `key`, hash `block_size`, and
89    /// `output_len` of the underlying `H`.
90    ///
91    /// - If `key.len() > block_size` the key is pre-hashed using a fresh
92    ///   hasher from `factory`.
93    /// - The padded key is zero-extended to exactly `block_size` bytes.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`CryptoError::BadInput`] when `block_size` or `output_len` is
98    /// zero, or when key pre-hashing would write into a zero-length buffer.
99    pub fn new(
100        key: &[u8],
101        block_size: usize,
102        output_len: usize,
103        factory: F,
104    ) -> Result<Self, CryptoError> {
105        if block_size == 0 || output_len == 0 {
106            return Err(CryptoError::BadInput);
107        }
108
109        // If key > block_size, hash it first (RFC 2104 §3).
110        let effective_key: Vec<u8> = if key.len() > block_size {
111            let mut hashed = vec![0u8; output_len];
112            let mut h = (factory)();
113            h.update(key);
114            h.finalize(&mut hashed)?;
115            hashed
116        } else {
117            key.to_vec()
118        };
119
120        // Zero-pad to exactly block_size.
121        let mut padded_key = vec![0u8; block_size];
122        let copy_len = effective_key.len().min(block_size);
123        padded_key[..copy_len].copy_from_slice(&effective_key[..copy_len]);
124
125        Ok(Self {
126            padded_key,
127            block_size,
128            output_len,
129            factory,
130        })
131    }
132
133    /// Compute a one-shot HMAC tag over `msg`, writing into `out`.
134    ///
135    /// `out.len()` must be at least `self.output_len()`.
136    ///
137    /// # Errors
138    ///
139    /// - [`CryptoError::BufferTooSmall`] if `out.len() < output_len`.
140    pub fn mac_oneshot(&self, msg: &[u8], out: &mut [u8]) -> Result<(), CryptoError> {
141        if out.len() < self.output_len {
142            return Err(CryptoError::BufferTooSmall);
143        }
144
145        // Pre-compute ipad/opad keys as contiguous byte slices.
146        let ipad_key: Vec<u8> = self.padded_key.iter().map(|b| b ^ 0x36u8).collect();
147        let opad_key: Vec<u8> = self.padded_key.iter().map(|b| b ^ 0x5cu8).collect();
148
149        // inner = H(ipad_key || msg)
150        let mut inner_tag = vec![0u8; self.output_len];
151        {
152            let mut h = (self.factory)();
153            h.update(&ipad_key);
154            h.update(msg);
155            h.finalize(&mut inner_tag)?;
156        }
157
158        // outer = H(opad_key || inner)
159        {
160            let mut h = (self.factory)();
161            h.update(&opad_key);
162            h.update(&inner_tag);
163            h.finalize(&mut out[..self.output_len])?;
164        }
165
166        Ok(())
167    }
168
169    /// The hash output length in bytes.
170    pub fn output_len(&self) -> usize {
171        self.output_len
172    }
173
174    /// The hash block size in bytes.
175    pub fn block_size(&self) -> usize {
176        self.block_size
177    }
178
179    /// Constant-time verification: compute the HMAC and compare to `expected`.
180    ///
181    /// Returns `Ok(())` if they match, [`CryptoError::InvalidTag`] otherwise.
182    pub fn verify(&self, msg: &[u8], expected: &[u8]) -> Result<(), CryptoError> {
183        if expected.len() != self.output_len {
184            return Err(CryptoError::InvalidTag);
185        }
186        let mut tag = vec![0u8; self.output_len];
187        self.mac_oneshot(msg, &mut tag)?;
188        if tag.as_slice().ct_eq(expected).into() {
189            Ok(())
190        } else {
191            Err(CryptoError::InvalidTag)
192        }
193    }
194
195    /// Create an incremental streaming HMAC session.
196    ///
197    /// Returns a [`StreamingHashHmacSession`] that accepts data via
198    /// `update()` and produces the final tag via `finalize()`.
199    pub fn streaming_session(&self) -> StreamingHashHmacSession<H, F>
200    where
201        F: Clone,
202    {
203        StreamingHashHmacSession::new(self)
204    }
205}
206
207// ── Incremental streaming session ────────────────────────────────────────────
208
209/// Incremental HMAC session.
210///
211/// Created by [`StreamingHashHmac::streaming_session`].  Maintains the inner
212/// hasher state pre-loaded with `ipad_key`, ready for message data via
213/// [`update`](Self::update).  Calling [`finalize`](Self::finalize) computes
214/// the outer hash and returns the final HMAC tag.
215pub struct StreamingHashHmacSession<H, F>
216where
217    H: StreamingHash,
218    F: Fn() -> H + Send,
219{
220    /// Inner hasher pre-loaded with `H(ipad_key ‖ …)`.
221    inner: H,
222    /// Outer padded key `opad_key` bytes, ready to prefix the outer hash.
223    opad_key: Vec<u8>,
224    /// Output length of the underlying hash.
225    output_len: usize,
226    /// Factory stored for the outer hash creation.
227    factory: F,
228}
229
230impl<H, F> StreamingHashHmacSession<H, F>
231where
232    H: StreamingHash,
233    F: Fn() -> H + Send + Clone,
234{
235    fn new(hmac: &StreamingHashHmac<H, F>) -> Self
236    where
237        F: Clone,
238    {
239        let ipad_key: Vec<u8> = hmac.padded_key.iter().map(|b| b ^ 0x36u8).collect();
240        let opad_key: Vec<u8> = hmac.padded_key.iter().map(|b| b ^ 0x5cu8).collect();
241
242        let mut inner = (hmac.factory)();
243        inner.update(&ipad_key);
244
245        Self {
246            inner,
247            opad_key,
248            output_len: hmac.output_len,
249            factory: hmac.factory.clone(),
250        }
251    }
252
253    /// Feed additional message bytes into the inner hash.
254    pub fn update(&mut self, data: &[u8]) {
255        self.inner.update(data);
256    }
257
258    /// Finalise the inner hash and compute the outer HMAC, writing the tag into
259    /// `out`.
260    ///
261    /// Consumes `self`.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`CryptoError::BufferTooSmall`] if `out.len() < output_len`.
266    pub fn finalize(self, out: &mut [u8]) -> Result<(), CryptoError> {
267        if out.len() < self.output_len {
268            return Err(CryptoError::BufferTooSmall);
269        }
270
271        // Finalise inner hash.
272        let mut inner_tag = vec![0u8; self.output_len];
273        self.inner.finalize(&mut inner_tag)?;
274
275        // Outer = H(opad_key ‖ inner_tag).
276        let mut outer = (self.factory)();
277        outer.update(&self.opad_key);
278        outer.update(&inner_tag);
279        outer.finalize(&mut out[..self.output_len])?;
280
281        Ok(())
282    }
283}
284
285// ── Free functions ────────────────────────────────────────────────────────────
286
287/// Compute an HMAC tag using any [`StreamingHash`] created by `make_hash`.
288///
289/// This is the lowest-friction entry point: supply the key, block size,
290/// expected output length, message, and a no-argument closure that returns a
291/// fresh `StreamingHash`.
292///
293/// ```rust,ignore
294/// let tag = hmac_with_streaming_hash(
295///     b"key", 64, 32, b"message",
296///     || oxicrypto_hash::Sha256Streaming::new(),
297/// )?;
298/// ```
299///
300/// # Errors
301///
302/// Returns [`CryptoError::BadInput`] for zero `block_size` / `output_len`,
303/// or [`CryptoError::BufferTooSmall`] if internal buffer logic fails.
304pub fn hmac_with_streaming_hash<H, F>(
305    key: &[u8],
306    block_size: usize,
307    output_len: usize,
308    msg: &[u8],
309    make_hash: F,
310) -> Result<Vec<u8>, CryptoError>
311where
312    H: StreamingHash,
313    F: Fn() -> H + Send,
314{
315    let hmac = StreamingHashHmac::new(key, block_size, output_len, make_hash)?;
316    let mut tag = vec![0u8; output_len];
317    hmac.mac_oneshot(msg, &mut tag)?;
318    Ok(tag)
319}
320
321// ── Unit tests ───────────────────────────────────────────────────────────────
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    // Minimal in-memory streaming hash for unit tests (SHA-256 via sha2 crate).
328    struct SimpleSha256Hasher {
329        inner: sha2::Sha256,
330    }
331
332    impl SimpleSha256Hasher {
333        fn new() -> Self {
334            use sha2::Digest;
335            Self {
336                inner: sha2::Sha256::new(),
337            }
338        }
339    }
340
341    impl StreamingHash for SimpleSha256Hasher {
342        fn update(&mut self, data: &[u8]) {
343            sha2::Digest::update(&mut self.inner, data);
344        }
345
346        fn finalize(self, out: &mut [u8]) -> Result<(), CryptoError> {
347            use sha2::Digest;
348            if out.len() < 32 {
349                return Err(CryptoError::BufferTooSmall);
350            }
351            let result = self.inner.finalize();
352            out[..32].copy_from_slice(&result);
353            Ok(())
354        }
355
356        fn reset(&mut self) {
357            sha2::Digest::reset(&mut self.inner);
358        }
359    }
360
361    // RFC 4231 Test Case 1: key=20×0x0b, data="Hi There", SHA-256
362    // Expected tag: b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
363    #[test]
364    fn test_hmac_sha256_rfc4231_tc1() {
365        let key = vec![0x0bu8; 20];
366        let msg = b"Hi There";
367        let expected =
368            hex_decode("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
369
370        let result = hmac_with_streaming_hash(&key, 64, 32, msg, SimpleSha256Hasher::new)
371            .expect("hmac_with_streaming_hash failed");
372
373        assert_eq!(result, expected);
374    }
375
376    // RFC 4231 Test Case 2: key="Jefe", data="what do ya want for nothing?"
377    // Expected (Python hmac.new("Jefe", "what do ya want for nothing?", sha256).hexdigest()):
378    // 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
379    #[test]
380    fn test_hmac_sha256_rfc4231_tc2() {
381        let key = b"Jefe";
382        let msg = b"what do ya want for nothing?";
383        let expected =
384            hex_decode("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843");
385
386        let result = hmac_with_streaming_hash(key, 64, 32, msg, SimpleSha256Hasher::new)
387            .expect("hmac_with_streaming_hash failed");
388
389        assert_eq!(result, expected);
390    }
391
392    // Key longer than block_size: key = 131 bytes of 0xaa (> 64)
393    // RFC 4231 TC5 uses such a key.
394    // Expected (SHA-256): 60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54
395    #[test]
396    fn test_hmac_sha256_rfc4231_tc5_long_key() {
397        let key = vec![0xaau8; 131];
398        let msg = b"Test Using Larger Than Block-Size Key - Hash Key First";
399        let expected =
400            hex_decode("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54");
401
402        let result = hmac_with_streaming_hash(&key, 64, 32, msg, SimpleSha256Hasher::new)
403            .expect("hmac with long key failed");
404
405        assert_eq!(result, expected);
406    }
407
408    // Verify: correct tag passes, incorrect tag fails.
409    #[test]
410    fn test_hmac_verify_correct_and_incorrect() {
411        let key = b"test-key";
412        let msg = b"test-message";
413        let hmac = StreamingHashHmac::new(key, 64, 32, SimpleSha256Hasher::new)
414            .expect("StreamingHashHmac::new failed");
415
416        let mut tag = vec![0u8; 32];
417        hmac.mac_oneshot(msg, &mut tag).expect("mac_oneshot");
418
419        assert!(hmac.verify(msg, &tag).is_ok(), "correct tag should verify");
420
421        // Flip one bit.
422        let mut bad_tag = tag.clone();
423        bad_tag[0] ^= 0x01;
424        assert!(
425            hmac.verify(msg, &bad_tag).is_err(),
426            "flipped tag should fail"
427        );
428    }
429
430    // Streaming session should produce the same tag as one-shot.
431    #[test]
432    fn test_streaming_session_matches_oneshot() {
433        let key = b"streaming-test-key";
434        let msg = b"the quick brown fox jumps over the lazy dog";
435
436        let hmac = StreamingHashHmac::new(key, 64, 32, SimpleSha256Hasher::new)
437            .expect("StreamingHashHmac::new");
438
439        // One-shot.
440        let mut tag_oneshot = vec![0u8; 32];
441        hmac.mac_oneshot(msg, &mut tag_oneshot)
442            .expect("mac_oneshot");
443
444        // Streaming (clone factory pattern — F must be Clone for session).
445        let hmac2 = StreamingHashHmac::new(key, 64, 32, SimpleSha256Hasher::new).expect("new2");
446        let mut session = hmac2.streaming_session();
447        for chunk in msg.chunks(7) {
448            session.update(chunk);
449        }
450        let mut tag_streaming = vec![0u8; 32];
451        session.finalize(&mut tag_streaming).expect("finalize");
452
453        assert_eq!(tag_oneshot, tag_streaming);
454    }
455
456    // Different keys → different MACs.
457    #[test]
458    fn test_different_keys_produce_different_macs() {
459        let msg = b"same message";
460        let r1 =
461            hmac_with_streaming_hash(b"key-alpha", 64, 32, msg, SimpleSha256Hasher::new).unwrap();
462        let r2 =
463            hmac_with_streaming_hash(b"key-beta", 64, 32, msg, SimpleSha256Hasher::new).unwrap();
464        assert_ne!(r1, r2);
465    }
466
467    // Empty message is accepted.
468    #[test]
469    fn test_empty_message_accepted() {
470        let result = hmac_with_streaming_hash(b"key", 64, 32, b"", SimpleSha256Hasher::new);
471        assert!(result.is_ok());
472        assert_eq!(result.unwrap().len(), 32);
473    }
474
475    // Buffer too small returns an error.
476    #[test]
477    fn test_buffer_too_small() {
478        let hmac = StreamingHashHmac::new(b"key", 64, 32, SimpleSha256Hasher::new)
479            .expect("StreamingHashHmac::new");
480        let mut out = vec![0u8; 16]; // too small
481        assert!(
482            hmac.mac_oneshot(b"msg", &mut out).is_err(),
483            "should fail with buffer too small"
484        );
485    }
486
487    fn hex_decode(s: &str) -> Vec<u8> {
488        (0..s.len())
489            .step_by(2)
490            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex decode"))
491            .collect()
492    }
493}