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}