Files
Matthew Jackson b8f0af9ef5 1.3.1: relicense to MIT (clean-room CSS + drop copyleft-lib references)
Relicensed from AGPL-3.0 to MIT, effective 1.3.1 (<=1.3.0 remain AGPL). The CSS
content cipher and Stevenson title-key attack are attributed to their published
cryptanalysis (not libdvdcss); all libaacs/libbluray/libdvdread/libdvdnav name
references were dropped from comments while keeping the standard format/spec
descriptions. Also bumps to 1.3.1.
2026-07-10 12:31:19 -07:00

8.8 KiB

libfreemkv Architecture

Open source optical drive access library for 4K UHD Blu-ray, Blu-ray, and DVD. Rust library with profiles bundled and all SCSI communication handled in-process. AACS decryption requires an external keydb.cfg (default ~/.config/freemkv/keydb.cfg) — the derivation math is internal, but no AACS key material is compiled in; DVD CSS player keys are the only compiled-in keys.

Repository: https://github.com/freemkv/libfreemkv License: MIT


Design Principles

  1. CLI is dumb. All drive communication, disc parsing, AACS decryption, and format handling live in the library. CLI binaries are thin wrappers that call Drive::open() and Disc::scan().

  2. Firmware-clean core. libfreemkv ships no firmware, no unlock CDBs, and no drive profiles. Drive-unlock logic is plugged in by an external crate through the Unlocker trait + registry (register_unlocker); without one the library still rips via the host-certificate AACS handshake.

  3. Transparent AACS. The ContentReader decrypts on the fly when keys are available. Callers read cleartext sectors without knowing whether the disc was encrypted.

  4. Structured errors, no English. Every error has a numeric code (E1000-E8000). The library never formats user-facing messages -- applications do that.

  5. Library-agnostic. No concept of "supported" vs "unsupported" drives at a policy level. If a profile exists, the library uses it.

  6. Streams are dumb pipes. Streams read/write PES frames. They don't know about encryption, transport format, or source type. Decrypt is a stream-internal concern; the pipeline just moves frames.


Module Map

libfreemkv (lib.rs)
│
├── Drive Access
│   ├── drive         Drive — open, identify, init, single-shot read
│   ├── scsi          ScsiTransport trait + platform backends (sg async, IOKit, SPTI)
│   ├── unlock        Unlocker trait + registry — the pluggable unlock seam
│   ├── identity      DriveId from INQUIRY + GET_CONFIG 010C
│   ├── speed         DriveSpeed enum, SET CD SPEED CDB builder
│   └── event         Event system for drive status callbacks
│
├── Disc Scanning
│   ├── disc          Disc::scan() — titles, streams, extents, AACS setup
│   ├── udf           UDF 2.50 filesystem reader (metadata partitions)
│   ├── mpls          MPLS playlist parser — clips, streams, STN table
│   ├── clpi          CLPI clip info parser — EP map, sector extents
│   ├── ifo           DVD IFO parser — title sets, PGC chains, cell addresses
│   └── labels/       BD-J label extraction (5 formats: Paramount, Criterion, Pixelogic, CTRM, Deluxe)
│
├── Encryption
│   ├── aacs/         AACS handshake, KEYDB, VUK lookup, MKB, unit decryption
│   ├── css           DVD CSS cipher — table-driven, no external keys needed
│   └── decrypt       decrypt_sectors() — unified AACS/CSS/None dispatcher
│
├── Streaming
│   ├── mux/          Stream implementations (Disc, ISO, MKV, M2TS, Network, Stdio, Null)
│   ├── pes           PES frame types; the unified pes::Stream (PesStream) read/write trait
│   └── sector/       SectorSource / SectorSink traits, FileSector{Source,Sink}, DecryptingSectorSource
│
├── I/O Primitives
│   ├── halt          Halt cancellation token (one Arc<AtomicBool>, cloneable)
│   └── io/           Pipeline<I, R> + Sink trait + WritebackFile (bounded-cache writer)
│
├── Support
│   ├── keydb         KEYDB.cfg download, parse, verify, save
│   └── error         Error enum with numeric codes E1000-E8000
│
└── lib.rs            Public API re-exports

Drive Access Flow

Drive::open(Path::new("/dev/sg4"))
  │
  ├─ scsi::open()           Open /dev/sg4 (async write/poll/read)
  ├─ DriveId::from_drive()  INQUIRY + GET_CONFIG 010C
  └─ Drive ready for init/read

