bitlocker

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: BSD-3-Clause Imports: 16 Imported by: 0

README

bitlocker

Read BitLocker (BDE) encrypted volumes in pure Go: unlock with a password, a recovery password, a startup key (.BEK file) or a clear key, and read the decrypted volume through an io.ReaderAt.

dev, err := bitlocker.Open("volume.img", []byte("password"))
if err != nil {
	log.Fatal(err)
}
defer dev.Close()

boot := make([]byte, 512)
_, err = dev.ReadAt(boot, 0) // the NTFS / exFAT / FAT boot sector

OpenFrom(r, size, secret, limits) reads from any io.ReaderAt (a partition, a QCOW2 disk), and ReadInfo(r, size) reads the metadata without a secret: GUID, method, protectors, description, the same fields as cryptsetup bitlkDump.

What it reads

Volumes Windows 7 and later, BitLocker To Go
Methods AES-CBC 128/256 with and without the Elephant diffuser, AES-XTS 128/256
Sectors 512 bytes and 4 KiB
Protectors password, recovery password, startup key (.BEK), clear key (suspended protection)

The secret is interpreted the way cryptsetup interprets it: as a password, as a recovery password when it has the 48-digit shape, as a .BEK file for startup-key protectors. A clear-key protector opens with any secret, nil included.

Refused with ErrUnsupported (their metadata still reads): Windows Vista metadata, encrypt-on-write ("used space only") volumes, and volumes in the middle of an encryption or decryption. These are the volumes cryptsetup does not activate either. TPM and smart-card protectors need hardware. Writing is not implemented: WriteAt returns ErrReadOnly.

How it is checked

The decrypted device is the one cryptsetup bitlkOpen maps, byte for byte: the first sectors are read from their encrypted copy, and the three metadata areas and that copy's location read as zeros.

The tests open the 21 volumes in cryptsetup's tests/bitlk-images.tar.xz, which Windows made. Each volume is opened with every secret its images.conf lists: password, Unicode password, one or two recovery passwords, startup key, clear key. The test then compares:

  • the SHA-256 of the whole decrypted device with cryptsetup's;
  • the file system UUID with blkid's;
  • every protector GUID with the reference.

The two volumes cryptsetup refuses (partially encrypted, encrypt-on-write) must be refused here as well. The archive is GPL-2.0+, so it is fetched at test time (pinned by commit and SHA-256), never committed:

BITLK_IMAGES=/path/to/bitlk-images.tar.xz go test ./...

Without it the interoperability test skips and says so. Unit tests write their own volumes to reach every refusal. CI runs with the archive and requires 100% coverage.

Untrusted images

  • Every offset and size the image declares is checked against the device. Regions that leave the device, are not sector-aligned, or overlap are refused.
  • A metadata copy is used only if it passes its CRC-32. Once the volume master key is open, the metadata is also authenticated: the AES-CCM validation record must hold the SHA-256 of the metadata block.
  • The key derivation is fixed by the format (2²⁰ rounds of SHA-256, about 0.1 s), so an image cannot make one derivation cost more. It can list many protectors, so Limits.MaxKeyDerivations (16 by default) bounds their number and returns ErrLimit beyond it.
  • The parsers are fuzzed (FuzzOpen, FuzzStartupKey, FuzzRecoveryPassword), and govulncheck runs in CI.

Performance

The number of allocations per ReadAt call does not depend on its length: two for XTS, three for CBC (the third holds the per-sector IVs). A sector-aligned read decrypts in the caller's buffer.

Each run of sectors goes to the cipher in a single call:

  • XTS calls go-encryptions/xts's DecryptSectors, which encrypts the tweaks of 8 sectors at a time.
  • CBC uses go-encryptions/cbc. One EncryptECB call produces the run's eboiv IVs, and one DecryptSectors call decrypts the run, 8 blocks at a time.

The table below was measured on one Apple M4 Max core, reading 1 MiB per call from 512-byte-sector volumes. The figures come from abbench --max-load 8, 6 rounds interleaved with v0.1.1, all p=0.002. The reference is OpenSSL 3.6.5 on the same core (openssl speed -seconds 2 -bytes 512 [-decrypt] -evp …). Each ReadAt also copies the megabyte out of the in-memory test device; OpenSSL's figure does not include such a copy.

