Skip to main content

dryoc/
kdf.rs

1//! # Key derivation functions
2//!
3//! [`Kdf`] derives separate, context-bound subkeys from one random master key.
4//! It implements libsodium's BLAKE2b-based `crypto_kdf` construction.
5//!
6//! Use [`Kdf`] when an application needs several keys for different purposes.
7//! Assign each purpose a distinct context and subkey ID so that it produces a
8//! different subkey.
9//!
10//! # Rustaceous API example
11//!
12//! ```
13//! # #[cfg(feature = "alloc")]
14//! # {
15//! use base64::Engine as _;
16//! use base64::engine::general_purpose;
17//! use dryoc::kdf::*;
18//!
19//! // Generate a random main key with the default stack-allocated type.
20//! let key = StackKdf::generate();
21//! let subkey_id = 0;
22//!
23//! let subkey = key
24//!     .derive_subkey_to_vec(subkey_id, 32)
25//!     .expect("derive failed");
26//! println!(
27//!     "Subkey {}: {}",
28//!     subkey_id,
29//!     general_purpose::STANDARD.encode(&subkey)
30//! );
31//! # }
32//! ```
33//!
34//! ## Additional resources
35//!
36//! * See the [libsodium documentation](https://doc.libsodium.org/key_derivation)
37//!   for more about key derivation
38
39#[cfg(feature = "alloc")]
40use alloc::vec::Vec;
41use core::fmt;
42
43#[cfg(feature = "serde")]
44use serde::{Deserialize, Serialize};
45use zeroize::{Zeroize, ZeroizeOnDrop};
46
47use crate::classic::crypto_kdf::{crypto_kdf_derive_from_key, validate_subkey_length};
48use crate::constants::{CRYPTO_KDF_CONTEXTBYTES, CRYPTO_KDF_KEYBYTES};
49use crate::error::Error;
50use crate::types::*;
51
52/// Stack-allocated key type alias for key derivation with [`Kdf`].
53pub type Key = StackByteArray<CRYPTO_KDF_KEYBYTES>;
54/// Stack-allocated context type alias for key derivation with [`Kdf`].
55pub type Context = StackByteArray<CRYPTO_KDF_CONTEXTBYTES>;
56
57#[derive(Zeroize, Clone)]
58#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
59/// Key derivation implementation based on Blake2b, compatible with libsodium's
60/// `crypto_kdf_*` functions.
61///
62/// The main-key type must implement [`ZeroizeOnDrop`] so keys remain
63/// self-wiping after [`Kdf::into_parts`] transfers ownership to the caller.
64pub struct Kdf<
65    Key: ByteArray<CRYPTO_KDF_KEYBYTES> + Zeroize + ZeroizeOnDrop,
66    Context: ByteArray<CRYPTO_KDF_CONTEXTBYTES> + Zeroize,
67> {
68    main_key: Key,
69    context: Context,
70}
71
72impl<
73    Key: ByteArray<CRYPTO_KDF_KEYBYTES> + Zeroize + ZeroizeOnDrop,
74    Context: ByteArray<CRYPTO_KDF_CONTEXTBYTES> + Zeroize,
75> fmt::Debug for Kdf<Key, Context>
76{
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.debug_struct("Kdf")
79            .field("main_key", &"[REDACTED]")
80            .field("context", &self.context.as_slice())
81            .finish()
82    }
83}
84
85/// Stack-allocated type alias for [`Kdf`]. Provided for convenience.
86pub type StackKdf = Kdf<Key, Context>;
87
88#[cfg(any(
89    all(feature = "protected", any(unix, windows)),
90    all(doc, not(doctest), feature = "std")
91))]
92#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "protected")))]
93pub mod protected {
94    //! # Protected memory type aliases for [`Kdf`]
95    //!
96    //! Protected-memory aliases for key derivation.
97    //!
98    //! ## Example
99    //!
100    //! ```
101    //! use base64::Engine as _;
102    //! use base64::engine::general_purpose;
103    //! use dryoc::kdf::Kdf;
104    //! use dryoc::kdf::protected::*;
105    //!
106    //! // Randomly generate a main key and context, using locked memory
107    //! let key: LockedKdf = Kdf::generate();
108    //! let subkey_id = 0;
109    //!
110    //! let subkey: Locked<Key> = key.derive_subkey(subkey_id).expect("derive failed");
111    //! println!(
112    //!     "Subkey {}: {}",
113    //!     subkey_id,
114    //!     general_purpose::STANDARD.encode(&subkey)
115    //! );
116    //! ```
117    use super::*;
118    pub use crate::protected::*;
119
120    /// Heap-allocated, page-aligned key type alias for key derivation with
121    /// [`Kdf`].
122    pub type Key = HeapByteArray<CRYPTO_KDF_KEYBYTES>;
123    /// Heap-allocated, page-aligned context type alias for key derivation with
124    /// [`Kdf`].
125    pub type Context = HeapByteArray<CRYPTO_KDF_CONTEXTBYTES>;
126
127    /// Locked [`Kdf`], provided as a type alias for convenience.
128    pub type LockedKdf = Kdf<Locked<Key>, Locked<Context>>;
129}
130
131impl<
132    Key: NewByteArray<CRYPTO_KDF_KEYBYTES> + Zeroize + ZeroizeOnDrop,
133    Context: NewByteArray<CRYPTO_KDF_CONTEXTBYTES> + Zeroize,
134> Kdf<Key, Context>
135{
136    /// Randomly generates a new pair of main key and context.
137    #[must_use]
138    pub fn generate() -> Self {
139        Self {
140            main_key: Key::generate(),
141            context: Context::generate(),
142        }
143    }
144}
145
146impl<
147    Key: ByteArray<CRYPTO_KDF_KEYBYTES> + Zeroize + ZeroizeOnDrop,
148    Context: ByteArray<CRYPTO_KDF_CONTEXTBYTES> + Zeroize,
149> Kdf<Key, Context>
150{
151    /// Derives a subkey for `subkey_id`, returning it.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error unless `LENGTH` is between
156    /// [`CRYPTO_KDF_BLAKE2B_BYTES_MIN`](crate::constants::CRYPTO_KDF_BLAKE2B_BYTES_MIN)
157    /// and
158    /// [`CRYPTO_KDF_BLAKE2B_BYTES_MAX`](crate::constants::CRYPTO_KDF_BLAKE2B_BYTES_MAX),
159    /// inclusive.
160    pub fn derive_subkey<const LENGTH: usize, Subkey: NewByteArray<LENGTH>>(
161        &self,
162        subkey_id: u64,
163    ) -> Result<Subkey, Error> {
164        validate_subkey_length(LENGTH)?;
165        let mut subkey = Subkey::new_byte_array();
166        crypto_kdf_derive_from_key(
167            subkey.as_mut_array(),
168            subkey_id,
169            self.context.as_array(),
170            self.main_key.as_array(),
171        )?;
172        Ok(subkey)
173    }
174
175    /// Derives a subkey for `subkey_id`, returning it as a [`Vec`]. Provided
176    /// for convenience.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error unless `length` is between
181    /// [`CRYPTO_KDF_BLAKE2B_BYTES_MIN`](crate::constants::CRYPTO_KDF_BLAKE2B_BYTES_MIN)
182    /// and
183    /// [`CRYPTO_KDF_BLAKE2B_BYTES_MAX`](crate::constants::CRYPTO_KDF_BLAKE2B_BYTES_MAX),
184    /// inclusive.
185    #[cfg(feature = "alloc")]
186    pub fn derive_subkey_to_vec(&self, subkey_id: u64, length: usize) -> Result<Vec<u8>, Error> {
187        validate_subkey_length(length)?;
188        let mut subkey = vec![0u8; length];
189        crypto_kdf_derive_from_key(
190            &mut subkey,
191            subkey_id,
192            self.context.as_array(),
193            self.main_key.as_array(),
194        )?;
195        Ok(subkey)
196    }
197
198    /// Constructs a new instance from `key` and `context`, consuming them both.
199    #[must_use]
200    pub fn from_parts(main_key: Key, context: Context) -> Self {
201        Self { main_key, context }
202    }
203
204    /// Moves the key and context out of this instance, returning them as a
205    /// tuple.
206    #[must_use]
207    pub fn into_parts(self) -> (Key, Context) {
208        (self.main_key, self.context)
209    }
210}
211
212#[cfg(all(test, feature = "alloc"))]
213mod tests {
214    use super::*;
215    use crate::constants::{
216        CRYPTO_KDF_BLAKE2B_BYTES_MAX as CRYPTO_KDF_BYTES_MAX,
217        CRYPTO_KDF_BLAKE2B_BYTES_MIN as CRYPTO_KDF_BYTES_MIN,
218    };
219
220    /// libsodium `test/default/kdf.c` main key (`0..32`) and context
221    /// (`"KDF test"`); `(subkey_id, expected subkey)` as produced by
222    /// libsodium's `crypto_kdf_derive_from_key` at 16, 32, and 64 bytes.
223    /// Only the 64-byte subkey 0 appears in `kdf.c` itself; the other rows
224    /// were generated with libsodium, and
225    /// `matches_libsodium_derive_from_key` checks the same ids and lengths
226    /// against it at runtime.
227    const KAT: [(u64, [&str; 3]); 2] = [
228        (
229            0,
230            [
231                "e9136a52b9690eb4df4e9665e819a6d3",
232                "c13fcc2e6cd0cd0f82d93b163a5696c5105378f8c629d36baf3ae0239de9c280",
233                concat!(
234                    "a0c724404728c8bb95e5433eb6a9716171144d61efb23e74b873fcbeda51d807",
235                    "1b5d70aae12066dfc94ce943f145aa176c055040c3dd73b0a15e36254d450614",
236                ),
237            ],
238        ),
239        (
240            u64::MAX,
241            [
242                "040f6b7312b53bce5d711bb9c589cdd4",
243                "500c3043b2b9177ec843ecbe9f98f92d8c11fbbd10a225ab844548de89c21d55",
244                concat!(
245                    "6be4464350f6934d151c1bb8f555bc18e75028be95b892c6dca047101f2827a1",
246                    "950b2b0fb35e996a2782db9a760e76c8b8da52e362f741bf5bcfefff0fc943fc",
247                ),
248            ],
249        ),
250    ];
251
252    fn kat_kdf() -> StackKdf {
253        let key: [u8; CRYPTO_KDF_KEYBYTES] = core::array::from_fn(|i| i as u8);
254        Kdf::from_parts(Key::from(key), Context::from(*b"KDF test"))
255    }
256
257    #[test]
258    fn derives_libsodium_known_answers_for_ids_zero_and_max() {
259        let kdf = kat_kdf();
260        for (subkey_id, expected) in KAT {
261            let expected16 = hex::decode(expected[0]).expect("hex");
262            let expected32 = hex::decode(expected[1]).expect("hex");
263            let expected64 = hex::decode(expected[2]).expect("hex");
264
265            let short: StackByteArray<16> = kdf.derive_subkey(subkey_id).expect("derive");
266            let medium: StackByteArray<32> = kdf.derive_subkey(subkey_id).expect("derive");
267            let long: StackByteArray<64> = kdf.derive_subkey(subkey_id).expect("derive");
268            assert_eq!(short.as_slice(), expected16.as_slice());
269            assert_eq!(medium.as_slice(), expected32.as_slice());
270            assert_eq!(long.as_slice(), expected64.as_slice());
271
272            assert_eq!(
273                kdf.derive_subkey_to_vec(subkey_id, 16).expect("derive"),
274                expected16
275            );
276            assert_eq!(
277                kdf.derive_subkey_to_vec(subkey_id, 32).expect("derive"),
278                expected32
279            );
280            assert_eq!(
281                kdf.derive_subkey_to_vec(subkey_id, 64).expect("derive"),
282                expected64
283            );
284
285            // Fixed-size and Vec outputs of different lengths are distinct
286            // derivations, not prefixes of one another.
287            assert_ne!(&expected64[..32], expected32.as_slice());
288            assert_ne!(&expected32[..16], expected16.as_slice());
289        }
290    }
291
292    #[test]
293    fn matches_classic_derive_from_key_and_separates_key_context_and_id() {
294        let kdf = kat_kdf();
295        let (key, context) = kdf.clone().into_parts();
296        for subkey_id in [0, 1, 2, u64::from(u32::MAX), u64::MAX - 1, u64::MAX] {
297            for length in [
298                CRYPTO_KDF_BYTES_MIN,
299                17,
300                31,
301                32,
302                33,
303                63,
304                CRYPTO_KDF_BYTES_MAX,
305            ] {
306                let mut classic = vec![0u8; length];
307                crypto_kdf_derive_from_key(
308                    &mut classic,
309                    subkey_id,
310                    context.as_array(),
311                    key.as_array(),
312                )
313                .expect("classic derive");
314                assert_eq!(
315                    kdf.derive_subkey_to_vec(subkey_id, length).expect("derive"),
316                    classic
317                );
318            }
319        }
320
321        let baseline = kdf.derive_subkey_to_vec(7, 32).expect("derive");
322        assert_ne!(kdf.derive_subkey_to_vec(8, 32).expect("derive"), baseline);
323
324        let mut other_key = key.clone();
325        other_key[0] ^= 1;
326        assert_ne!(
327            Kdf::from_parts(other_key, context.clone())
328                .derive_subkey_to_vec(7, 32)
329                .expect("derive"),
330            baseline
331        );
332        let mut other_context = context.clone();
333        other_context[CRYPTO_KDF_CONTEXTBYTES - 1] ^= 1;
334        assert_ne!(
335            Kdf::from_parts(key, other_context)
336                .derive_subkey_to_vec(7, 32)
337                .expect("derive"),
338            baseline
339        );
340    }
341
342    #[test]
343    fn rejects_out_of_range_subkey_lengths_and_redacts_debug_output() {
344        let kdf = kat_kdf();
345        assert!(format!("{kdf:?}").contains("[REDACTED]"));
346        assert!(!format!("{kdf:?}").contains("KDF test"));
347
348        for length in [
349            0,
350            CRYPTO_KDF_BYTES_MIN - 1,
351            CRYPTO_KDF_BYTES_MAX + 1,
352            usize::MAX,
353        ] {
354            assert!(matches!(
355                kdf.derive_subkey_to_vec(0, length),
356                Err(Error::InvalidLength {
357                    context: crate::ErrorContext::Subkey,
358                    actual,
359                    ..
360                }) if actual == length
361            ));
362        }
363        assert!(matches!(
364            kdf.derive_subkey::<15, StackByteArray<15>>(0),
365            Err(Error::InvalidLength {
366                context: crate::ErrorContext::Subkey,
367                actual: 15,
368                ..
369            })
370        ));
371        assert!(matches!(
372            kdf.derive_subkey::<65, StackByteArray<65>>(0),
373            Err(Error::InvalidLength {
374                context: crate::ErrorContext::Subkey,
375                actual: 65,
376                ..
377            })
378        ));
379    }
380
381    #[cfg(feature = "serde")]
382    #[test]
383    fn serde_round_trip_derives_the_same_subkeys() {
384        let kdf = kat_kdf();
385        let json = serde_json::to_string(&kdf).expect("serialize");
386        let decoded: StackKdf = serde_json::from_str(&json).expect("deserialize");
387        for (subkey_id, expected) in KAT {
388            assert_eq!(
389                decoded.derive_subkey_to_vec(subkey_id, 64).expect("derive"),
390                hex::decode(expected[2]).expect("hex")
391            );
392        }
393        let (key, context) = decoded.into_parts();
394        assert_eq!(context.as_slice(), b"KDF test");
395        assert_eq!(key, kat_kdf().into_parts().0);
396    }
397
398    #[cfg(all(feature = "protected", any(unix, windows)))]
399    #[test]
400    fn locked_kdf_derives_the_same_subkeys_as_the_stack_kdf() {
401        use crate::kdf::protected::*;
402
403        let (key, context) = kat_kdf().into_parts();
404        let locked: LockedKdf = Kdf::from_parts(
405            protected::Key::from_slice_into_locked(key.as_slice()).expect("lock key"),
406            protected::Context::from_slice_into_locked(context.as_slice()).expect("lock context"),
407        );
408        for (subkey_id, expected) in KAT {
409            let locked_subkey: Locked<HeapByteArray<64>> =
410                locked.derive_subkey(subkey_id).expect("derive");
411            assert_eq!(
412                locked_subkey.as_slice(),
413                hex::decode(expected[2]).expect("hex").as_slice()
414            );
415            assert_eq!(
416                locked.derive_subkey_to_vec(subkey_id, 16).expect("derive"),
417                hex::decode(expected[0]).expect("hex")
418            );
419        }
420    }
421
422    #[cfg(dryoc_native_tests)]
423    #[test]
424    fn matches_libsodium_derive_from_key() {
425        use crate::utils::test_util::XorShift64;
426
427        crate::native_test_util::init();
428
429        let mut rng = XorShift64::new(0x6b64_665f_7465_7374);
430        for _ in 0..8 {
431            let key = Key::from(rng.next_bytes32());
432            let context =
433                Context::try_from(&rng.next_bytes32()[..CRYPTO_KDF_CONTEXTBYTES]).expect("context");
434            let kdf = Kdf::from_parts(key.clone(), context.clone());
435            for subkey_id in [0, rng.next_u64(), u64::MAX] {
436                for length in [CRYPTO_KDF_BYTES_MIN, 32, CRYPTO_KDF_BYTES_MAX] {
437                    let mut sodium = vec![0u8; length];
438                    let rc = unsafe {
439                        libsodium_sys::crypto_kdf_derive_from_key(
440                            sodium.as_mut_ptr(),
441                            length,
442                            subkey_id,
443                            context.as_ptr().cast(),
444                            key.as_ptr(),
445                        )
446                    };
447                    assert_eq!(rc, 0);
448                    assert_eq!(
449                        kdf.derive_subkey_to_vec(subkey_id, length).expect("derive"),
450                        sodium
451                    );
452                }
453            }
454        }
455    }
456}