After open:

  • init() -- routes to the matching registered unlocker (if any); otherwise a no-op and the cert handshake carries the disc
  • probe_disc() -- probe disc surface for optimal speeds
  • read(lba, count, buf, recovery) -- single-shot read; recovery only selects the per-CDB timeout (1.5 s vs. 30 s)
  • wait_ready() -- wait for disc insertion
  • eject() -- eject tray

Recovery is layered above Drive::read, not inside it. Layer 1 (Disc::patch) handles bad-range retry by replaying the ddrescue mapfile. Layer 3 (DiscStream::fill_extents adaptive batch sizer) handles in-loop request-size adaptation. Inline recovery (gentle retry → SCSI reset → retry) was removed in 0.13.6 — see rip-recovery.md and the stop-wedge postmortem (2026-04-25).


Disc Scanning Flow

Disc::scan(&mut drive, &ScanOptions)
  │
  ├─ READ CAPACITY          Get disc size in sectors
  ├─ udf::read_filesystem() Parse UDF 2.50 (AVDP → VDS → metadata → FSD → root)
  ├─ For each BDMV/PLAYLIST/*.mpls:
  │   ├─ mpls::parse()      Extract play items, STN streams
  │   └─ For each clip:
  │       └─ clpi::parse()  EP map → sector extents for the clip's time range
  ├─ labels::detect()       Parse BD-J JARs for stream labels
  ├─ Detect AACS            Check for /AACS directory on disc
  └─ Disc::setup_aacs()     Handshake + KEYDB → VUK → unit keys (if encrypted)

For DVD:

Disc::scan_dvd(&mut drive, &ScanOptions)
  │
  ├─ ifo::parse()           Parse VIDEO_TS.IFO — title sets, PGC chains
  ├─ CSS detection          Check disc structure flag
  └─ CSS key cracking       Table-driven, no KEYDB needed

The result is a Disc with:

  • titles: Vec<DiscTitle> -- sorted by duration, each with streams, sector extents, codec_privates
  • decrypt_keys() -- DecryptKeys for content decryption
  • encrypted: bool -- whether the disc uses AACS/CSS

AACS Decryption

Four key resolution paths, tried in order:

Path Method Speed
1 VUK lookup by disc hash in KEYDB.cfg Instant
2 Media Key + Volume ID from KEYDB → derive VUK Fast
3 Processing Keys + MKB → Media Key → VUK Medium
4 Device Keys + MKB subset-difference tree → VUK Slow

The AACS handshake (aacs/handshake) performs ECDH key agreement over the AACS 1.0 160-bit elliptic curve to obtain:

  • Volume ID -- needed for VUK derivation (paths 2-4)
  • Read Data Key -- needed for AACS 2.0 (UHD) bus decryption

Content decryption uses AES-128-CBC on 6144-byte aligned units. The ContentReader handles this transparently. Streams that read sectors (DiscStream, IsoStream) decrypt internally — the pipeline sees clean bytes.


Error Codes

All errors carry a numeric code for programmatic handling. No user-facing text is baked into the library.

Range Category Examples
E1xxx Device errors DeviceNotFound, DevicePermission
E2xxx Profile errors UnsupportedDrive, ProfileNotFound, ProfileParse
E3xxx Unlock errors UnlockFailed, SignatureMismatch, NotUnlocked, NotCalibrated
E4xxx SCSI errors ScsiError, ScsiTimeout
E5xxx I/O errors IoError (wraps std::io::Error)
E6xxx Disc format errors DiscError (UDF, MPLS, CLPI parse failures)
E7xxx AACS errors AacsError (key resolution, handshake, decryption)
E8xxx KEYDB errors KeydbError (download, parse, save)

Platform Support

Platform Transport Status
Linux async sg write/poll/read on /dev/sg* Supported
macOS IOKit SCSITask Supported
Windows SPTI (IOCTL_SCSI_PASS_THROUGH_DIRECT) Supported

The ScsiTransport trait abstracts the platform. Adding a new platform requires implementing execute() for that OS and wiring it into scsi::open().


Drive Unlock

libfreemkv carries no drive-unlock mechanism. The Unlocker trait + registry (src/unlock.rs) is the seam: an external crate implements Unlocker and registers it once via register_unlocker(...). At drive-prep the registry is walked in order and the first unlocker whose matches() is true is asked to unlock_drive() over the raw ScsiTransport. If none match, the drive is left untouched and the host-certificate AACS handshake carries the disc.

The implementor owns everything firmware-specific — drive profiles, vendor CDBs, variant logic. Concrete unlockers live in the separate freemkv-unlock repository, never in libfreemkv. See drive-access.md for the trait definition and routing.


Build

cargo build --release

Produces a Rust library crate. The libc dependency is unix-only (gated). All three platforms build and pass CI.