Skip to main content

dcrypt_algorithms/mac/hmac/
mod.rs

1//! HMAC (Hash-based Message Authentication Code)
2//!
3//! • RFC 2104 / FIPS 198-1 compliant
4//! • Key-derived padding and hash state are zeroized on drop
5//! • Tag bytes are compared in constant time after an exact-length check
6
7use crate::error::{Error, Result};
8use crate::hash::HashFunction;
9use dcrypt_common::security::{SecretBuffer, SecureZeroingType};
10use subtle::ConstantTimeEq;
11use zeroize::{Zeroize, ZeroizeOnDrop};
12
13const MAX_BLOCK: usize = 144; // SHA3-224 block size (largest among SHA-2 and SHA-3)
14
15/// HMAC implementation with constant-time tag-byte comparison.
16#[derive(Clone, Zeroize, ZeroizeOnDrop)]
17pub struct Hmac<H: HashFunction + Clone + Zeroize> {
18    // The inner hash has absorbed the key-derived ipad and is secret state.
19    hash: H,
20    ipad: SecretBuffer<MAX_BLOCK>,
21    opad: SecretBuffer<MAX_BLOCK>,
22    block_size: usize,
23    is_finalized: bool,
24}
25
26impl<H> Hmac<H>
27where
28    H: HashFunction + Clone + Zeroize,
29    H::Output: AsRef<[u8]> + Clone + Zeroize,
30{
31    const IPAD_BYTE: u8 = 0x36;
32    const OPAD_BYTE: u8 = 0x5c;
33
34    /* ------------------------------------------------------------------ */
35    /*                         Construction helpers                       */
36    /* ------------------------------------------------------------------ */
37
38    /// Create a new HMAC instance from `key`.
39    pub fn new(key: &[u8]) -> Result<Self> {
40        let bs = H::block_size();
41        debug_assert!(bs <= MAX_BLOCK);
42
43        /* --- Derive K′ in constant-time --- */
44        // Hash the key unconditionally so the running time
45        // depends only on the public key length.
46        let mut hk = H::new();
47        if let Err(error) = hk.update(key) {
48            hk.zeroize();
49            return Err(error);
50        }
51        let mut hashed = match hk.finalize() {
52            Ok(output) => output,
53            Err(error) => {
54                hk.zeroize();
55                return Err(error);
56            }
57        }; // ≤ bs bytes
58        hk.zeroize();
59
60        // Select either `key` or `hashed` per byte with a mask.
61        let mut k_prime = SecretBuffer::<MAX_BLOCK>::zeroed();
62        let long = (key.len() > bs) as u8; // 1 if key > bs
63        let mask = long.wrapping_neg(); // 0xFF when long else 0x00
64        #[allow(clippy::needless_range_loop)] // We need the index for multiple arrays
65        for i in 0..bs {
66            let k = *key.get(i).unwrap_or(&0);
67            let hk = hashed.as_ref().get(i).copied().unwrap_or(0);
68            k_prime.as_mut()[i] = (hk & mask) | (k & !mask);
69        }
70        hashed.zeroize();
71
72        /* --- Build inner / outer paddings --- */
73        let mut ipad = SecretBuffer::<MAX_BLOCK>::zeroed();
74        let mut opad = SecretBuffer::<MAX_BLOCK>::zeroed();
75        #[allow(clippy::needless_range_loop)] // We need to index multiple arrays
76        for i in 0..bs {
77            ipad.as_mut()[i] = k_prime.as_ref()[i] ^ Self::IPAD_BYTE;
78            opad.as_mut()[i] = k_prime.as_ref()[i] ^ Self::OPAD_BYTE;
79        }
80
81        // Zero K′ before any fallible hash operation.
82        k_prime.zeroize();
83
84        /* --- Initialise inner hash --- */
85        let mut hash = H::new();
86        if let Err(error) = hash.update(&ipad.as_ref()[..bs]) {
87            hash.zeroize();
88            return Err(error);
89        }
90
91        Ok(Self {
92            hash,
93            ipad,
94            opad,
95            block_size: bs,
96            is_finalized: false,
97        })
98    }
99
100    /* ------------------------------------------------------------------ */
101    /*                            Streaming API                           */
102    /* ------------------------------------------------------------------ */
103
104    /// Feed additional `data` into the MAC.
105    pub fn update(&mut self, data: &[u8]) -> Result<()> {
106        if self.is_finalized {
107            return Err(Error::param(
108                "hmac_state",
109                "Cannot update after finalization",
110            ));
111        }
112
113        self.hash.update(data).map(|_| ())
114    }
115
116    /// Finalise and return the tag.
117    pub fn finalize(&mut self) -> Result<Vec<u8>> {
118        if self.is_finalized {
119            return Err(Error::param("hmac_state", "HMAC already finalized"));
120        }
121
122        self.is_finalized = true;
123
124        let mut inner_hash = match self.hash.finalize() {
125            Ok(output) => {
126                self.hash.zeroize();
127                output
128            }
129            Err(error) => {
130                self.hash.zeroize();
131                return Err(error);
132            }
133        };
134
135        let mut outer = H::new();
136        if let Err(error) = outer.update(&self.opad.as_ref()[..self.block_size]) {
137            inner_hash.zeroize();
138            outer.zeroize();
139            return Err(error);
140        }
141        if let Err(error) = outer.update(inner_hash.as_ref()) {
142            inner_hash.zeroize();
143            outer.zeroize();
144            return Err(error);
145        }
146        inner_hash.zeroize();
147
148        let mut output = match outer.finalize() {
149            Ok(output) => output,
150            Err(error) => {
151                outer.zeroize();
152                return Err(error);
153            }
154        };
155        outer.zeroize();
156        let tag = output.as_ref().to_vec();
157        output.zeroize();
158        Ok(tag)
159    }
160
161    /* ------------------------------------------------------------------ */
162    /*                        Convenience wrappers                         */
163    /* ------------------------------------------------------------------ */
164
165    /// One-shot MAC helper.
166    pub fn mac(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
167        let mut h = Self::new(key)?;
168        h.update(data)?;
169        h.finalize()
170    }
171
172    /// Fixed-width verification of `tag` against `key` / `data`.
173    ///
174    /// Tag bytes are accumulated without an early exit. Public lengths, hash
175    /// errors, allocation, and the returned boolean still have ordinary control
176    /// flow, so this is not a whole-operation constant-time guarantee.
177    pub fn verify(key: &[u8], data: &[u8], tag: &[u8]) -> Result<bool> {
178        let expected = Self::mac(key, data)?;
179
180        // Always iterate over the fixed, public digest length to avoid
181        // timing variation when the caller supplies a shorter tag.
182        let mut diff = 0u8;
183        #[allow(clippy::needless_range_loop)] // Accessing both arrays with same index
184        for i in 0..H::output_size() {
185            let a = expected.get(i).copied().unwrap_or(0);
186            let b = tag.get(i).copied().unwrap_or(0);
187            diff |= a ^ b;
188        }
189        // Lengths are public, but compare them without narrowing `usize`: a
190        // difference of 256 bytes must never disappear in an `as u8` cast.
191        diff |= tag.len().ct_eq(&H::output_size()).unwrap_u8() ^ 1;
192
193        Ok(diff.ct_eq(&0u8).unwrap_u8() == 1)
194    }
195}
196
197impl<H> SecureZeroingType for Hmac<H>
198where
199    H: HashFunction + Default + Clone + Zeroize,
200{
201    fn zeroed() -> Self {
202        Self {
203            hash: H::default(),
204            ipad: SecretBuffer::zeroed(),
205            opad: SecretBuffer::zeroed(),
206            block_size: 0,
207            is_finalized: false,
208        }
209    }
210
211    fn secure_clone(&self) -> Self {
212        Self {
213            hash: self.hash.clone(),
214            ipad: self.ipad.secure_clone(),
215            opad: self.opad.secure_clone(),
216            block_size: self.block_size,
217            is_finalized: self.is_finalized,
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests;