Skip to main content

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}