Method v0.1.1 this version OpenSSL
AES-128-XTS 7.33 GB/s 11.8 GB/s 12.1 GB/s the xts kernel alone is ahead of OpenSSL (go-encryptions/xts v0.2.0)
AES-256-XTS 6.19 GB/s 10.0 GB/s —
AES-128-CBC 1.97 GB/s 13.0 GB/s 17.6 GB/s the cbc kernel alone is at 104 % of OpenSSL (go-encryptions/cbc v0.1.0)
AES-128-CBC + Elephant 0.38 GB/s 0.46 GB/s no equivalent the diffuser dominates; dm-crypt's is kernel C, not measurable on macOS
Key derivation (2²⁰ SHA-256) 67 ms 67 ms ≈ 100 ms (2²⁰ × openssl speed sha256 at 64 bytes)

References

  • cryptsetup lib/bitlk/bitlk.c: metadata, key chain and the device-mapper layout this package reproduces.
  • Linux drivers/md/dm-crypt.c: eboiv and the Elephant diffuser, ported index for index.
  • libbde format documentation and dislocker.

Licence

BSD-3-Clause. The test corpus is cryptsetup's (GPL-2.0+) and is not part of this repository.

Documentation

Overview

Package bitlocker reads BitLocker (BDE) encrypted volumes in pure Go.

It unlocks a volume made by Windows 7 or later, including BitLocker To Go, with a password, a recovery password, a startup key (.BEK file) or a clear key, and exposes the decrypted volume as an io.ReaderAt. All six methods are read: AES-CBC 128/256 with and without the Elephant diffuser, and AES-XTS 128/256, on 512-byte and 4 KiB sectors.

The decrypted device is the one cryptsetup's bitlkOpen maps, byte for byte: the original first sectors are read from their encrypted copy, the three metadata areas and that copy's location read as zeros. The tests compare SHA-256 sums with cryptsetup's on volumes Windows made.

Usage

dev, err := bitlocker.Open("/path/to/volume.img", []byte("password"))
if err != nil { log.Fatal(err) }
defer dev.Close()
buf := make([]byte, 512)
_, err = dev.ReadAt(buf, 0) // the NTFS or FAT boot sector

The secret is interpreted as cryptsetup interprets it: as a password for password protectors, as a recovery password ("123456-…", 48 digits) for recovery protectors when it has that shape, and as the contents of a .BEK file for startup-key protectors. A clear-key protector (a volume whose protection is suspended) opens with any secret, nil included.

Not supported

Windows Vista metadata, encrypt-on-write ("used space only") volumes and volumes in the middle of an encryption or decryption are refused with ErrUnsupported; their metadata still reads with ReadInfo. TPM and smart card protectors need hardware. Writing is not implemented: WriteAt returns ErrReadOnly.

Untrusted images

Every offset and size an image declares is checked against the device before use, the metadata is authenticated with the volume master key (AES-CCM, then SHA-256 of the metadata block), and Limits bounds how many 2^20-round key derivations an image with many protectors can ask for.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotBitLocker means the volume header carries neither BitLocker
	// signature: the device is not a BitLocker volume.
	ErrNotBitLocker = errors.New("bitlocker: not a BitLocker volume")
	// ErrCorrupt wraps every refusal of a malformed or inconsistent image.
	ErrCorrupt = errors.New("bitlocker: corrupt volume")
	// ErrUnsupported is a well-formed volume this package does not open:
	// Windows Vista (version 1) metadata, encrypt-on-write ("used space
	// only") volumes, volumes being encrypted or decrypted, unknown methods.
	ErrUnsupported = errors.New("bitlocker: unsupported volume")
	// ErrWrongSecret means no protector of the volume opened with the
	// secret given.
	ErrWrongSecret = errors.New("bitlocker: no protector opened with this secret")
	// ErrLimit means the volume asked for more key derivations than the
	// Limits allow before one opened.
	ErrLimit = errors.New("bitlocker: limit exceeded")
	// ErrReadOnly is returned by WriteAt: this version only reads.
	ErrReadOnly = errors.New("bitlocker: read-only")
)

Errors. Every error this package returns for a malformed image wraps ErrCorrupt; the others are distinct and work with errors.Is.

View Source
var DefaultLimits = Limits{MaxKeyDerivations: 16}

DefaultLimits allows 16 key derivations: Windows writes one per password or recovery password, and a volume rarely has more than three.

Functions

func Detect

func Detect(r io.ReaderAt) bool

