# bitlocker-forensic
[](https://crates.io/crates/bitlocker-core)
[](https://crates.io/crates/bitlocker-forensic)
[](https://docs.rs/bitlocker-core)
[](https://www.rust-lang.org)
[](LICENSE)
[](https://github.com/sponsors/h4x0r)
[](https://github.com/SecurityRonin/bitlocker-forensic/actions/workflows/ci.yml)
[](docs/validation.md)
[](https://github.com/rust-secure-code/safety-dance)
[](https://rustsec.org)
**Unlock a BitLocker volume from its password and read the plaintext — a
from-scratch, pure-Rust BitLocker (BDE) decryptor, validated byte-for-byte
against `pybde` on real disk images.**
No `dislocker` C dependency, no FUSE, no mounting: one library that parses the
FVE metadata, derives the keys from a password or recovery password, and
decrypts sectors — AES-CBC (± Elephant Diffuser) and AES-XTS, 128- and 256-bit.
```rust,ignore
use std::fs::File;
use bitlocker::BitLockerVolume;
// Unlock the dfvfs BitLocker To Go test image with its published password.
let mut vol = BitLockerVolume::unlock_with_password(File::open("bdetogo.raw")?, "bde-TEST")?;
let mut boot = [0u8; 512];
vol.read_at(0, &mut boot)?; // decrypted FAT boot sector
assert_eq!(&boot[3..11], b"MSWIN4.1");
# Ok::<(), Box<dyn std::error::Error>>(())
```
## Scope
This build unlocks the **password** (`0x2000`) and **recovery-password**
(`0x0800`) protectors and decrypts **five of the six** BitLocker ciphers, each
validated against a `pybde` oracle:
| `0x8000` | AES-128-CBC + Elephant Diffuser | dfvfs `bdetogo.raw` (Tier-1) |
| `0x8002` | AES-128-CBC | picoCTF 2025 `bitlocker-1.dd` (Tier-1) |
| `0x8003` | AES-256-CBC | self-minted `m8003` (Tier-2) |
| `0x8004` | XTS-AES-128 | BelkaCTF6 `vault` (Tier-1) + `m8004` (Tier-2) |
| `0x8005` | XTS-AES-256 | self-minted `m8005` (Tier-2) |
The dispatch decodes all six ciphers (`0x8000`–`0x8005`) into their axes and
ships a decrypt for a cipher only once a real oracle validates it. The remaining
method, AES-256-CBC + Elephant Diffuser (`0x8001`), is **recognized and refused
with a named error** — never decrypted by construction — so it lights up as a
one-line change plus a test the moment it gets an oracle. Startup-key and TPM
protectors are out of scope for *unlock*, but the metadata parser still
**reports** every protector and cipher it finds. See
[`docs/RESEARCH.md`](docs/RESEARCH.md).
## The two-crate split
Following the fleet reader/analyzer standard:
| **`bitlocker-core`** | reader / decryptor (`aes` · `cbc` · `ccm` · `xts-mode` · `sha2`) | plaintext `Read + Seek` view + typed FVE metadata |
| **`bitlocker-forensic`** | anomaly analyzer over the metadata | graded `forensicnomicon::report` `Finding`s |
### Analyzer findings
| `BDE-CLEAR-KEY-PRESENT` | High | a clear-key protector (`0x0000`) is present ⇒ the volume is effectively unencrypted |
| `BDE-PROTECTOR-INVENTORY` | Info | one per protector (password / recovery / TPM / startup key / …) |
| `BDE-WEAK-CIPHER` | Low | AES-CBC ± diffuser is weaker than AES-XTS — consistent with an older OS |
| `BDE-TO-GO` | Info | a BitLocker To Go removable-media volume |
Findings are **observations, never verdicts** — the examiner draws conclusions.
## Trust but verify
- **Every primitive is an audited RustCrypto crate** (`aes`, `cbc`, `ccm`,
`sha2`). The only hand-written cryptographic routine is the **Elephant
Diffuser** — no crate exists for it — implemented to the `libbde` reference and
validated **only** against the independent `pybde` oracle on the real
`bdetogo.raw` image, never a self-authored round-trip.
- **Panic-free, bounds-checked** parsing of untrusted volumes; `unwrap`/`expect`
denied in production code (`#![forbid(unsafe_code)]`); the metadata parser is
fuzzed.
- **Tier-1 validated**: decrypted sectors match `pybde` byte-for-byte — see
[`docs/validation.md`](docs/validation.md).
[Privacy Policy](https://securityronin.github.io/bitlocker-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/bitlocker-forensic/terms/) · © 2026 Security Ronin Ltd