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, §or_key);
22//! elephant_diffuser::encrypt(&mut sector, §or_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 from the diffused words (the per-sector-key XOR in
36/// [`decrypt`]/[`encrypt`] still covers them), so the transform touches
37/// `sector.len() / 4` words. Real BitLocker sectors are word-aligned, so a
38/// sub-word remainder only arises for out-of-spec inputs the reference never
39/// specifies.
40fn to_words(sector: &[u8]) -> Vec<u32> {
41 sector
42 .chunks_exact(4)
43 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
44 .collect()
45}
46
47/// Write words back over the sector they came from.
48fn from_words(words: &[u32], out: &mut [u8]) {
49 for (i, w) in words.iter().enumerate() {
50 // `words` was produced by `to_words(out)`, so `i*4 + 4 <= out.len()` for
51 // every `i`; the `get_mut` guard keeps this panic-free regardless.
52 if let Some(slot) = out.get_mut(i * 4..i * 4 + 4) {
53 slot.copy_from_slice(&w.to_le_bytes());
54 }
55 }
56}
57
58/// `(i - k) mod n`, computed without unsigned underflow for any `k` and `n >= 1`.
59/// For the real BitLocker case (`n >= 5`, i.e. sectors of 20+ bytes) this equals
60/// the reference `(i + n - k) % n`; it additionally stays panic-free for the
61/// 1..=4-word buffers a fuzzer can present.
62#[inline]
63fn back(i: usize, k: usize, n: usize) -> usize {
64 (i + n - (k % n)) % n
65}
66
67/// Diffuser A, decryption direction: `d[i] += d[i-2] ^ ROL(d[i-5], Ra[i%4])`,
68/// five cycles, indices ascending, modulo the word count.
69fn diffuser_a_decrypt(sector: &mut [u8]) {
70 let mut d = to_words(sector);
71 let n = d.len();
72 if n == 0 {
73 return;
74 }
75 for _ in 0..5 {
76 for i in 0..n {
77 let a = d[back(i, 2, n)];
78 let b = d[back(i, 5, n)].rotate_left(RA[i % 4]);
79 d[i] = d[i].wrapping_add(a ^ b);
80 }
81 }
82 from_words(&d, sector);
83}
84
85/// Diffuser B, decryption direction: `d[i] += d[i+2] ^ ROL(d[i+5], Rb[i%4])`,
86/// three cycles, indices ascending, modulo the word count.
87fn diffuser_b_decrypt(sector: &mut [u8]) {
88 let mut d = to_words(sector);
89 let n = d.len();
90 if n == 0 {
91 return;
92 }
93 for _ in 0..3 {
94 for i in 0..n {
95 let a = d[(i + 2) % n];
96 let b = d[(i + 5) % n].rotate_left(RB[i % 4]);
97 d[i] = d[i].wrapping_add(a ^ b);
98 }
99 }
100 from_words(&d, sector);
101}
102
103/// Diffuser A, encryption direction — the inverse of [`diffuser_a_decrypt`]
104/// (indices descending, `wrapping_sub`).
105fn diffuser_a_encrypt(sector: &mut [u8]) {
106 let mut d = to_words(sector);
107 let n = d.len();
108 if n == 0 {
109 return;
110 }
111 for _ in 0..5 {
112 for i in (0..n).rev() {
113 let a = d[back(i, 2, n)];
114 let b = d[back(i, 5, n)].rotate_left(RA[i % 4]);
115 d[i] = d[i].wrapping_sub(a ^ b);
116 }
117 }
118 from_words(&d, sector);
119}
120
121/// Diffuser B, encryption direction — the inverse of [`diffuser_b_decrypt`].
122fn diffuser_b_encrypt(sector: &mut [u8]) {
123 let mut d = to_words(sector);
124 let n = d.len();
125 if n == 0 {
126 return;
127 }
128 for _ in 0..3 {
129 for i in (0..n).rev() {
130 let a = d[(i + 2) % n];
131 let b = d[(i + 5) % n].rotate_left(RB[i % 4]);
132 d[i] = d[i].wrapping_sub(a ^ b);
133 }
134 }
135 from_words(&d, sector);
136}
137
138/// Decrypt one sector in place (the method-`0x8000` diffuser order): Diffuser B,
139/// then Diffuser A, then XOR the 32-byte sector key. The exact inverse of
140/// [`encrypt`]. Operates on a sector of any length and never panics.
141pub fn decrypt(sector: &mut [u8], sector_key: &[u8; 32]) {
142 diffuser_b_decrypt(sector);
143 diffuser_a_decrypt(sector);
144 for (i, b) in sector.iter_mut().enumerate() {
145 *b ^= sector_key[i % 32];
146 }
147}
148
149/// Encrypt one sector in place — the exact inverse of [`decrypt`]: XOR the
150/// 32-byte sector key, then Diffuser A, then Diffuser B. Operates on a sector of
151/// any length and never panics.
152pub fn encrypt(sector: &mut [u8], sector_key: &[u8; 32]) {
153 for (i, b) in sector.iter_mut().enumerate() {
154 *b ^= sector_key[i % 32];
155 }
156 diffuser_a_encrypt(sector);
157 diffuser_b_encrypt(sector);
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn hex(bytes: &[u8]) -> String {
165 use std::fmt::Write;
166 bytes.iter().fold(String::new(), |mut s, b| {
167 let _ = write!(s, "{b:02x}");
168 s
169 })
170 }
171
172 /// The same 512-byte pattern the source impl was captured against.
173 fn sample_sector() -> Vec<u8> {
174 (0..512u32)
175 .map(|i| (i.wrapping_mul(31) ^ 0xA5) as u8)
176 .collect()
177 }
178
179 /// Sector key = bytes 0x00..=0x1f.
180 fn sample_key() -> [u8; 32] {
181 let mut k = [0u8; 32];
182 for (i, b) in k.iter_mut().enumerate() {
183 *b = i as u8;
184 }
185 k
186 }
187
188 // Tier-3 regression vector captured from the Tier-1-validated bitlocker-core
189 // impl BEFORE extraction (rustc -O over the exact diffuser code). The
190 // authoritative proof is bitlocker-core's in-situ 0x8000 oracle
191 // (bdetogo.raw vs pybde) — see docs/validation.md.
192 #[test]
193 fn decrypt_matches_captured_regression_vector() {
194 let mut buf = sample_sector();
195 decrypt(&mut buf, &sample_key());
196 assert_eq!(
197 hex(&buf[..32]),
198 "9649e3f15c8ecdb6fceb5a864f24e97596052689bf414d5c3137edb27dc43c6e"
199 );
200 assert_eq!(hex(&buf[496..512]), "21eafdd00ad4826068a2d7a8f28fcf97");
201 }
202
203 #[test]
204 fn encrypt_decrypt_roundtrip_is_identity() {
205 let key = sample_key();
206 let orig = sample_sector();
207 let mut buf = orig.clone();
208 encrypt(&mut buf, &key);
209 assert_ne!(buf, orig);
210 decrypt(&mut buf, &key);
211 assert_eq!(buf, orig);
212 }
213
214 #[test]
215 fn empty_and_subword_inputs_do_not_panic() {
216 let key = sample_key();
217 let mut empty: [u8; 0] = [];
218 decrypt(&mut empty, &key);
219 encrypt(&mut empty, &key);
220 let mut three = [1u8, 2, 3]; // < 1 word after chunks_exact -> no-op
221 decrypt(&mut three, &key);
222 encrypt(&mut three, &key);
223 }
224
225 // Real BitLocker sectors are >=512 bytes (128 words), so the diffuser is only
226 // ever driven at n>=5. A fuzzer over arbitrary bytes, however, hits n in
227 // 1..=4 where a naive `(i + n - 5)` index underflows. This locks in the
228 // panic-free modular form for those word counts.
229 #[test]
230 fn tiny_sector_word_counts_do_not_panic() {
231 let key = sample_key();
232 for words in 1..=6usize {
233 let mut buf = vec![0xABu8; words * 4];
234 decrypt(&mut buf, &key);
235 let mut buf2 = vec![0xABu8; words * 4];
236 encrypt(&mut buf2, &key);
237 }
238 }
239}