Detect reports whether r begins with a BitLocker volume header. It reads 512 bytes and never returns an error for a short or unreadable device: such a device is not BitLocker.

Types

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is an unlocked BitLocker volume. ReadAt is safe for concurrent use.

func Open

func Open(path string, secret []byte) (*Device, error)

Open opens the BitLocker volume at path read-only and unlocks it with secret. A block device's size is not what Stat reports on every system: open one with OpenFrom and its size.

func OpenFrom

func OpenFrom(r ReadAtCloser, size int64, secret []byte, lim Limits) (*Device, error)

OpenFrom unlocks the BitLocker volume of size bytes in r with secret. Close on the returned Device closes r.

func (*Device) Close

func (d *Device) Close() error

Close closes the underlying reader.

func (*Device) Info

func (d *Device) Info() *Info

Info returns the volume's metadata.

func (*Device) ReadAt

func (d *Device) ReadAt(p []byte, off int64) (int, error)

ReadAt reads the decrypted volume at off.

func (*Device) Size

func (d *Device) Size() int64

Size is the length of the decrypted volume in bytes.

func (*Device) WriteAt

func (d *Device) WriteAt(p []byte, off int64) (int, error)

WriteAt is not implemented.

type Info

type Info struct {
	ToGo        bool
	Kind        Kind
	SectorSize  int
	VolumeSize  uint64 // the size the metadata records
	GUID        string // the volume identifier
	Method      Method
	Description string
	Protectors  []Protector
	// Normal reports that the volume is fully encrypted and not being
	// converted (current and next state both "encrypted").
	Normal bool
	// MetadataOffsets are the three 64 KiB metadata areas.
	MetadataOffsets [3]uint64
	// HeaderOffset and HeaderSize locate the encrypted copy of the original
	// first sectors of the volume.
	HeaderOffset uint64
	HeaderSize   uint64
	// contains filtered or unexported fields
}

Info is what the metadata says, readable without any secret.

func ReadInfo

func ReadInfo(r io.ReaderAt, size int64) (*Info, error)

ReadInfo reads the metadata of the BitLocker volume in r, of size bytes, without any secret: what cryptsetup bitlkDump prints.

type Kind

type Kind int

Kind is the encryption kind the volume header names.

const (
	// Normal is a volume whose every sector is encrypted.
	Normal Kind = iota
	// EncryptOnWrite is a "used space only" volume: free space was never
	// encrypted. Its metadata reads; it does not open.
	EncryptOnWrite
	// UnknownKind is any other identifier in the volume header.
	UnknownKind
)

func (Kind) String

func (k Kind) String() string

type Limits

type Limits struct {
	// MaxKeyDerivations is how many protectors may run the key derivation
	// (2^20 SHA-256 each, about 0.1 s) before Open gives up with ErrLimit.
	MaxKeyDerivations int
}

Limits bounds the work an untrusted image can ask for.

type Method

type Method uint16

Method is the volume's encryption method.

const (
	AES128CBCElephant Method = 0x8000
	AES256CBCElephant Method = 0x8001
	AES128CBC         Method = 0x8002
	AES256CBC         Method = 0x8003
	AES128XTS         Method = 0x8004
	AES256XTS         Method = 0x8005
)

The six methods (cryptsetup BITLK_read_sb; libbde "Encryption methods").

func (Method) KeyBits

func (m Method) KeyBits() int

KeyBits is the length of the volume key, in bits, as cryptsetup counts it (the Elephant key included).

func (Method) String

func (m Method) String() string

String is cryptsetup's cipher-mode spelling.

type Protection

type Protection uint16

Protection is how a volume master key protector is unlocked.

const (
	ClearKey         Protection = 0x0000
	TPM              Protection = 0x0100
	StartupKey       Protection = 0x0200
	TPMAndPIN        Protection = 0x0500
	RecoveryPassword Protection = 0x0800
	SmartCard        Protection = 0x1000
	Password         Protection = 0x2000
)

The protections Windows writes (cryptsetup get_vmk_protection).

func (Protection) String

func (p Protection) String() string

type Protector

type Protector struct {
	GUID       string
	Protection Protection
	// contains filtered or unexported fields
}

Protector is one volume master key protector.

type ReadAtCloser

type ReadAtCloser interface {
	io.ReaderAt
	io.Closer
}

ReadAtCloser is what OpenFrom reads the encrypted volume from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL