# 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:** **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, cloneable) │ └── io/ Pipeline + 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`](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` -- 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](https://github.com/freemkv/freemkv-unlock)** repository, never in libfreemkv. See [`drive-access.md`](drive-access.md#drive-unlock-seam) 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.