Skip to main content

key_vault/decoy/
key_derived.rs

1//! [`KeyDerivedDecoy`] — BLAKE3-XOF derived decoy bytes.
2
3use alloc::borrow::Cow;
4use alloc::vec;
5use alloc::vec::Vec;
6
7use super::DecoyStrategy;
8use crate::Result;
9use crate::error::Error;
10use crate::fetcher::RawKey;
11
12/// Decoy strategy that produces bytes via BLAKE3's extendable-output (XOF)
13/// mode, seeded by the key and a fresh CSPRNG nonce.
14///
15/// **Threat profile.** A middle ground between [`RandomDecoy`](super::RandomDecoy)
16/// and [`SelfReferenceDecoy`](super::SelfReferenceDecoy):
17///
18/// - The output passes general statistical tests like a CSPRNG would, so
19///   simple entropy/chi-squared distinguishers cannot tell decoy bytes from
20///   `RandomDecoy` output.
21/// - Because the seed includes the key, downstream cryptographic analysis
22///   that looks for "uniform-random vs. structured" patterns sees the same
23///   thing it sees for the real key (which is itself a hashed/derived blob in
24///   most modern protocols).
25/// - Less aggressive than `SelfReferenceDecoy` for keys with very
26///   non-uniform byte distributions (e.g. DER-encoded RSA keys), but
27///   strictly stronger than `RandomDecoy`.
28///
29/// Use `KeyDerivedDecoy` when you want CSPRNG-like output but seeded by the
30/// real key so the resulting profile correlates with it.
31///
32/// # Examples
33///
34/// ```
35/// use key_vault::decoy::{DecoyStrategy, KeyDerivedDecoy};
36/// use key_vault::RawKey;
37///
38/// let key = RawKey::new(b"the key".to_vec());
39/// let decoy = KeyDerivedDecoy.generate(&key, 32).unwrap();
40/// assert_eq!(decoy.len(), 32);
41/// ```
42#[derive(Debug, Default, Clone, Copy)]
43pub struct KeyDerivedDecoy;
44
45impl DecoyStrategy for KeyDerivedDecoy {
46    fn generate(&self, key: &RawKey, output_len: usize) -> Result<Vec<u8>> {
47        if output_len == 0 {
48            return Ok(Vec::new());
49        }
50        // Mix in a per-call nonce so two consecutive `generate` calls with
51        // the same key produce different output. Without the nonce the
52        // strategy would be a pure function of the key, which would allow
53        // an attacker who knows the key bytes to recompute the decoy and
54        // confirm a fragmentation.
55        let mut nonce = [0u8; 32];
56        getrandom::getrandom(&mut nonce).map_err(|_| Error::Internal("OS RNG failed"))?;
57
58        let mut hasher = blake3::Hasher::new();
59        // `Hasher::update` returns `&mut Self` for chaining; we bind to `_`
60        // to satisfy `#![deny(unused_results)]`.
61        let _ = hasher.update(key.as_bytes());
62        let _ = hasher.update(&nonce);
63
64        let mut out = vec![0u8; output_len];
65        let mut reader = hasher.finalize_xof();
66        reader.fill(&mut out);
67
68        // Scrub the nonce. The key bytes themselves belong to `key` and are
69        // not our responsibility to wipe.
70        // SAFETY: nonce is a fixed-size stack array we own; we write within
71        // bounds.
72        unsafe {
73            let ptr = nonce.as_mut_ptr();
74            for i in 0..nonce.len() {
75                core::ptr::write_volatile(ptr.add(i), 0u8);
76            }
77        }
78        core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
79
80        Ok(out)
81    }
82
83    fn describe(&self) -> Cow<'_, str> {
84        Cow::Borrowed("key-derived")
85    }
86}
87
88#[cfg(test)]
89#[allow(
90    clippy::unwrap_used,
91    clippy::expect_used,
92    clippy::cast_possible_truncation,
93    clippy::cast_sign_loss
94)]
95mod tests {
96    use super::*;
97
98    fn raw(bytes: &[u8]) -> RawKey {
99        RawKey::new(bytes.to_vec())
100    }
101
102    #[test]
103    fn produces_requested_length() {
104        let key = raw(b"k");
105        for n in [0usize, 1, 7, 32, 256, 4096] {
106            let out = KeyDerivedDecoy.generate(&key, n).unwrap();
107            assert_eq!(out.len(), n, "wrong length for n = {n}");
108        }
109    }
110
111    #[test]
112    fn two_calls_with_same_key_produce_different_outputs() {
113        // Without the per-call nonce this would be the SAME bytes both
114        // times. With it, two outputs must differ.
115        let key = raw(b"deterministic seed");
116        let a = KeyDerivedDecoy.generate(&key, 64).unwrap();
117        let b = KeyDerivedDecoy.generate(&key, 64).unwrap();
118        assert_ne!(a, b);
119    }
120
121    #[test]
122    fn different_keys_produce_different_outputs() {
123        let a = KeyDerivedDecoy.generate(&raw(b"key one"), 32).unwrap();
124        let b = KeyDerivedDecoy.generate(&raw(b"key two"), 32).unwrap();
125        assert_ne!(a, b);
126    }
127
128    #[test]
129    fn empty_key_is_accepted() {
130        // BLAKE3 of any input (including empty) is well-defined. Unlike
131        // SelfReferenceDecoy this strategy does not need at least one source
132        // byte.
133        let out = KeyDerivedDecoy.generate(&raw(&[]), 32).unwrap();
134        assert_eq!(out.len(), 32);
135    }
136
137    #[test]
138    fn describe_returns_key_derived() {
139        assert_eq!(KeyDerivedDecoy.describe(), "key-derived");
140    }
141}