dcrypt_algorithms/mac/hmac/
mod.rs

1//! HMAC (Hash-based Message Authentication Code) – constant-time & allocation-free
2//!
3//! • RFC 2104 / FIPS 198-1 compliant  
4//! • Secret-dependent work happens on stack-fixed buffers (≤ 144 bytes)  
5//! • Error paths burn the same CPU cycles as success paths
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/// Constant-time HMAC implementation.
16#[derive(Clone, Zeroize, ZeroizeOnDrop)]
17pub struct Hmac<H: HashFunction + Clone> {
18    #[zeroize(skip)] // hash state contains no secrets
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,
29    H::Output: AsRef<[u8]> + Clone,
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        hk.update(key)?;
48        let hashed = hk.finalize()?; // ≤ bs bytes
49
50        // Select either `key` or `hashed` per byte with a mask.
51        let mut k_prime = [0u8; MAX_BLOCK];
52        let long = (key.len() > bs) as u8; // 1 if key > bs
53        let mask = long.wrapping_neg(); // 0xFF when long else 0x00
54        #[allow(clippy::needless_range_loop)] // We need the index for multiple arrays
55        for i in 0..bs {
56            let k = *key.get(i).unwrap_or(&0);
57            let hk = hashed.as_ref().get(i).copied().unwrap_or(0);
58            k_prime[i] = (hk & mask) | (k & !mask);
59        }
60
61        /* --- Build inner / outer paddings --- */
62        let mut ipad_bytes = [0u8; MAX_BLOCK];
63        let mut opad_bytes = [0u8; MAX_BLOCK];
64        #[allow(clippy::needless_range_loop)] // We need to index multiple arrays
65        for i in 0..bs {
66            ipad_bytes[i] = k_prime[i] ^ Self::IPAD_BYTE;
67            opad_bytes[i] = k_prime[i] ^ Self::OPAD_BYTE;
68        }
69
70        // Zero K′ early
71        for b in k_prime.iter_mut().take(bs) {
72            *b = 0;
73        }
74
75        /* --- Initialise inner hash --- */
76        let mut hash = H::new();
77        hash.update(&ipad_bytes[..bs])?;
78
79        Ok(Self {
80            hash,
81            ipad: SecretBuffer::new(ipad_bytes),
82            opad: SecretBuffer::new(opad_bytes),
83            block_size: bs,
84            is_finalized: false,
85        })
86    }
87
88    /* ------------------------------------------------------------------ */
89    /*                            Streaming API                           */
90    /* ------------------------------------------------------------------ */
91
92    /// Feed additional `data` into the MAC.
93    pub fn update(&mut self, data: &[u8]) -> Result<()> {
94        if self.is_finalized {
95            /* ----------------------------------------------------------
96             * Equal-cost dummy path: hash the input into a fresh hasher
97             * and discard the result so error & success match timings.
98             * -------------------------------------------------------- */
99            let mut dummy = H::new();
100            dummy.update(data)?;
101            let _ = dummy.finalize();
102            return Err(Error::param(
103                "hmac_state",
104                "Cannot update after finalization",
105            ));
106        }
107
108        self.hash.update(data).map(|_| ())
109    }
110
111    /// Finalise and return the tag.
112    pub fn finalize(&mut self) -> Result<Vec<u8>> {
113        if self.is_finalized {
114            // Equal-cost burn: mimic normal finalisation cost.
115            let inner_dummy = [0u8; 64]; // max SHA-512 output
116            let mut outer = H::new();
117            outer.update(&self.opad.as_ref()[..self.block_size])?;
118            outer.update(&inner_dummy[..H::output_size()])?;
119            let _ = outer.finalize();
120            return Err(Error::param("hmac_state", "HMAC already finalized"));
121        }
122
123        self.is_finalized = true;
124
125        let inner_hash = self.hash.finalize()?;
126
127        let mut outer = H::new();
128        outer.update(&self.opad.as_ref()[..self.block_size])?;
129        outer.update(inner_hash.as_ref())?;
130
131        outer.finalize().map(|out| out.as_ref().to_vec())
132    }
133
134    /* ------------------------------------------------------------------ */
135    /*                        Convenience wrappers                         */
136    /* ------------------------------------------------------------------ */
137
138    /// One-shot MAC helper.
139    pub fn mac(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
140        let mut h = Self::new(key)?;
141        h.update(data)?;
142        h.finalize()
143    }
144
145    /// Constant-time verification of `tag` against `key` / `data`.
146    pub fn verify(key: &[u8], data: &[u8], tag: &[u8]) -> Result<bool> {
147        let expected = Self::mac(key, data)?;
148
149        // Always iterate over the fixed, public digest length to avoid
150        // timing variation when the caller supplies a shorter tag.
151        let mut diff = 0u8;
152        #[allow(clippy::needless_range_loop)] // Accessing both arrays with same index
153        for i in 0..H::output_size() {
154            let a = expected.get(i).copied().unwrap_or(0);
155            let b = tag.get(i).copied().unwrap_or(0);
156            diff |= a ^ b;
157        }
158        // Fold any length mismatch into the diff in a single operation.
159        diff |= (tag.len() ^ H::output_size()) as u8;
160
161        Ok(diff.ct_eq(&0u8).unwrap_u8() == 1)
162    }
163}
164
165impl<H> SecureZeroingType for Hmac<H>
166where
167    H: HashFunction + Default + Clone,
168{
169    fn zeroed() -> Self {
170        Self {
171            hash: H::default(),
172            ipad: SecretBuffer::zeroed(),
173            opad: SecretBuffer::zeroed(),
174            block_size: 0,
175            is_finalized: false,
176        }
177    }
178
179    fn secure_clone(&self) -> Self {
180        Self {
181            hash: self.hash.clone(),
182            ipad: self.ipad.secure_clone(),
183            opad: self.opad.secure_clone(),
184            block_size: self.block_size,
185            is_finalized: self.is_finalized,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests;