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}