Skip to main content

dryoc/
hkdf.rs

1//! # HKDF key derivation
2//!
3//! [`HkdfSha256`] and [`HkdfSha512`] provide Rustaceous wrappers around
4//! libsodium's HKDF-SHA-256 and HKDF-SHA-512 functions.
5//!
6//! HKDF turns input keying material into one or more independent keys. It has
7//! two steps:
8//!
9//! * extract: mix the input keying material with an optional salt to produce a
10//!   pseudorandom key (PRK)
11//! * expand: derive output bytes from that PRK and a context string
12//!
13//! Use HKDF when you already have keying material, such as a shared secret from
14//! key exchange, and need separate keys for different purposes. The context is
15//! public domain-separation data; changing it changes the derived output.
16//!
17//! # Rustaceous API example
18//!
19//! ```
20//! use dryoc::hkdf::{HkdfSha256, HkdfSha256Prk};
21//!
22//! let hkdf: HkdfSha256 =
23//!     HkdfSha256::extract(Some(b"Act IV salt"), b"Now is the winter of our discontent");
24//! let output: Vec<u8> = hkdf
25//!     .expand_to_vec(42, b"session key")
26//!     .expect("expand failed");
27//! assert_eq!(output.len(), 42);
28//! ```
29//!
30//! # One-shot extract and expand
31//!
32//! ```
33//! use dryoc::hkdf::HkdfSha512;
34//!
35//! let output = HkdfSha512::extract_and_expand_to_vec(
36//!     64,
37//!     Some(b"optional deployment salt"),
38//!     b"Our remedies oft in ourselves do lie",
39//!     b"application secret",
40//! )
41//! .expect("expand failed");
42//! assert_eq!(output.len(), 64);
43//! ```
44//!
45//! # Reusing an extracted PRK
46//!
47//! ```
48//! use dryoc::hkdf::{HkdfSha256, HkdfSha256Prk};
49//!
50//! let hkdf = HkdfSha256::extract(Some(b"deployment salt"), b"We know what we are");
51//! let encryption_key: HkdfSha256Prk = hkdf.expand(b"encryption key").expect("expand failed");
52//! let authentication_key: HkdfSha256Prk =
53//!     hkdf.expand(b"authentication key").expect("expand failed");
54//! assert_ne!(encryption_key, authentication_key);
55//! ```
56//!
57//! The concrete expanders are type aliases over [`Hkdf`] and can also be used
58//! through [`HkdfVariant`] in generic code.
59
60use std::marker::PhantomData;
61
62#[cfg(feature = "serde")]
63use serde::{Deserialize, Serialize};
64use zeroize::{Zeroize, ZeroizeOnDrop};
65
66use crate::classic::crypto_kdf::{
67    crypto_kdf_hkdf_sha256_expand, crypto_kdf_hkdf_sha256_extract, crypto_kdf_hkdf_sha512_expand,
68    crypto_kdf_hkdf_sha512_extract,
69};
70use crate::constants::{
71    CRYPTO_KDF_HKDF_SHA256_BYTES_MAX, CRYPTO_KDF_HKDF_SHA256_BYTES_MIN,
72    CRYPTO_KDF_HKDF_SHA256_KEYBYTES, CRYPTO_KDF_HKDF_SHA512_BYTES_MAX,
73    CRYPTO_KDF_HKDF_SHA512_BYTES_MIN, CRYPTO_KDF_HKDF_SHA512_KEYBYTES,
74};
75use crate::error::Error;
76use crate::types::*;
77
78/// Stack-allocated pseudorandom key for HKDF-SHA-256.
79pub type HkdfSha256Prk = StackByteArray<CRYPTO_KDF_HKDF_SHA256_KEYBYTES>;
80/// Stack-allocated pseudorandom key for HKDF-SHA-512.
81pub type HkdfSha512Prk = StackByteArray<CRYPTO_KDF_HKDF_SHA512_KEYBYTES>;
82/// Stack-allocated HKDF-SHA-256 expander.
83pub type HkdfSha256 = Hkdf<HkdfSha256Variant, HkdfSha256Prk, CRYPTO_KDF_HKDF_SHA256_KEYBYTES>;
84/// Stack-allocated HKDF-SHA-512 expander.
85pub type HkdfSha512 = Hkdf<HkdfSha512Variant, HkdfSha512Prk, CRYPTO_KDF_HKDF_SHA512_KEYBYTES>;
86
87#[cfg_attr(
88    feature = "serde",
89    derive(Zeroize, Clone, Debug, Serialize, Deserialize)
90)]
91#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
92/// HKDF expander for a specific [`HkdfVariant`].
93pub struct Hkdf<Variant, Prk, const PRK_LENGTH: usize>
94where
95    Variant: HkdfVariant<PRK_LENGTH>,
96    Prk: ByteArray<PRK_LENGTH> + Zeroize + ZeroizeOnDrop,
97{
98    prk: Prk,
99    _variant: PhantomData<Variant>,
100}
101
102/// HKDF-SHA-256 expander.
103pub type HkdfSha256Expander<Prk> = Hkdf<HkdfSha256Variant, Prk, CRYPTO_KDF_HKDF_SHA256_KEYBYTES>;
104/// HKDF-SHA-512 expander.
105pub type HkdfSha512Expander<Prk> = Hkdf<HkdfSha512Variant, Prk, CRYPTO_KDF_HKDF_SHA512_KEYBYTES>;
106
107/// HKDF-SHA-256 algorithm marker.
108#[derive(Clone, Copy, Debug, Default)]
109pub struct HkdfSha256Variant;
110/// HKDF-SHA-512 algorithm marker.
111#[derive(Clone, Copy, Debug, Default)]
112pub struct HkdfSha512Variant;
113
114#[cfg(any(all(feature = "protected", any(unix, windows)), all(doc, not(doctest))))]
115#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
116pub mod protected {
117    //! # Protected memory type aliases for HKDF
118    //!
119    //! This mod provides protected-memory PRK aliases and locked HKDF aliases.
120    //! Use these aliases when the extracted PRK or expanded output should stay
121    //! in locked memory.
122    //!
123    //! ```
124    //! use dryoc::hkdf::HkdfSha512Expander;
125    //! use dryoc::hkdf::protected::*;
126    //!
127    //! let ikm = HeapBytes::from_slice_into_readonly_locked(b"Truth will come to light.")
128    //!     .expect("ikm failed");
129    //! let hkdf: LockedHkdfSha512 = HkdfSha512Expander::extract(None::<&[u8]>, &ikm);
130    //! let output: Locked<HeapBytes> = hkdf.expand_to_bytes(64, b"context").expect("expand failed");
131    //! assert_eq!(output.len(), 64);
132    //! ```
133    use super::*;
134    pub use crate::protected::*;
135
136    /// Heap-allocated, page-aligned pseudorandom key for HKDF-SHA-256.
137    pub type HkdfSha256Prk = HeapByteArray<CRYPTO_KDF_HKDF_SHA256_KEYBYTES>;
138    /// Heap-allocated, page-aligned pseudorandom key for HKDF-SHA-512.
139    pub type HkdfSha512Prk = HeapByteArray<CRYPTO_KDF_HKDF_SHA512_KEYBYTES>;
140
141    /// Locked HKDF-SHA-256 expander.
142    pub type LockedHkdfSha256 = HkdfSha256Expander<Locked<HkdfSha256Prk>>;
143    /// Locked HKDF-SHA-512 expander.
144    pub type LockedHkdfSha512 = HkdfSha512Expander<Locked<HkdfSha512Prk>>;
145}
146
147/// HKDF algorithm variant used by [`Hkdf`].
148pub trait HkdfVariant<const PRK_LENGTH: usize> {
149    /// Default stack-allocated PRK type for this variant.
150    type Prk: NewByteArray<PRK_LENGTH> + Zeroize + ZeroizeOnDrop;
151    /// Minimum output length accepted by this variant.
152    const OUTPUT_BYTES_MIN: usize;
153    /// Maximum output length accepted by this variant.
154    const OUTPUT_BYTES_MAX: usize;
155
156    /// Creates a PRK from input keying material and optional salt.
157    fn extract(prk: &mut [u8; PRK_LENGTH], salt: Option<&[u8]>, ikm: &[u8]);
158    /// Expands a PRK into output keying material.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if `output.len()` is outside the range supported by
163    /// this variant.
164    fn expand(output: &mut [u8], context: &[u8], prk: &[u8; PRK_LENGTH]) -> Result<(), Error>;
165
166    /// Validates an output length before allocating output storage.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if `output_len` is smaller than
171    /// [`Self::OUTPUT_BYTES_MIN`] or larger than [`Self::OUTPUT_BYTES_MAX`].
172    fn validate_output_len(output_len: usize) -> Result<(), Error> {
173        if output_len < Self::OUTPUT_BYTES_MIN || output_len > Self::OUTPUT_BYTES_MAX {
174            Err(length_error!(
175                crate::ErrorContext::Output,
176                output_len,
177                range Self::OUTPUT_BYTES_MIN,
178                Self::OUTPUT_BYTES_MAX
179            ))
180        } else {
181            Ok(())
182        }
183    }
184}
185
186macro_rules! impl_hkdf_variant {
187    (
188        $variant:ty,
189        $prk_len:expr,
190        $prk:ty,
191        $bytes_min:expr,
192        $bytes_max:expr,
193        $extract:path,
194        $expand:path
195    ) => {
196        impl HkdfVariant<$prk_len> for $variant {
197            type Prk = $prk;
198
199            const OUTPUT_BYTES_MAX: usize = $bytes_max;
200            const OUTPUT_BYTES_MIN: usize = $bytes_min;
201
202            fn extract(prk: &mut [u8; $prk_len], salt: Option<&[u8]>, ikm: &[u8]) {
203                $extract(prk, salt, ikm);
204            }
205
206            fn expand(
207                output: &mut [u8],
208                context: &[u8],
209                prk: &[u8; $prk_len],
210            ) -> Result<(), Error> {
211                $expand(output, context, prk)
212            }
213        }
214    };
215}
216
217impl_hkdf_variant!(
218    HkdfSha256Variant,
219    CRYPTO_KDF_HKDF_SHA256_KEYBYTES,
220    HkdfSha256Prk,
221    CRYPTO_KDF_HKDF_SHA256_BYTES_MIN,
222    CRYPTO_KDF_HKDF_SHA256_BYTES_MAX,
223    crypto_kdf_hkdf_sha256_extract,
224    crypto_kdf_hkdf_sha256_expand
225);
226
227impl_hkdf_variant!(
228    HkdfSha512Variant,
229    CRYPTO_KDF_HKDF_SHA512_KEYBYTES,
230    HkdfSha512Prk,
231    CRYPTO_KDF_HKDF_SHA512_BYTES_MIN,
232    CRYPTO_KDF_HKDF_SHA512_BYTES_MAX,
233    crypto_kdf_hkdf_sha512_extract,
234    crypto_kdf_hkdf_sha512_expand
235);
236
237impl<Variant, Prk, const PRK_LENGTH: usize> Hkdf<Variant, Prk, PRK_LENGTH>
238where
239    Variant: HkdfVariant<PRK_LENGTH>,
240    Prk: NewByteArray<PRK_LENGTH> + Zeroize + ZeroizeOnDrop,
241{
242    /// Randomly generates a new PRK for HKDF expand.
243    pub fn generate() -> Self {
244        Self {
245            prk: Prk::generate(),
246            _variant: PhantomData,
247        }
248    }
249
250    /// Randomly generates a new PRK for HKDF expand.
251    ///
252    /// Prefer [`generate`](Self::generate). `gen` is retained for compatibility
253    /// with older Rust editions.
254    #[deprecated(note = "use generate() instead")]
255    pub fn r#gen() -> Self {
256        Self::generate()
257    }
258
259    /// Extracts a PRK from input keying material and optional salt.
260    pub fn extract<Salt: Bytes + ?Sized, Ikm: Bytes + ?Sized>(
261        salt: Option<&Salt>,
262        ikm: &Ikm,
263    ) -> Self {
264        let mut prk = Prk::new_byte_array();
265        Variant::extract(
266            prk.as_mut_array(),
267            salt.map(|s| s.as_slice()),
268            ikm.as_slice(),
269        );
270        Self {
271            prk,
272            _variant: PhantomData,
273        }
274    }
275
276    /// One-shot HKDF extract-and-expand into a fixed-size output type.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if `OUTPUT_LENGTH` is outside the range supported by
281    /// the selected HKDF variant.
282    pub fn extract_and_expand<
283        const OUTPUT_LENGTH: usize,
284        Salt: Bytes + ?Sized,
285        Ikm: Bytes + ?Sized,
286        Context: Bytes + ?Sized,
287        Output: NewByteArray<OUTPUT_LENGTH>,
288    >(
289        salt: Option<&Salt>,
290        ikm: &Ikm,
291        context: &Context,
292    ) -> Result<Output, Error> {
293        Self::extract(salt, ikm).expand(context)
294    }
295
296    /// One-shot HKDF extract-and-expand into a [`Vec`].
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if `output_len` is outside the range supported by the
301    /// selected HKDF variant.
302    pub fn extract_and_expand_to_vec<
303        Salt: Bytes + ?Sized,
304        Ikm: Bytes + ?Sized,
305        Context: Bytes + ?Sized,
306    >(
307        output_len: usize,
308        salt: Option<&Salt>,
309        ikm: &Ikm,
310        context: &Context,
311    ) -> Result<Vec<u8>, Error> {
312        Self::extract(salt, ikm).expand_to_vec(output_len, context)
313    }
314
315    /// One-shot HKDF extract-and-expand into a runtime-sized byte container.
316    ///
317    /// # Errors
318    ///
319    /// Returns an error if `output_len` is outside the range supported by the
320    /// selected HKDF variant.
321    pub fn extract_and_expand_to_bytes<
322        Salt: Bytes + ?Sized,
323        Ikm: Bytes + ?Sized,
324        Context: Bytes + ?Sized,
325        Output: NewBytes + ResizableBytes,
326    >(
327        output_len: usize,
328        salt: Option<&Salt>,
329        ikm: &Ikm,
330        context: &Context,
331    ) -> Result<Output, Error> {
332        Self::extract(salt, ikm).expand_to_bytes(output_len, context)
333    }
334}
335
336impl<Variant, Prk, const PRK_LENGTH: usize> Hkdf<Variant, Prk, PRK_LENGTH>
337where
338    Variant: HkdfVariant<PRK_LENGTH>,
339    Prk: ByteArray<PRK_LENGTH> + Zeroize + ZeroizeOnDrop,
340{
341    /// Constructs an HKDF expander from a PRK, consuming it.
342    pub fn from_prk(prk: Prk) -> Self {
343        Self {
344            prk,
345            _variant: PhantomData,
346        }
347    }
348
349    /// Moves the PRK out of this expander.
350    pub fn into_prk(self) -> Prk {
351        self.prk
352    }
353
354    /// Expands this PRK into a fixed-size output type.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if `OUTPUT_LENGTH` is outside the range supported by
359    /// the selected HKDF variant.
360    pub fn expand<const OUTPUT_LENGTH: usize, Context: Bytes + ?Sized, Output>(
361        &self,
362        context: &Context,
363    ) -> Result<Output, Error>
364    where
365        Output: NewByteArray<OUTPUT_LENGTH>,
366    {
367        Variant::validate_output_len(OUTPUT_LENGTH)?;
368        let mut output = Output::new_byte_array();
369        Variant::expand(
370            output.as_mut_slice(),
371            context.as_slice(),
372            self.prk.as_array(),
373        )?;
374        Ok(output)
375    }
376
377    /// Expands this PRK into a [`Vec`] of `output_len` bytes.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if `output_len` is outside the range supported by the
382    /// selected HKDF variant.
383    pub fn expand_to_vec<Context: Bytes + ?Sized>(
384        &self,
385        output_len: usize,
386        context: &Context,
387    ) -> Result<Vec<u8>, Error> {
388        self.expand_to_bytes(output_len, context)
389    }
390
391    /// Expands this PRK into a runtime-sized byte container.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if `output_len` is outside the range supported by the
396    /// selected HKDF variant.
397    pub fn expand_to_bytes<Context: Bytes + ?Sized, Output: NewBytes + ResizableBytes>(
398        &self,
399        output_len: usize,
400        context: &Context,
401    ) -> Result<Output, Error> {
402        Variant::validate_output_len(output_len)?;
403        let mut output = Output::new_bytes();
404        output.resize(output_len, 0);
405        Variant::expand(
406            output.as_mut_slice(),
407            context.as_slice(),
408            self.prk.as_array(),
409        )?;
410        Ok(output)
411    }
412}
413
414impl<Variant, const PRK_LENGTH: usize> Hkdf<Variant, Variant::Prk, PRK_LENGTH>
415where
416    Variant: HkdfVariant<PRK_LENGTH>,
417{
418    /// Randomly generates a new PRK using the default stack-allocated type.
419    pub fn generate_with_defaults() -> Self {
420        Self::generate()
421    }
422
423    /// Randomly generates a new PRK using the default stack-allocated type.
424    ///
425    /// Prefer [`generate_with_defaults`](Self::generate_with_defaults). This
426    /// method is retained for compatibility.
427    #[deprecated(note = "use generate_with_defaults() instead")]
428    pub fn gen_with_defaults() -> Self {
429        Self::generate_with_defaults()
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_hkdf_sha256() {
439        let hkdf = HkdfSha256::extract(Some(b"salt"), b"input keying material");
440        let output: HkdfSha256Prk = hkdf.expand(b"context").expect("expand failed");
441        assert_eq!(output.len(), CRYPTO_KDF_HKDF_SHA256_KEYBYTES);
442
443        let output = hkdf.expand_to_vec(42, b"context").expect("expand failed");
444        assert_eq!(output.len(), 42);
445    }
446
447    #[test]
448    fn test_hkdf_sha512() {
449        let output: Vec<u8> =
450            HkdfSha512::extract_and_expand_to_vec(64, Some(b"salt"), b"ikm", b"context")
451                .expect("expand failed");
452        assert_eq!(output.len(), 64);
453    }
454
455    #[test]
456    fn test_hkdf_rejects_invalid_length() {
457        let hkdf = HkdfSha256::extract(None::<&[u8]>, b"ikm");
458        hkdf.expand_to_vec(
459            crate::constants::CRYPTO_KDF_HKDF_SHA256_BYTES_MAX + 1,
460            b"context",
461        )
462        .expect_err("oversized output should fail");
463    }
464
465    #[test]
466    fn test_hkdf_rejects_huge_length_before_allocation() {
467        let hkdf = HkdfSha256::extract(None::<&[u8]>, b"ikm");
468        hkdf.expand_to_vec(usize::MAX, b"context")
469            .expect_err("huge output should fail before allocation");
470    }
471
472    #[test]
473    fn test_hkdf_variant_generic_api() {
474        fn extract_and_expand_with_variant<Variant, const PRK_LENGTH: usize>(
475            salt: Option<&[u8]>,
476            ikm: &[u8],
477            context: &[u8],
478        ) -> Vec<u8>
479        where
480            Variant: HkdfVariant<PRK_LENGTH>,
481        {
482            Hkdf::<Variant, Variant::Prk, PRK_LENGTH>::extract_and_expand_to_vec(
483                42, salt, ikm, context,
484            )
485            .expect("expand failed")
486        }
487
488        let generic_output = extract_and_expand_with_variant::<
489            HkdfSha256Variant,
490            CRYPTO_KDF_HKDF_SHA256_KEYBYTES,
491        >(Some(b"salt"), b"input keying material", b"context");
492        let concrete_output = HkdfSha256::extract_and_expand_to_vec(
493            42,
494            Some(b"salt"),
495            b"input keying material",
496            b"context",
497        )
498        .expect("expand failed");
499
500        assert_eq!(generic_output, concrete_output);
501    }
502}