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