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 (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}