Skip to main content

elephant_diffuser/
lib.rs

1//! # elephant-diffuser — the BitLocker Elephant Diffuser
2//!
3//! The BitLocker Elephant Diffuser as a standalone, dependency-free primitive:
4//! Diffuser A, Diffuser B, and the per-sector-key XOR that together form the
5//! diffuser stage of BitLocker's CBC-plus-diffuser sector cipher (encryption
6//! methods `0x8000` and `0x8001`).
7//!
8//! The diffuser is **not** a cipher and holds no secret — it is a keyed,
9//! invertible byte-mixing transform applied to a sector *after* AES-CBC
10//! decryption (and *before* AES-CBC encryption), spreading each bit across the
11//! whole sector. Every other primitive in BitLocker's cipher (AES, CBC, CCM,
12//! SHA-256) has an audited RustCrypto crate; the Elephant Diffuser does not, so
13//! it is the one documented exception to "never hand-roll crypto." The rotation
14//! constants and cycle order follow the `dislocker` (`diffuser.c`) / `libbde`
15//! reference; correctness is proven in situ by `bitlocker-core`'s Tier-1
16//! `bdetogo.raw`-vs-`pybde` oracle (see `docs/validation.md`).
17//!
18//! ```
19//! let mut sector = vec![0u8; 512];
20//! let sector_key = [0u8; 32]; // caller-derived (BitLocker: AES-ECB over the offset with the TWEAK key)
21//! elephant_diffuser::decrypt(&mut sector, &sector_key);
22//! elephant_diffuser::encrypt(&mut sector, &sector_key); // exact inverse
23//! assert_eq!(sector, vec![0u8; 512]);
24//! ```
25
26#![forbid(unsafe_code)]
27#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
28
29/// Diffuser A rotation amounts (`Ra`), indexed by word position `i % 4`.
30const RA: [u32; 4] = [9, 0, 13, 0];
31/// Diffuser B rotation amounts (`Rb`), indexed by word position `i % 4`.
32const RB: [u32; 4] = [0, 10, 0, 25];
33
34/// Split a sector into little-endian 32-bit words. Trailing bytes that do not
35/// fill a word are dropped (they are XOR-mixed but not diffused, matching the
36/// reference), so the transform touches `sector.len() / 4` words.
37fn to_words(sector: &[u8]) -> Vec<u32> {
38    sector
39        .chunks_exact(4)
40        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
41        .collect()
42}
43
44/// Write words back over the sector they came from.
45fn from_words(words: &[u32], out: &mut [u8]) {
46    for (i, w) in words.iter().enumerate() {
47        // `words` was produced by `to_words(out)`, so `i*4 + 4 <= out.len()` for
48        // every `i`; the `get_mut` guard keeps this panic-free regardless.
49        if let Some(slot) = out.get_mut(i * 4..i * 4 + 4) {
50            slot.copy_from_slice(&w.to_le_bytes());
51        }
52    }
53}
54
55/// `(i - k) mod n`, computed without unsigned underflow for any `k` and `n >= 1`.
56/// For the real BitLocker case (`n >= 5`, i.e. sectors of 20+ bytes) this equals
57/// the reference `(i + n - k) % n`; it additionally stays panic-free for the
58/// 1..=4-word buffers a fuzzer can present.
59#[inline]
60fn back(i: usize, k: usize, n: usize) -> usize {
61    (i + n - (k % n)) % n
62}
63
64/// Diffuser A, decryption direction: `d[i] += d[i-2] ^ ROL(d[i-5], Ra[i%4])`,
65/// five cycles, indices ascending, modulo the word count.
66fn diffuser_a_decrypt(sector: &mut [u8]) {
67    let mut d = to_words(sector);
68    let n = d.len();
69    if n == 0 {
70        return;
71    }
72    for _ in 0..5 {
73        for i in 0..n {
74            let a = d[back(i, 2, n)];
75            let b = d[back(i, 5, n)].rotate_left(RA[i % 4]);
76            d[i] = d[i].wrapping_add(a ^ b);
77        }
78    }
79    from_words(&d, sector);
80}
81
82/// Diffuser B, decryption direction: `d[i] += d[i+2] ^ ROL(d[i+5], Rb[i%4])`,
83/// three cycles, indices ascending, modulo the word count.
84fn diffuser_b_decrypt(sector: &mut [u8]) {
85    let mut d = to_words(sector);
86    let n = d.len();
87    if n == 0 {
88        return;
89    }
90    for _ in 0..3 {
91        for i in 0..n {
92            let a = d[(i + 2) % n];
93            let b = d[(i + 5) % n].rotate_left(RB[i % 4]);
94            d[i] = d[i].wrapping_add(a ^ b);
95        }
96    }
97    from_words(&d, sector);
98}
99
100/// Diffuser A, encryption direction — the inverse of [`diffuser_a_decrypt`]
101/// (indices descending, `wrapping_sub`).
102fn diffuser_a_encrypt(sector: &mut [u8]) {
103    let mut d = to_words(sector);
104    let n = d.len();
105    if n == 0 {
106        return;
107    }
108    for _ in 0..5 {
109        for i in (0..n).rev() {
110            let a = d[back(i, 2, n)];
111            let b = d[back(i, 5, n)].rotate_left(RA[i % 4]);
112            d[i] = d[i].wrapping_sub(a ^ b);
113        }
114    }
115    from_words(&d, sector);
116}
117
118/// Diffuser B, encryption direction — the inverse of [`diffuser_b_decrypt`].
119fn diffuser_b_encrypt(sector: &mut [u8]) {
120    let mut d = to_words(sector);
121    let n = d.len();
122    if n == 0 {
123        return;
124    }
125    for _ in 0..3 {
126        for i in (0..n).rev() {
127            let a = d[(i + 2) % n];
128            let b = d[(i + 5) % n].rotate_left(RB[i % 4]);
129            d[i] = d[i].wrapping_sub(a ^ b);
130        }
131    }
132    from_words(&d, sector);
133}
134
135/// Decrypt one sector in place (the method-`0x8000` diffuser order): Diffuser B,
136/// then Diffuser A, then XOR the 32-byte sector key. The exact inverse of
137/// [`encrypt`]. Operates on a sector of any length and never panics.
138pub fn decrypt(sector: &mut [u8], sector_key: &[u8; 32]) {
139    diffuser_b_decrypt(sector);
140    diffuser_a_decrypt(sector);
141    for (i, b) in sector.iter_mut().enumerate() {
142        *b ^= sector_key[i % 32];
143    }
144}
145
146/// Encrypt one sector in place — the exact inverse of [`decrypt`]: XOR the
147/// 32-byte sector key, then Diffuser A, then Diffuser B. Operates on a sector of
148/// any length and never panics.
149pub fn encrypt(sector: &mut [u8], sector_key: &[u8; 32]) {
150    for (i, b) in sector.iter_mut().enumerate() {
151        *b ^= sector_key[i % 32];
152    }
153    diffuser_a_encrypt(sector);
154    diffuser_b_encrypt(sector);
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn hex(bytes: &[u8]) -> String {
162        use std::fmt::Write;
163        bytes.iter().fold(String::new(), |mut s, b| {
164            let _ = write!(s, "{b:02x}");
165            s
166        })
167    }
168
169    /// The same 512-byte pattern the source impl was captured against.
170    fn sample_sector() -> Vec<u8> {
171        (0..512u32)
172            .map(|i| (i.wrapping_mul(31) ^ 0xA5) as u8)
173            .collect()
174    }
175
176    /// Sector key = bytes 0x00..=0x1f.
177    fn sample_key() -> [u8; 32] {
178        let mut k = [0u8; 32];
179        for (i, b) in k.iter_mut().enumerate() {
180            *b = i as u8;
181        }
182        k
183    }
184
185    // Tier-3 regression vector captured from the Tier-1-validated bitlocker-core
186    // impl BEFORE extraction (rustc -O over the exact diffuser code). The
187    // authoritative proof is bitlocker-core's in-situ 0x8000 oracle
188    // (bdetogo.raw vs pybde) — see docs/validation.md.
189    #[test]
190    fn decrypt_matches_captured_regression_vector() {
191        let mut buf = sample_sector();
192        decrypt(&mut buf, &sample_key());
193        assert_eq!(
194            hex(&buf[..32]),
195            "9649e3f15c8ecdb6fceb5a864f24e97596052689bf414d5c3137edb27dc43c6e"
196        );
197        assert_eq!(hex(&buf[496..512]), "21eafdd00ad4826068a2d7a8f28fcf97");
198    }
199
200    #[test]
201    fn encrypt_decrypt_roundtrip_is_identity() {
202        let key = sample_key();
203        let orig = sample_sector();
204        let mut buf = orig.clone();
205        encrypt(&mut buf, &key);
206        assert_ne!(buf, orig);
207        decrypt(&mut buf, &key);
208        assert_eq!(buf, orig);
209    }
210
211    #[test]
212    fn empty_and_subword_inputs_do_not_panic() {
213        let key = sample_key();
214        let mut empty: [u8; 0] = [];
215        decrypt(&mut empty, &key);
216        encrypt(&mut empty, &key);
217        let mut three = [1u8, 2, 3]; // < 1 word after chunks_exact -> no-op
218        decrypt(&mut three, &key);
219        encrypt(&mut three, &key);
220    }
221
222    // Real BitLocker sectors are >=512 bytes (128 words), so the diffuser is only
223    // ever driven at n>=5. A fuzzer over arbitrary bytes, however, hits n in
224    // 1..=4 where a naive `(i + n - 5)` index underflows. This locks in the
225    // panic-free modular form for those word counts.
226    #[test]
227    fn tiny_sector_word_counts_do_not_panic() {
228        let key = sample_key();
229        for words in 1..=6usize {
230            let mut buf = vec![0xABu8; words * 4];
231            decrypt(&mut buf, &key);
232            let mut buf2 = vec![0xABu8; words * 4];
233            encrypt(&mut buf2, &key);
234        }
235    }
236}