key_vault/decoy/self_reference.rs
1//! [`SelfReferenceDecoy`] — decoy bytes sampled from the key itself.
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 draws bytes from the real key at random positions.
13///
14/// **Threat profile.** `SelfReferenceDecoy` is the **strongest** of the three
15/// built-in strategies and the recommended default. The decoy bytes are
16/// literally drawn from the key's own byte distribution, so any statistical
17/// analysis of memory regions (byte-value histogram, entropy estimate,
18/// chi-squared distinguisher) will report identical profiles for real
19/// fragments and decoy fragments. An attacker has no statistical signal to
20/// separate them.
21///
22/// The only way for an attacker to recover the key, given this strategy, is
23/// to (a) obtain the position map (separately mlock'd) and (b) reverse the
24/// fragmentation. Statistical attacks alone do not work.
25///
26/// # Why not just shuffle key bytes?
27///
28/// Sampling with replacement (which is what we do) is important: shuffling
29/// would still preserve the multiset of key bytes, and a long contiguous
30/// match would reveal a chunk boundary. With independent sampling, the
31/// decoy bytes match the key's byte *distribution* without containing any
32/// contiguous run of key bytes long enough to be confirmed.
33///
34/// # Examples
35///
36/// ```
37/// use key_vault::decoy::{DecoyStrategy, SelfReferenceDecoy};
38/// use key_vault::RawKey;
39///
40/// let key = RawKey::new(vec![0xa1, 0xb2, 0xc3, 0xd4, 0xe5]);
41/// let decoy = SelfReferenceDecoy.generate(&key, 32).unwrap();
42/// assert_eq!(decoy.len(), 32);
43/// // Every decoy byte is drawn from the key's byte set.
44/// for b in &decoy {
45/// assert!([0xa1, 0xb2, 0xc3, 0xd4, 0xe5].contains(b));
46/// }
47/// ```
48#[derive(Debug, Default, Clone, Copy)]
49pub struct SelfReferenceDecoy;
50
51impl DecoyStrategy for SelfReferenceDecoy {
52 fn generate(&self, key: &RawKey, output_len: usize) -> Result<Vec<u8>> {
53 if output_len == 0 {
54 return Ok(Vec::new());
55 }
56 let key_bytes = key.as_bytes();
57 if key_bytes.is_empty() {
58 return Err(Error::Decoy(alloc::string::ToString::to_string(
59 "self-reference decoy requires a non-empty key",
60 )));
61 }
62
63 // Fetch the index randomness in one syscall instead of one per byte.
64 // We use four bytes of CSPRNG output per decoy byte (more than enough
65 // entropy to index into a key of any practical size).
66 let rand_byte_count = output_len.saturating_mul(4);
67 let mut rand_buf = vec![0u8; rand_byte_count];
68 getrandom::getrandom(&mut rand_buf).map_err(|_| Error::Internal("OS RNG failed"))?;
69
70 let key_len = key_bytes.len();
71 let mut out = Vec::with_capacity(output_len);
72 for i in 0..output_len {
73 let raw: [u8; 4] = rand_buf[i * 4..i * 4 + 4]
74 .try_into()
75 .map_err(|_| Error::Internal("rand buffer slice did not size to u32"))?;
76 // Modulo bias against `key_len` is at worst 2^-32 / key_len; for
77 // any practical key length this is negligible.
78 let idx = (u32::from_le_bytes(raw) as usize) % key_len;
79 out.push(key_bytes[idx]);
80 }
81
82 // Scrub the temporary randomness buffer.
83 for b in &mut rand_buf {
84 // SAFETY-equivalent: we still own rand_buf and the slice is
85 // valid; volatile-zero defeats dead-store elimination.
86 //
87 // Note: we are not inside an `unsafe` block — `write_volatile`
88 // is unsafe but we are taking the safe `iter_mut` path here for
89 // clarity. To actually defeat dead-store elimination we need
90 // the volatile write; see below.
91 let _ = b;
92 }
93 // Real volatile-zero pass for the randomness buffer.
94 // SAFETY: rand_buf points to a valid `rand_byte_count`-element
95 // allocation we just constructed; we write within bounds.
96 unsafe {
97 let ptr = rand_buf.as_mut_ptr();
98 for i in 0..rand_buf.len() {
99 core::ptr::write_volatile(ptr.add(i), 0u8);
100 }
101 }
102 core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
103 drop(rand_buf);
104
105 Ok(out)
106 }
107
108 fn describe(&self) -> Cow<'_, str> {
109 Cow::Borrowed("self-reference")
110 }
111}
112
113#[cfg(test)]
114#[allow(
115 clippy::unwrap_used,
116 clippy::expect_used,
117 clippy::cast_possible_truncation,
118 clippy::cast_sign_loss
119)]
120mod tests {
121 use super::*;
122 use alloc::collections::BTreeSet;
123
124 fn raw(bytes: &[u8]) -> RawKey {
125 RawKey::new(bytes.to_vec())
126 }
127
128 #[test]
129 fn produces_requested_length() {
130 let key = raw(&[1, 2, 3, 4, 5]);
131 for n in [0usize, 1, 7, 32, 256, 4096] {
132 let out = SelfReferenceDecoy.generate(&key, n).unwrap();
133 assert_eq!(out.len(), n, "wrong length for n = {n}");
134 }
135 }
136
137 #[test]
138 fn every_byte_is_drawn_from_the_key() {
139 let key_bytes: alloc::vec::Vec<u8> = (0u8..16).collect();
140 let key = raw(&key_bytes);
141 let allowed: BTreeSet<u8> = key_bytes.iter().copied().collect();
142
143 let out = SelfReferenceDecoy.generate(&key, 1024).unwrap();
144 for (i, b) in out.iter().enumerate() {
145 assert!(
146 allowed.contains(b),
147 "decoy byte {b:#04x} at index {i} not in key"
148 );
149 }
150 }
151
152 #[test]
153 fn empty_key_is_rejected() {
154 let key = raw(&[]);
155 let err = SelfReferenceDecoy.generate(&key, 16).unwrap_err();
156 assert!(matches!(err, Error::Decoy(_)));
157 }
158
159 #[test]
160 fn two_calls_with_same_key_produce_different_outputs() {
161 let key = raw(b"a key with reasonable length");
162 let a = SelfReferenceDecoy.generate(&key, 64).unwrap();
163 let b = SelfReferenceDecoy.generate(&key, 64).unwrap();
164 // With independent sampling from a 28-byte key the chance of two
165 // 64-byte outputs matching is (1/28)^64 — effectively zero.
166 assert_ne!(a, b);
167 }
168
169 #[test]
170 fn describe_returns_self_reference() {
171 assert_eq!(SelfReferenceDecoy.describe(), "self-reference");
172 }
173}