Skip to main content

dcrypt_algorithms/kdf/hkdf/
mod.rs

1//! HMAC-based Key Derivation Function (HKDF)
2//!
3//! This module implements HKDF as defined in RFC 5869.
4//! HKDF is designed to take input keying material (IKM) that is not necessarily
5//! uniform and produce output keying material (OKM) suitable for use in cryptographic
6//! contexts.
7
8#[cfg(feature = "alloc")]
9use crate::alloc_prelude::*;
10
11use crate::error::{validate, Error, Result};
12use crate::hash::HashFunction;
13use crate::kdf::{KdfAlgorithm, KdfOperation, KeyDerivationFunction, ParamProvider, SecurityLevel};
14use crate::mac::hmac::Hmac;
15use crate::types::salt::HkdfCompatible;
16use crate::types::Salt;
17
18// Import security types from dcrypt-core
19use dcrypt_common::security::SecureZeroingType;
20
21use dcrypt_internal::random::{CryptoRng, RngCore};
22use dcrypt_internal::zeroing::{
23    boxed_bytes_zeroed, Zeroize, ZeroizeOnDrop, Zeroizing, ZeroizingBytes,
24};
25
26#[cfg(not(feature = "std"))]
27use alloc::vec::Vec;
28use core::marker::PhantomData;
29
30/// Type-level constants for HKDF algorithm
31pub enum HkdfAlgorithm<H: HashFunction> {
32    /// Phantom field for the hash function
33    _Hash(PhantomData<H>),
34}
35
36impl<H: HashFunction> KdfAlgorithm for HkdfAlgorithm<H> {
37    const MIN_SALT_SIZE: usize = 16;
38    const DEFAULT_OUTPUT_SIZE: usize = 32;
39    const ALGORITHM_ID: &'static str = "HKDF";
40
41    fn name() -> String {
42        format!("{}-{}", Self::ALGORITHM_ID, H::name())
43    }
44
45    fn security_level() -> SecurityLevel {
46        match H::output_size() * 8 {
47            bits if bits >= 512 => SecurityLevel::L256,
48            bits if bits >= 384 => SecurityLevel::L192,
49            bits if bits >= 256 => SecurityLevel::L128,
50            bits => SecurityLevel::Custom(bits as u32 / 2),
51        }
52    }
53}
54
55/// Parameters for HKDF
56#[derive(Clone, Debug)]
57pub struct HkdfParams<const S: usize = 16> {
58    /// Optional default salt (can be overridden in derive_key)
59    pub salt: Option<Salt<S>>,
60    /// Optional default info (context, can be overridden in derive_key)
61    pub info: Option<Vec<u8>>,
62}
63
64impl<const S: usize> Zeroize for HkdfParams<S> {
65    fn zeroize(&mut self) {
66        self.salt.zeroize();
67        self.info.zeroize();
68    }
69}
70
71impl<const S: usize> Default for HkdfParams<S> {
72    fn default() -> Self {
73        Self {
74            salt: None,
75            info: None,
76        }
77    }
78}
79
80/// HKDF implementation using any hash function
81#[derive(Clone)]
82pub struct Hkdf<H: HashFunction, const S: usize = 16> {
83    _hash_type: PhantomData<H>,
84    params: HkdfParams<S>,
85}
86
87impl<H: HashFunction, const S: usize> Zeroize for Hkdf<H, S> {
88    fn zeroize(&mut self) {
89        self.params.zeroize();
90    }
91}
92
93impl<H: HashFunction, const S: usize> Drop for Hkdf<H, S> {
94    fn drop(&mut self) {
95        self.zeroize();
96    }
97}
98
99impl<H: HashFunction, const S: usize> ZeroizeOnDrop for Hkdf<H, S> {}
100
101/// Operation for HKDF operations
102pub struct HkdfOperation<'a, H: HashFunction, const S: usize = 16> {
103    #[allow(dead_code)] // Kept for potential future use and API consistency
104    kdf: &'a Hkdf<H, S>,
105    ikm: Option<&'a [u8]>,
106    salt: Option<&'a [u8]>,
107    info: Option<&'a [u8]>,
108    length: usize,
109}
110
111impl<'a, H: HashFunction + Clone, const S: usize> KdfOperation<'a, HkdfAlgorithm<H>>
112    for HkdfOperation<'a, H, S>
113where
114    Salt<S>: HkdfCompatible,
115{
116    fn with_ikm(mut self, ikm: &'a [u8]) -> Self {
117        self.ikm = Some(ikm);
118        self
119    }
120
121    fn with_salt(mut self, salt: &'a [u8]) -> Self {
122        self.salt = Some(salt);
123        self
124    }
125
126    fn with_info(mut self, info: &'a [u8]) -> Self {
127        self.info = Some(info);
128        self
129    }
130
131    fn with_output_length(mut self, length: usize) -> Self {
132        self.length = length;
133        self
134    }
135
136    fn derive(self) -> Result<ZeroizingBytes> {
137        let ikm = self
138            .ikm
139            .ok_or_else(|| Error::param("ikm", "Input keying material is required"))?;
140
141        let salt_bytes = self.salt;
142        let info_bytes = self.info;
143
144        Hkdf::<H, S>::derive(salt_bytes, ikm, info_bytes, self.length)
145    }
146
147    fn derive_array<const N: usize>(self) -> Result<Zeroizing<[u8; N]>> {
148        // Ensure the requested size matches
149        validate::length("HKDF output", self.length, N)?;
150
151        let vec = self.derive()?;
152
153        // Convert to fixed-size array
154        let mut array = Zeroizing::new([0u8; N]);
155        array.copy_from_slice(&vec);
156        Ok(array)
157    }
158}
159
160impl<H: HashFunction + Clone, const S: usize> Hkdf<H, S>
161where
162    Salt<S>: HkdfCompatible,
163{
164    /// HKDF-Extract
165    pub fn extract(salt: Option<&[u8]>, ikm: &[u8]) -> Result<ZeroizingBytes> {
166        Hmac::<H>::mac(salt.unwrap_or(&[]), ikm)
167    }
168
169    /// HKDF-Expand
170    pub fn expand(prk: &[u8], info: Option<&[u8]>, length: usize) -> Result<ZeroizingBytes> {
171        let hash_len = H::output_size();
172        let max_len = 255 * hash_len;
173
174        // Specified max-length check (length is public)
175        validate::max_length("HKDF-Expand output", length, max_len)?;
176
177        // PRK length check (must be at least one hash block)
178        validate::min_length("PRK for HKDF-Expand", prk.len(), hash_len)?;
179
180        // Number of blocks needed - FIXED: Using div_ceil
181        let n = length.div_ceil(hash_len);
182
183        // Pre-allocate OKM buffer and temporary block buffer
184        let mut okm = Zeroizing::new(boxed_bytes_zeroed(length));
185        let mut t_buf = Zeroizing::new(boxed_bytes_zeroed(hash_len));
186        let info_bytes = info.unwrap_or(&[]);
187
188        for i in 1..=n {
189            let mut hmac = Hmac::<H>::new(prk)?;
190            if i > 1 {
191                // feed previous block for iterations > 1
192                hmac.update(&t_buf)?;
193            }
194            hmac.update(info_bytes)?;
195            hmac.update(&[i as u8])?;
196            let block = hmac.finalize()?;
197            t_buf.copy_from_slice(&block);
198            let start = (i - 1) * hash_len;
199            let end = core::cmp::min(start + hash_len, length);
200            okm[start..end].copy_from_slice(&t_buf[..end - start]);
201        }
202
203        Ok(okm)
204    }
205
206    /// Full HKDF (Extract + Expand) with warm-up
207    pub fn derive(
208        salt: Option<&[u8]>,
209        ikm: &[u8],
210        info: Option<&[u8]>,
211        length: usize,
212    ) -> Result<ZeroizingBytes> {
213        let _ = Hmac::<H>::new(&[])?; // warm-up
214
215        // Extract phase - produces PRK
216        let prk = Self::extract(salt, ikm)?;
217
218        // Expand phase - uses PRK to generate OKM
219        Self::expand(&prk, info, length)
220    }
221}
222
223impl<H: HashFunction, const S: usize> ParamProvider for Hkdf<H, S>
224where
225    Salt<S>: HkdfCompatible,
226{
227    type Params = HkdfParams<S>;
228    fn with_params(params: Self::Params) -> Self {
229        Hkdf {
230            _hash_type: PhantomData,
231            params,
232        }
233    }
234    fn params(&self) -> &Self::Params {
235        &self.params
236    }
237    fn set_params(&mut self, params: Self::Params) {
238        self.params = params;
239    }
240}
241
242impl<H: HashFunction + Clone, const S: usize> KeyDerivationFunction for Hkdf<H, S>
243where
244    Salt<S>: HkdfCompatible,
245{
246    type Algorithm = HkdfAlgorithm<H>;
247    type Salt = Salt<S>;
248
249    fn new() -> Self {
250        Hkdf {
251            _hash_type: PhantomData,
252            params: HkdfParams::default(),
253        }
254    }
255
256    fn derive_key(
257        &self,
258        input: &[u8],
259        salt: Option<&[u8]>,
260        info: Option<&[u8]>,
261        length: usize,
262    ) -> Result<ZeroizingBytes> {
263        let effective_salt = salt.or_else(|| self.params.salt.as_ref().map(|s| s.as_ref()));
264        let effective_info = info.or_else(|| self.params.info.as_deref());
265        Self::derive(effective_salt, input, effective_info, length)
266    }
267
268    // FIXED: Elided lifetime
269    fn builder(&self) -> impl KdfOperation<'_, Self::Algorithm> {
270        HkdfOperation::<H, S> {
271            kdf: self,
272            ikm: None,
273            salt: None,
274            info: None,
275            length: Self::Algorithm::DEFAULT_OUTPUT_SIZE,
276        }
277    }
278
279    fn generate_salt<R: RngCore + CryptoRng>(rng: &mut R) -> Result<Self::Salt> {
280        Salt::random_with_size(rng, Self::Algorithm::MIN_SALT_SIZE)
281    }
282
283    // Changed from instance method to static method
284    fn security_level() -> SecurityLevel {
285        match H::output_size() * 8 {
286            bits if bits >= 512 => SecurityLevel::L256,
287            bits if bits >= 384 => SecurityLevel::L192,
288            bits if bits >= 256 => SecurityLevel::L128,
289            bits => SecurityLevel::Custom(bits as u32 / 2),
290        }
291    }
292}
293
294impl<H: HashFunction + Clone, const S: usize> SecureZeroingType for Hkdf<H, S>
295where
296    Salt<S>: HkdfCompatible,
297{
298    fn zeroed() -> Self {
299        Self {
300            _hash_type: PhantomData,
301            params: HkdfParams::default(),
302        }
303    }
304
305    fn secure_clone(&self) -> Self {
306        self.clone()
307    }
308}
309
310#[cfg(test)]
311mod tests;