Skip to main content

crypto/
hmac.rs

1//! HMAC (Hash-based Message Authentication Code)
2
3#[cfg(feature = "zeroize")]
4use zeroize::{Zeroize, ZeroizeOnDrop};
5
6use crate::{Hash, Hasher, MAX_HASH_BLOCK_SIZE};
7
8/// HMAC (Hash-based Message Authentication Code) implementation.
9///
10/// Uses a generic hash function implementing the [`Hasher`] trait (e.g.
11/// [`Sha256`](crate::sha2::Sha256) or [`Sha512`](crate::sha2::Sha512)).
12///
13/// # One-shot API
14///
15/// ```ignore
16/// use crypto::hmac::Hmac;
17/// use crypto::sha2::Sha256;
18///
19/// let tag = Hmac::<Sha256>::mac(b"key", b"message");
20/// ```
21///
22/// # Incremental API
23///
24/// ```ignore
25/// use crypto::hmac::Hmac;
26/// use crypto::sha2::Sha256;
27///
28/// let mut mac = Hmac::<Sha256>::new(b"key");
29/// mac.update(b"hello ");
30/// mac.update(b"world");
31/// let tag = mac.finalize();
32/// ```
33#[derive(Clone)]
34#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
35pub struct Hmac<H: Hasher> {
36    hash: H,
37    opad: [u8; MAX_HASH_BLOCK_SIZE],
38}
39
40impl<H: Hasher> Hmac<H> {
41    /// One-shot HMAC: computes `HMAC(key, data)` in a single call.
42    ///
43    /// This is a convenience wrapper around [`new`](Self::new) +
44    /// [`update`](Self::update) + [`finalize`](Self::finalize).
45    #[inline]
46    pub fn mac(key: &[u8], data: &[u8]) -> Hash {
47        let mut mac = Self::new(key);
48        mac.update(data);
49        return mac.finalize();
50    }
51
52    pub fn new(key: &[u8]) -> Self {
53        let mut key_block = [0u8; MAX_HASH_BLOCK_SIZE];
54
55        // normalize key to block size
56        if key.len() > H::BLOCK_SIZE {
57            let mut h = H::new();
58            h.update(key);
59            let hashed = h.sum();
60            let hashed_bytes = hashed.as_ref();
61            key_block[..hashed_bytes.len()].copy_from_slice(hashed_bytes);
62        } else {
63            key_block[..key.len()].copy_from_slice(key);
64        }
65
66        // inner pad = key ^ 0x36
67        let mut inner_key = [0u8; MAX_HASH_BLOCK_SIZE];
68        for i in 0..H::BLOCK_SIZE {
69            inner_key[i] = key_block[i] ^ 0x36;
70        }
71
72        // outer pad = key ^ 0x5c
73        let mut opad = [0u8; MAX_HASH_BLOCK_SIZE];
74        for i in 0..H::BLOCK_SIZE {
75            opad[i] = key_block[i] ^ 0x5c;
76        }
77
78        // initialize inner hash: create a fresh instance and feed inner pad
79        let mut hash = H::new();
80        hash.update(&inner_key[..H::BLOCK_SIZE]);
81
82        Hmac {
83            hash,
84            opad,
85        }
86    }
87
88    /// Feed message data to HMAC (can be called multiple times)
89    pub fn update(&mut self, data: &[u8]) {
90        self.hash.update(data);
91    }
92
93    /// Finalize and return HMAC tag. This consumes the Hmac state.
94    pub fn finalize(mut self) -> Hash {
95        // `self.hash` cannot be moved out directly because `Hmac` implements
96        // `Drop` (to wipe the key material). Take the state and leave a fresh
97        // hasher in its place; the remainder is zeroized when `self` drops.
98        let inner_hash = core::mem::replace(&mut self.hash, H::new());
99        let inner_sum = inner_hash.sum();
100
101        // compute outer hash using a fresh instance
102        let mut outer = H::new();
103        outer.update(&self.opad[..H::BLOCK_SIZE]);
104        outer.update(inner_sum.as_ref());
105        outer.sum()
106    }
107}
108
109#[cfg(test)]
110mod hmac_tests {
111    use crate::{
112        hmac::Hmac,
113        sha2::{Sha256, Sha384, Sha512},
114        sha3::{Sha3_256, Sha3_512},
115    };
116
117    #[cfg(feature = "zeroize")]
118    #[test]
119    fn hmac_zeroize_clears_opad() {
120        use zeroize::Zeroize;
121
122        let mut mac = Hmac::<Sha256>::new(b"secret key");
123        assert_ne!(mac.opad, [0u8; crate::MAX_HASH_BLOCK_SIZE]);
124        mac.zeroize();
125        assert_eq!(mac.opad, [0u8; crate::MAX_HASH_BLOCK_SIZE]);
126    }
127
128    #[derive(Clone, Copy)]
129    enum TestInput {
130        Bytes(&'static [u8]),
131        Repeated { byte: u8, len: usize },
132        RangeInclusive { start: u8, end: u8 },
133    }
134
135    #[derive(Clone, Copy)]
136    struct HmacTestVector {
137        source: &'static str,
138        key: TestInput,
139        data: TestInput,
140        expected_sha256: &'static str,
141        expected_sha512: &'static str,
142    }
143
144    const HMAC_TEST_VECTORS: [HmacTestVector; 6] = [
145        // RFC 4231 TC1
146        HmacTestVector {
147            source: "RFC 4231 TC1",
148            key: TestInput::Repeated {
149                byte: 0x0b,
150                len: 20,
151            },
152            data: TestInput::Bytes(b"Hi There"),
153            expected_sha256: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
154            expected_sha512: "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854",
155        },
156        // RFC 4231 TC2
157        HmacTestVector {
158            source: "RFC 4231 TC2",
159            key: TestInput::Bytes(b"Jefe"),
160            data: TestInput::Bytes(b"what do ya want for nothing?"),
161            expected_sha256: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
162            expected_sha512: "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737",
163        },
164        // RFC 4231 TC3
165        HmacTestVector {
166            source: "RFC 4231 TC3",
167            key: TestInput::Repeated {
168                byte: 0xaa,
169                len: 20,
170            },
171            data: TestInput::Repeated {
172                byte: 0xdd,
173                len: 50,
174            },
175            expected_sha256: "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe",
176            expected_sha512: "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb",
177        },
178        // RFC 4231 TC4
179        HmacTestVector {
180            source: "RFC 4231 TC4",
181            key: TestInput::RangeInclusive {
182                start: 0x01,
183                end: 0x19,
184            },
185            data: TestInput::Repeated {
186                byte: 0xcd,
187                len: 50,
188            },
189            expected_sha256: "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b",
190            expected_sha512: "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd",
191        },
192        // RFC 4231 TC6 (TC5 is truncated-output only)
193        HmacTestVector {
194            source: "RFC 4231 TC6",
195            key: TestInput::Repeated {
196                byte: 0xaa,
197                len: 131,
198            },
199            data: TestInput::Bytes(b"Test Using Larger Than Block-Size Key - Hash Key First"),
200            expected_sha256: "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54",
201            expected_sha512: "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598",
202        },
203        // RFC 4231 TC7
204        HmacTestVector {
205            source: "RFC 4231 TC7",
206            key: TestInput::Repeated {
207                byte: 0xaa,
208                len: 131,
209            },
210            data: TestInput::Bytes(
211                b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.",
212            ),
213            expected_sha256: "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2",
214            expected_sha512: "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58",
215        },
216    ];
217
218    fn materialize(input: TestInput) -> Vec<u8> {
219        match input {
220            TestInput::Bytes(bytes) => bytes.to_vec(),
221            TestInput::Repeated {
222                byte,
223                len,
224            } => vec![byte; len],
225            TestInput::RangeInclusive {
226                start,
227                end,
228            } => (start..=end).collect(),
229        }
230    }
231
232    fn hmac256(key: &[u8], data: &[u8]) -> String {
233        let mut mac = Hmac::<Sha256>::new(key);
234        mac.update(data);
235        hex::encode(mac.finalize().as_ref())
236    }
237
238    fn hmac512(key: &[u8], data: &[u8]) -> String {
239        let mut mac = Hmac::<Sha512>::new(key);
240        mac.update(data);
241        hex::encode(mac.finalize().as_ref())
242    }
243
244    #[test]
245    fn hmac_vectors() {
246        for vector in HMAC_TEST_VECTORS {
247            let key = materialize(vector.key);
248            let data = materialize(vector.data);
249
250            let single256 = hmac256(&key, &data);
251            let single512 = hmac512(&key, &data);
252
253            assert_eq!(single256, vector.expected_sha256, "{}", vector.source);
254            assert_eq!(single512, vector.expected_sha512, "{}", vector.source);
255
256            let mut mac256 = Hmac::<Sha256>::new(&key);
257            for chunk in data.chunks(7) {
258                mac256.update(chunk);
259            }
260            let incremental256 = hex::encode(mac256.finalize().as_ref());
261            assert_eq!(incremental256, single256, "{} incremental sha256", vector.source);
262
263            let mut mac512 = Hmac::<Sha512>::new(&key);
264            for chunk in data.chunks(13) {
265                mac512.update(chunk);
266            }
267            let incremental512 = hex::encode(mac512.finalize().as_ref());
268            assert_eq!(incremental512, single512, "{} incremental sha512", vector.source);
269        }
270    }
271
272    // --- Wycheproof test vectors ---
273
274    #[test]
275    fn hmac_sha256_wycheproof() {
276        let data: serde_json::Value =
277            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hmac_sha256_test.json")).unwrap();
278        let mut valid_tested = 0u64;
279        let mut invalid_tested = 0u64;
280        for group in data["testGroups"].as_array().unwrap() {
281            let tag_size_bits = group["tagSize"].as_u64().unwrap();
282            let tag_size_bytes = (tag_size_bits / 8) as usize;
283            for test in group["tests"].as_array().unwrap() {
284                let key_hex = test["key"].as_str().unwrap();
285                let msg_hex = test["msg"].as_str().unwrap();
286                let expected_tag_hex = test["tag"].as_str().unwrap();
287                let result = test["result"].as_str().unwrap();
288
289                let key = hex::decode(key_hex).unwrap();
290                let msg = hex::decode(msg_hex).unwrap();
291
292                let computed = Hmac::<Sha256>::mac(&key, &msg);
293                let computed_tag = hex::encode(&computed.as_ref()[..tag_size_bytes]);
294
295                if result == "valid" {
296                    assert_eq!(
297                        computed_tag, expected_tag_hex,
298                        "wycheproof HMAC-SHA-256 tcId={} tagSize={}",
299                        test["tcId"], tag_size_bits
300                    );
301                    valid_tested += 1;
302                } else {
303                    assert_ne!(
304                        computed_tag, expected_tag_hex,
305                        "wycheproof HMAC-SHA-256 tcId={} ModifiedTag not detected",
306                        test["tcId"]
307                    );
308                    invalid_tested += 1;
309                }
310            }
311        }
312        assert!(valid_tested > 0, "no valid HMAC-SHA-256 wycheproof tests were run");
313        assert!(invalid_tested > 0, "no invalid HMAC-SHA-256 wycheproof tests were run");
314    }
315
316    #[test]
317    fn hmac_sha512_wycheproof() {
318        let data: serde_json::Value =
319            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hmac_sha512_test.json")).unwrap();
320        let mut valid_tested = 0u64;
321        let mut invalid_tested = 0u64;
322        for group in data["testGroups"].as_array().unwrap() {
323            let tag_size_bits = group["tagSize"].as_u64().unwrap();
324            let tag_size_bytes = (tag_size_bits / 8) as usize;
325            for test in group["tests"].as_array().unwrap() {
326                let key_hex = test["key"].as_str().unwrap();
327                let msg_hex = test["msg"].as_str().unwrap();
328                let expected_tag_hex = test["tag"].as_str().unwrap();
329                let result = test["result"].as_str().unwrap();
330
331                let key = hex::decode(key_hex).unwrap();
332                let msg = hex::decode(msg_hex).unwrap();
333
334                let computed = Hmac::<Sha512>::mac(&key, &msg);
335                let computed_tag = hex::encode(&computed.as_ref()[..tag_size_bytes]);
336
337                if result == "valid" {
338                    assert_eq!(
339                        computed_tag, expected_tag_hex,
340                        "wycheproof HMAC-SHA-512 tcId={} tagSize={}",
341                        test["tcId"], tag_size_bits
342                    );
343                    valid_tested += 1;
344                } else {
345                    assert_ne!(
346                        computed_tag, expected_tag_hex,
347                        "wycheproof HMAC-SHA-512 tcId={} ModifiedTag not detected",
348                        test["tcId"]
349                    );
350                    invalid_tested += 1;
351                }
352            }
353        }
354        assert!(valid_tested > 0, "no valid HMAC-SHA-512 wycheproof tests were run");
355        assert!(invalid_tested > 0, "no invalid HMAC-SHA-512 wycheproof tests were run");
356    }
357
358    #[test]
359    fn hmac_sha384_wycheproof() {
360        let data: serde_json::Value =
361            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hmac_sha384_test.json")).unwrap();
362        let mut valid_tested = 0u64;
363        let mut invalid_tested = 0u64;
364        for group in data["testGroups"].as_array().unwrap() {
365            let tag_size_bits = group["tagSize"].as_u64().unwrap();
366            let tag_size_bytes = (tag_size_bits / 8) as usize;
367            for test in group["tests"].as_array().unwrap() {
368                let key_hex = test["key"].as_str().unwrap();
369                let msg_hex = test["msg"].as_str().unwrap();
370                let expected_tag_hex = test["tag"].as_str().unwrap();
371                let result = test["result"].as_str().unwrap();
372
373                let key = hex::decode(key_hex).unwrap();
374                let msg = hex::decode(msg_hex).unwrap();
375
376                let computed = Hmac::<Sha384>::mac(&key, &msg);
377                let computed_tag = hex::encode(&computed.as_ref()[..tag_size_bytes]);
378
379                if result == "valid" {
380                    assert_eq!(
381                        computed_tag, expected_tag_hex,
382                        "wycheproof HMAC-SHA-384 tcId={} tagSize={}",
383                        test["tcId"], tag_size_bits
384                    );
385                    valid_tested += 1;
386                } else {
387                    assert_ne!(
388                        computed_tag, expected_tag_hex,
389                        "wycheproof HMAC-SHA-384 tcId={} ModifiedTag not detected",
390                        test["tcId"]
391                    );
392                    invalid_tested += 1;
393                }
394            }
395        }
396        assert!(valid_tested > 0, "no valid HMAC-SHA-384 wycheproof tests were run");
397        assert!(invalid_tested > 0, "no invalid HMAC-SHA-384 wycheproof tests were run");
398    }
399
400    #[test]
401    fn hmac_sha3_256_wycheproof() {
402        let data: serde_json::Value =
403            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hmac_sha3_256_test.json"))
404                .unwrap();
405        let mut valid_tested = 0u64;
406        let mut invalid_tested = 0u64;
407        for group in data["testGroups"].as_array().unwrap() {
408            let tag_size_bits = group["tagSize"].as_u64().unwrap();
409            let tag_size_bytes = (tag_size_bits / 8) as usize;
410            for test in group["tests"].as_array().unwrap() {
411                let key_hex = test["key"].as_str().unwrap();
412                let msg_hex = test["msg"].as_str().unwrap();
413                let expected_tag_hex = test["tag"].as_str().unwrap();
414                let result = test["result"].as_str().unwrap();
415
416                let key = hex::decode(key_hex).unwrap();
417                let msg = hex::decode(msg_hex).unwrap();
418
419                let computed = Hmac::<Sha3_256>::mac(&key, &msg);
420                let computed_tag = hex::encode(&computed.as_ref()[..tag_size_bytes]);
421
422                if result == "valid" {
423                    assert_eq!(
424                        computed_tag, expected_tag_hex,
425                        "wycheproof HMAC-SHA3-256 tcId={} tagSize={}",
426                        test["tcId"], tag_size_bits
427                    );
428                    valid_tested += 1;
429                } else {
430                    assert_ne!(
431                        computed_tag, expected_tag_hex,
432                        "wycheproof HMAC-SHA3-256 tcId={} ModifiedTag not detected",
433                        test["tcId"]
434                    );
435                    invalid_tested += 1;
436                }
437            }
438        }
439        assert!(valid_tested > 0, "no valid HMAC-SHA3-256 wycheproof tests were run");
440        assert!(invalid_tested > 0, "no invalid HMAC-SHA3-256 wycheproof tests were run");
441    }
442
443    #[test]
444    fn hmac_sha3_512_wycheproof() {
445        let data: serde_json::Value =
446            serde_json::from_str(include_str!("../testdata/wycheproof/testvectors_v1/hmac_sha3_512_test.json"))
447                .unwrap();
448        let mut valid_tested = 0u64;
449        let mut invalid_tested = 0u64;
450        for group in data["testGroups"].as_array().unwrap() {
451            let tag_size_bits = group["tagSize"].as_u64().unwrap();
452            let tag_size_bytes = (tag_size_bits / 8) as usize;
453            for test in group["tests"].as_array().unwrap() {
454                let key_hex = test["key"].as_str().unwrap();
455                let msg_hex = test["msg"].as_str().unwrap();
456                let expected_tag_hex = test["tag"].as_str().unwrap();
457                let result = test["result"].as_str().unwrap();
458
459                let key = hex::decode(key_hex).unwrap();
460                let msg = hex::decode(msg_hex).unwrap();
461
462                let computed = Hmac::<Sha3_512>::mac(&key, &msg);
463                let computed_tag = hex::encode(&computed.as_ref()[..tag_size_bytes]);
464
465                if result == "valid" {
466                    assert_eq!(
467                        computed_tag, expected_tag_hex,
468                        "wycheproof HMAC-SHA3-512 tcId={} tagSize={}",
469                        test["tcId"], tag_size_bits
470                    );
471                    valid_tested += 1;
472                } else {
473                    assert_ne!(
474                        computed_tag, expected_tag_hex,
475                        "wycheproof HMAC-SHA3-512 tcId={} ModifiedTag not detected",
476                        test["tcId"]
477                    );
478                    invalid_tested += 1;
479                }
480            }
481        }
482        assert!(valid_tested > 0, "no valid HMAC-SHA3-512 wycheproof tests were run");
483        assert!(invalid_tested > 0, "no invalid HMAC-SHA3-512 wycheproof tests were run");
484    }
485}