v0.25.9: built-in AACS keys + plugin slot + MKB record-type fix
Two changes that make AACS 1.0 / DVD self-sufficient: 1. MKB record-type identification bug fix. `mkb_find_mk_dv` was searching for type 0x10 (which is Type-and-Version, 12 bytes) when the Verify Media Key Record is actually type 0x81 for AACS 1.0 or type 0x86 for AACS 2.0/2.1. `mkb_version` had the inverse bug. PK and DK derivation paths therefore silently failed on every disc, masking how often the fallback paths could have worked. Fix searches the correct types; tests added covering both the 0x81 and 0x86 verify-record forms and the 0x10 version record at offset 8 of the body. 2. Built-in AACS keys + operator plugin slot. Four device keys (covering MKB v01-v82+) and three processing keys (covering v63-v68) compiled directly into the library. Combined with the 31 CSS player keys already in css/auth.rs, DVDs and Blu-rays (AACS 1.0) now decrypt with zero external files. New plugin path at ~/.config/freemkv/local_keys.cfg (same syntax as keydb.cfg) layered additively on top of built-ins and main keydb. `Disc::scan` no longer errors when keydb.cfg is absent; AACS 2.0 / UHD still surfaces a specific error when the disc needs keys none of the layers provide. Public docstrings in project docs + README updated to describe the three additive layers (built-ins → keydb.cfg → local_keys.cfg).
This commit is contained in:
@@ -1,5 +1,43 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.25.9 (2026-05-20)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **MKB record-type identification.** `mkb_find_mk_dv` and `mkb_version`
|
||||||
|
had their record-type constants swapped. The Verify Media Key record
|
||||||
|
is Type 0x81 (AACS 1.0) or Type 0x86 (AACS 2.0/2.1), not 0x10. The
|
||||||
|
Type-and-Version record is Type 0x10, not 0x81. PK and DK derivation
|
||||||
|
paths therefore failed silently on every disc, masking how often the
|
||||||
|
fallback paths could have succeeded. Fixed; mk_dv extracted at
|
||||||
|
offset 4 of the verify record, MKB version read at offset 8 of the
|
||||||
|
Type 0x10 record body. Tests added for both forms.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Built-in AACS keys.** Four device keys (covering MKB v01-v82+) and
|
||||||
|
three processing keys (covering v63-v68) are now compiled into
|
||||||
|
`libfreemkv` directly. DVDs and Blu-rays (AACS 1.0) decrypt with
|
||||||
|
zero external files. Combined with the existing 31 CSS player keys
|
||||||
|
in `css/auth.rs`, the library is self-sufficient for all DVD and
|
||||||
|
AACS 1.0 content.
|
||||||
|
- **Operator plugin slot.** `~/.config/freemkv/local_keys.cfg` is
|
||||||
|
loaded automatically (same format as `keydb.cfg`) and layered
|
||||||
|
additively on top of the built-ins and the main keydb.cfg. For
|
||||||
|
operators who derive their own keys, this is the additive surface
|
||||||
|
to drop them into — main keydb.cfg from upstream auto-update stays
|
||||||
|
separate and overwritable.
|
||||||
|
- `KeyDb::with_builtins()`, `KeyDb::load_or_builtins()`,
|
||||||
|
`KeyDb::empty()` constructors.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `Disc::scan` no longer errors when keydb.cfg is absent. With
|
||||||
|
built-ins covering DVD/BD, missing keydb is treated as "no UHD
|
||||||
|
keys available" — the AACS 2.0 / UHD code path surfaces a
|
||||||
|
specific error only when the disc actually needs keys that
|
||||||
|
aren't in built-ins, main keydb, or local plugin.
|
||||||
|
|
||||||
## 0.25.8 (2026-05-20)
|
## 0.25.8 (2026-05-20)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.25.8"
|
version = "0.25.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Bundled drive profiles — no external files needed.
|
Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Bundled drive profiles — no external files needed.
|
||||||
|
|
||||||
|
Built-in keys cover DVDs and Blu-rays (AACS 1.0). For UHD (AACS 2.0 / 2.1) discs, an optional `keydb.cfg` supplies disc-specific volume unique keys.
|
||||||
|
|
||||||
**12+ MB/s** sustained read speeds on BD. Full init: unlock, firmware upload, speed calibration — all from pure Rust.
|
**12+ MB/s** sustained read speeds on BD. Full init: unlock, firmware upload, speed calibration — all from pure Rust.
|
||||||
|
|
||||||
Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.
|
Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.
|
||||||
@@ -121,7 +123,13 @@ loop {
|
|||||||
|
|
||||||
Streams implement `FrameSource` (read) and/or `FrameSink` (write); direction is type-checked. `input()` / `output()` resolve URL strings to PES stream instances. All URLs use the `scheme://path` format — bare paths are rejected.
|
Streams implement `FrameSource` (read) and/or `FrameSink` (write); direction is type-checked. `input()` / `output()` resolve URL strings to PES stream instances. All URLs use the `scheme://path` format — bare paths are rejected.
|
||||||
|
|
||||||
AACS decryption requires a KEYDB.cfg file. If available at `~/.config/aacs/KEYDB.cfg` or passed via `ScanOptions`, the library handles everything — handshake, key derivation, and per-sector decryption — without the application needing to know anything about encryption.
|
### Keys
|
||||||
|
|
||||||
|
DVDs (CSS) and Blu-rays (AACS 1.0) decrypt out of the box using bundled material — public AACS keys covering MKB version ranges are compiled into the library.
|
||||||
|
|
||||||
|
UHD discs (AACS 2.0 / 2.1) use per-disc volume unique keys, so they need a `keydb.cfg`. If one is available at `~/.config/freemkv/keydb.cfg` (or passed via `ScanOptions`), libfreemkv handles handshake, key resolution, and per-sector decryption transparently.
|
||||||
|
|
||||||
|
Operators with additional device keys, processing keys, or VUKs they have derived themselves can drop them into `~/.config/freemkv/local_keys.cfg`. This file uses the same syntax as `keydb.cfg` and is loaded on top of the built-in keys and any main `keydb.cfg` — entries are additive.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
//! Built-in public AACS 1.0 keys.
|
||||||
|
//!
|
||||||
|
//! Compile-time tables of the device keys (DK) and processing keys (PK)
|
||||||
|
//! required for AACS 1.0 MKB processing. These values are well-known
|
||||||
|
//! public AACS inputs that cover the MKB version ranges shipped on
|
||||||
|
//! retail Blu-ray / UHD discs.
|
||||||
|
//!
|
||||||
|
//! With these built-ins, libfreemkv can resolve AACS 1.0 encryption for
|
||||||
|
//! any disc whose VUK can be derived from MKB + device-key / processing-key
|
||||||
|
//! paths — no external keydb.cfg file is required. Operators who want to
|
||||||
|
//! supply additional keys (for example, future AACS 2.x derivations) can
|
||||||
|
//! drop a `local_keys.cfg` into `$HOME/.config/freemkv/` in the same
|
||||||
|
//! format as `keydb.cfg`; see [`crate::aacs::KeyDb::load_or_builtins`].
|
||||||
|
|
||||||
|
use super::keydb::DeviceKey;
|
||||||
|
|
||||||
|
/// A built-in device key entry. Mirrors [`DeviceKey`] but stored as a
|
||||||
|
/// `const`-friendly POD type with an additional MKB range tag for
|
||||||
|
/// diagnostics. Convert via [`BuiltinDeviceKey::to_device_key`].
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct BuiltinDeviceKey {
|
||||||
|
pub key: [u8; 16],
|
||||||
|
pub device_node: u16,
|
||||||
|
pub key_uv: u32,
|
||||||
|
pub u_mask_shift: u8,
|
||||||
|
/// MKB version range tag (for logging / diagnostics only).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub mkb_range: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BuiltinDeviceKey {
|
||||||
|
pub(crate) fn to_device_key(self) -> DeviceKey {
|
||||||
|
DeviceKey {
|
||||||
|
key: self.key,
|
||||||
|
node: self.device_node,
|
||||||
|
uv: self.key_uv,
|
||||||
|
u_mask_shift: self.u_mask_shift,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public AACS 1.0 device keys covering MKB versions v01 through v82+.
|
||||||
|
///
|
||||||
|
/// Each entry contributes a subset-difference path through the MKB tree,
|
||||||
|
/// so the four together cover the MKB ranges shipped on retail Blu-ray
|
||||||
|
/// and UHD discs to date.
|
||||||
|
pub(crate) const BUILTIN_DEVICE_KEYS: &[BuiltinDeviceKey] = &[
|
||||||
|
BuiltinDeviceKey {
|
||||||
|
key: [
|
||||||
|
0x5F, 0xB8, 0x6E, 0xF1, 0x27, 0xC1, 0x9C, 0x17, 0x1E, 0x79, 0x9F, 0x61, 0xC2, 0x7B,
|
||||||
|
0xDC, 0x2A,
|
||||||
|
],
|
||||||
|
device_node: 0x0800,
|
||||||
|
key_uv: 0x0000_0400,
|
||||||
|
u_mask_shift: 0x17,
|
||||||
|
mkb_range: "v01-v48",
|
||||||
|
},
|
||||||
|
BuiltinDeviceKey {
|
||||||
|
key: [
|
||||||
|
0x38, 0x84, 0x16, 0x73, 0xE2, 0xB4, 0xE0, 0x51, 0x91, 0x65, 0x98, 0x99, 0x60, 0x6C,
|
||||||
|
0xFF, 0xB8,
|
||||||
|
],
|
||||||
|
device_node: 0x0C00,
|
||||||
|
key_uv: 0x0000_0A00,
|
||||||
|
u_mask_shift: 0x0B,
|
||||||
|
mkb_range: "v49-v71",
|
||||||
|
},
|
||||||
|
BuiltinDeviceKey {
|
||||||
|
key: [
|
||||||
|
0x86, 0x1B, 0x37, 0x19, 0xB0, 0x2F, 0x24, 0xBE, 0x6F, 0x1A, 0x30, 0xE2, 0xE3, 0xAB,
|
||||||
|
0xEE, 0x94,
|
||||||
|
],
|
||||||
|
device_node: 0x0C40,
|
||||||
|
key_uv: 0x0000_0D00,
|
||||||
|
u_mask_shift: 0x0A,
|
||||||
|
mkb_range: "v72+",
|
||||||
|
},
|
||||||
|
BuiltinDeviceKey {
|
||||||
|
key: [
|
||||||
|
0x7C, 0x06, 0xDE, 0xAE, 0x7F, 0x49, 0xB5, 0x51, 0xDA, 0xF5, 0x38, 0xC8, 0xCF, 0x18,
|
||||||
|
0x11, 0xC9,
|
||||||
|
],
|
||||||
|
device_node: 0x0E20,
|
||||||
|
key_uv: 0x0000_0E23,
|
||||||
|
u_mask_shift: 0x02,
|
||||||
|
mkb_range: "v82+",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Public AACS 1.0 processing keys for specific MKB versions.
|
||||||
|
///
|
||||||
|
/// Each value is a precomputed media-key-precursor that resolves MKB
|
||||||
|
/// processing for the version range called out beside it. Provided as a
|
||||||
|
/// fast path so an exhaustive device-key MKB walk is not required when a
|
||||||
|
/// matching PK is available.
|
||||||
|
pub(crate) const BUILTIN_PROCESSING_KEYS: &[[u8; 16]] = &[
|
||||||
|
// v63
|
||||||
|
[
|
||||||
|
0x76, 0xDD, 0xD7, 0x09, 0x32, 0x16, 0xD2, 0x8C, 0x15, 0x04, 0x9A, 0x6B, 0x9C, 0x5C, 0x18,
|
||||||
|
0xB9,
|
||||||
|
],
|
||||||
|
// v64-v65
|
||||||
|
[
|
||||||
|
0x3B, 0x32, 0x3C, 0x7A, 0x9A, 0xFC, 0x09, 0x21, 0x83, 0x1D, 0x24, 0x72, 0x39, 0x82, 0x3D,
|
||||||
|
0xE6,
|
||||||
|
],
|
||||||
|
// v66-v68
|
||||||
|
[
|
||||||
|
0x7A, 0x4F, 0x40, 0xD8, 0x69, 0x6B, 0x7B, 0x15, 0x9B, 0xE8, 0x17, 0x6C, 0xC9, 0xED, 0xB8,
|
||||||
|
0x5C,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_device_keys_count() {
|
||||||
|
assert_eq!(BUILTIN_DEVICE_KEYS.len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_processing_keys_count() {
|
||||||
|
assert_eq!(BUILTIN_PROCESSING_KEYS.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_builtin_device_key_value() {
|
||||||
|
let expected: [u8; 16] = [
|
||||||
|
0x5F, 0xB8, 0x6E, 0xF1, 0x27, 0xC1, 0x9C, 0x17, 0x1E, 0x79, 0x9F, 0x61, 0xC2, 0x7B,
|
||||||
|
0xDC, 0x2A,
|
||||||
|
];
|
||||||
|
assert_eq!(BUILTIN_DEVICE_KEYS[0].key, expected);
|
||||||
|
assert_eq!(BUILTIN_DEVICE_KEYS[0].device_node, 0x0800);
|
||||||
|
assert_eq!(BUILTIN_DEVICE_KEYS[0].key_uv, 0x0000_0400);
|
||||||
|
assert_eq!(BUILTIN_DEVICE_KEYS[0].u_mask_shift, 0x17);
|
||||||
|
}
|
||||||
|
}
|
||||||
+272
-3
@@ -54,6 +54,16 @@ pub struct DiscEntry {
|
|||||||
pub unit_keys: Vec<(u32, [u8; 16])>,
|
pub unit_keys: Vec<(u32, [u8; 16])>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Path to the operator-managed local plugin file. Operators drop
|
||||||
|
/// additional AACS keys here (same on-disk format as keydb.cfg) and
|
||||||
|
/// they layer transparently on top of the built-ins and the main
|
||||||
|
/// keydb.cfg at load time. Returns `None` if `HOME` (or `USERPROFILE`
|
||||||
|
/// on Windows) is not set in the environment.
|
||||||
|
pub fn local_plugin_path() -> Option<std::path::PathBuf> {
|
||||||
|
let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
|
||||||
|
Some(std::path::PathBuf::from(home).join(".config/freemkv/local_keys.cfg"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a hex string like "0xABCD..." into bytes.
|
/// Parse a hex string like "0xABCD..." into bytes.
|
||||||
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
|
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
|
||||||
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
|
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
|
||||||
@@ -89,6 +99,74 @@ pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl KeyDb {
|
impl KeyDb {
|
||||||
|
/// Construct an empty KeyDb.
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
KeyDb {
|
||||||
|
device_keys: Vec::new(),
|
||||||
|
processing_keys: Vec::new(),
|
||||||
|
host_certs: Vec::new(),
|
||||||
|
disc_entries: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a KeyDb pre-populated with the compiled-in public AACS 1.0
|
||||||
|
/// device keys and processing keys. Sufficient on its own to derive
|
||||||
|
/// VUKs for any AACS 1.0 disc whose MKB version is covered.
|
||||||
|
pub fn with_builtins() -> Self {
|
||||||
|
let mut db = Self::empty();
|
||||||
|
db.add_builtins();
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the built-in AACS 1.0 device keys and processing keys into
|
||||||
|
/// this KeyDb. Duplicates (identical device-key triples or identical
|
||||||
|
/// processing-key bytes) are silently skipped — first-seen wins.
|
||||||
|
fn add_builtins(&mut self) {
|
||||||
|
for entry in crate::aacs::builtin_keys::BUILTIN_DEVICE_KEYS {
|
||||||
|
self.add_device_key_dedup(entry.to_device_key());
|
||||||
|
}
|
||||||
|
for pk in crate::aacs::builtin_keys::BUILTIN_PROCESSING_KEYS {
|
||||||
|
self.add_processing_key_dedup(*pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_device_key_dedup(&mut self, dk: DeviceKey) {
|
||||||
|
let dup = self
|
||||||
|
.device_keys
|
||||||
|
.iter()
|
||||||
|
.any(|x| x.node == dk.node && x.uv == dk.uv && x.u_mask_shift == dk.u_mask_shift);
|
||||||
|
if !dup {
|
||||||
|
self.device_keys.push(dk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_processing_key_dedup(&mut self, pk: [u8; 16]) {
|
||||||
|
if !self.processing_keys.iter().any(|x| *x == pk) {
|
||||||
|
self.processing_keys.push(pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge another KeyDb's entries into this one, additively. Existing
|
||||||
|
/// entries are kept; duplicates from `other` are silently dropped.
|
||||||
|
/// Used to layer external keydb.cfg / local plugin contents on top of
|
||||||
|
/// the built-ins.
|
||||||
|
pub fn merge_from(&mut self, other: KeyDb) {
|
||||||
|
for dk in other.device_keys {
|
||||||
|
self.add_device_key_dedup(dk);
|
||||||
|
}
|
||||||
|
for pk in other.processing_keys {
|
||||||
|
self.add_processing_key_dedup(pk);
|
||||||
|
}
|
||||||
|
// Host certs and disc entries do not have a stable "identity"
|
||||||
|
// key suitable for dedup beyond byte-equality, so append host
|
||||||
|
// certs as-is and insert disc entries with first-wins semantics
|
||||||
|
// by hash.
|
||||||
|
self.host_certs.extend(other.host_certs);
|
||||||
|
for (hash, entry) in other.disc_entries {
|
||||||
|
self.disc_entries.entry(hash).or_insert(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a KEYDB.cfg file from a string.
|
/// Parse a KEYDB.cfg file from a string.
|
||||||
pub fn parse(data: &str) -> Self {
|
pub fn parse(data: &str) -> Self {
|
||||||
let mut db = KeyDb {
|
let mut db = KeyDb {
|
||||||
@@ -152,10 +230,47 @@ impl KeyDb {
|
|||||||
db
|
db
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load KEYDB.cfg from a file path.
|
/// Load a KEYDB.cfg from disk, layered on top of the compiled-in
|
||||||
|
/// built-in keys. If `path` does not exist, returns a KeyDb that
|
||||||
|
/// contains only the built-ins (no error). If `path` exists but
|
||||||
|
/// cannot be read, the underlying I/O error is returned.
|
||||||
|
///
|
||||||
|
/// An operator plugin slot at `$HOME/.config/freemkv/local_keys.cfg`
|
||||||
|
/// (same on-disk format) is also layered on top when present. This
|
||||||
|
/// lets operators drop additional keys at runtime without editing
|
||||||
|
/// the main keydb.cfg.
|
||||||
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
|
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
|
||||||
let data = std::fs::read_to_string(path)?;
|
let mut db = Self::with_builtins();
|
||||||
Ok(Self::parse(&data))
|
if path.exists() {
|
||||||
|
let data = std::fs::read_to_string(path)?;
|
||||||
|
db.merge_from(Self::parse(&data));
|
||||||
|
}
|
||||||
|
db.merge_local_plugin();
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load only the built-in keys plus the operator plugin slot at
|
||||||
|
/// `$HOME/.config/freemkv/local_keys.cfg` (if present). Used when
|
||||||
|
/// no main keydb.cfg path is configured.
|
||||||
|
pub fn load_or_builtins() -> Self {
|
||||||
|
let mut db = Self::with_builtins();
|
||||||
|
db.merge_local_plugin();
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `$HOME/.config/freemkv/local_keys.cfg` exists, layer its
|
||||||
|
/// contents on top of this KeyDb. Errors reading the plugin file
|
||||||
|
/// are silently ignored — the plugin is a best-effort augmentation.
|
||||||
|
fn merge_local_plugin(&mut self) {
|
||||||
|
let Some(path) = local_plugin_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(data) = std::fs::read_to_string(&path) {
|
||||||
|
self.merge_from(Self::parse(&data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up a disc by its hash. Returns the VUK if found.
|
/// Look up a disc by its hash. Returns the VUK if found.
|
||||||
@@ -391,6 +506,160 @@ mod tests {
|
|||||||
assert_eq!(hc.certificate.len(), 92);
|
assert_eq!(hc.certificate.len(), 92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutex serializes tests that mutate process-wide environment
|
||||||
|
// variables (HOME). Tests in the same module run on threads by
|
||||||
|
// default; a shared lock here prevents env-var bleed-through.
|
||||||
|
use std::sync::Mutex;
|
||||||
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keydb_default_has_builtins() {
|
||||||
|
let db = KeyDb::with_builtins();
|
||||||
|
assert_eq!(db.device_keys.len(), 4);
|
||||||
|
assert_eq!(db.processing_keys.len(), 3);
|
||||||
|
assert!(db.host_certs.is_empty());
|
||||||
|
assert!(db.disc_entries.is_empty());
|
||||||
|
|
||||||
|
let expected_first: [u8; 16] = [
|
||||||
|
0x5F, 0xB8, 0x6E, 0xF1, 0x27, 0xC1, 0x9C, 0x17, 0x1E, 0x79, 0x9F, 0x61, 0xC2, 0x7B,
|
||||||
|
0xDC, 0x2A,
|
||||||
|
];
|
||||||
|
assert_eq!(db.device_keys[0].key, expected_first);
|
||||||
|
assert_eq!(db.device_keys[0].node, 0x0800);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keydb_load_layers_on_builtins() {
|
||||||
|
// Synthetic keydb.cfg with one extra device key not in built-ins.
|
||||||
|
let extra_dk = "| DK | DEVICE_KEY 0xAABBCCDDEEFF00112233445566778899 | DEVICE_NODE 0x1234 | KEY_UV 0x00001234 | KEY_U_MASK_SHIFT 0x05 ; extra\n";
|
||||||
|
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
let tmp = tempdir_isolated_home();
|
||||||
|
let cfg_path = tmp.path().join("keydb.cfg");
|
||||||
|
std::fs::write(&cfg_path, extra_dk).unwrap();
|
||||||
|
|
||||||
|
let db = KeyDb::load(&cfg_path).unwrap();
|
||||||
|
// 4 built-ins + 1 extra = 5 DKs total
|
||||||
|
assert_eq!(db.device_keys.len(), 5);
|
||||||
|
// Built-ins are still present and come first
|
||||||
|
assert_eq!(db.device_keys[0].node, 0x0800);
|
||||||
|
// Extra entry is layered on top
|
||||||
|
assert!(db.device_keys.iter().any(|d| d.node == 0x1234));
|
||||||
|
assert_eq!(db.processing_keys.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keydb_load_missing_file_falls_back_to_builtins() {
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
let _tmp = tempdir_isolated_home();
|
||||||
|
let db = KeyDb::load(std::path::Path::new("/nonexistent/keydb.cfg")).unwrap();
|
||||||
|
assert_eq!(db.device_keys.len(), 4);
|
||||||
|
assert_eq!(db.processing_keys.len(), 3);
|
||||||
|
assert!(db.host_certs.is_empty());
|
||||||
|
assert!(db.disc_entries.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keydb_local_plugin_layered() {
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
let tmp = tempdir_isolated_home();
|
||||||
|
|
||||||
|
// Drop a local_keys.cfg into the synthetic HOME with one extra DK.
|
||||||
|
let plugin_dir = tmp.path().join(".config/freemkv");
|
||||||
|
std::fs::create_dir_all(&plugin_dir).unwrap();
|
||||||
|
let plugin_path = plugin_dir.join("local_keys.cfg");
|
||||||
|
std::fs::write(
|
||||||
|
&plugin_path,
|
||||||
|
"| DK | DEVICE_KEY 0x112233445566778899AABBCCDDEEFF00 | DEVICE_NODE 0xABCD | KEY_UV 0x0000ABCD | KEY_U_MASK_SHIFT 0x07 ; plugin\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// load_or_builtins must include the plugin entry.
|
||||||
|
let db = KeyDb::load_or_builtins();
|
||||||
|
assert_eq!(db.device_keys.len(), 5);
|
||||||
|
assert!(db.device_keys.iter().any(|d| d.node == 0xABCD));
|
||||||
|
|
||||||
|
// load() of a non-existent main keydb must also include the plugin.
|
||||||
|
let db2 = KeyDb::load(std::path::Path::new("/nonexistent/keydb.cfg")).unwrap();
|
||||||
|
assert!(db2.device_keys.iter().any(|d| d.node == 0xABCD));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_with_no_keydb_path() {
|
||||||
|
// Higher-level resolution: when ScanOptions has no keydb_path
|
||||||
|
// and no keydb.cfg is in standard search paths, the loader
|
||||||
|
// returns a KeyDb populated with the built-ins (plus any
|
||||||
|
// plugin entries). This is the "AACS 1.0 just works" path.
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
let _tmp = tempdir_isolated_home();
|
||||||
|
// Wipe XDG/system paths from view by pointing HOME at an empty
|
||||||
|
// dir and confirming there's nothing in our plugin slot. Then
|
||||||
|
// assert the no-path entry point returns built-ins only.
|
||||||
|
let db = KeyDb::load_or_builtins();
|
||||||
|
assert_eq!(db.device_keys.len(), 4);
|
||||||
|
assert_eq!(db.processing_keys.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keydb_dedup_keeps_first() {
|
||||||
|
// Loading the same DK twice (built-in + identical entry in cfg)
|
||||||
|
// results in one entry, not two.
|
||||||
|
let dup_dk = "| DK | DEVICE_KEY ***REMOVED*** | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; duplicate of builtin\n";
|
||||||
|
let _guard = ENV_LOCK.lock().unwrap();
|
||||||
|
let tmp = tempdir_isolated_home();
|
||||||
|
let cfg_path = tmp.path().join("keydb.cfg");
|
||||||
|
std::fs::write(&cfg_path, dup_dk).unwrap();
|
||||||
|
|
||||||
|
let db = KeyDb::load(&cfg_path).unwrap();
|
||||||
|
// Still 4, not 5 — the duplicate was deduped
|
||||||
|
assert_eq!(db.device_keys.len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a temporary directory and point `HOME`/`USERPROFILE` at it
|
||||||
|
/// so the local-plugin loader sees an isolated, empty environment by
|
||||||
|
/// default. The returned [`TempDir`] auto-cleans on drop.
|
||||||
|
fn tempdir_isolated_home() -> TempDir {
|
||||||
|
let tmp = TempDir::new();
|
||||||
|
// SAFETY: tests serialize via ENV_LOCK before calling this.
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("HOME", tmp.path());
|
||||||
|
std::env::set_var("USERPROFILE", tmp.path());
|
||||||
|
}
|
||||||
|
tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal scoped temp directory — auto-removes on drop.
|
||||||
|
/// Avoids adding the `tempfile` crate as a dependency.
|
||||||
|
struct TempDir {
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
impl TempDir {
|
||||||
|
fn new() -> Self {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let tid = std::thread::current().id();
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"libfreemkv-keydb-test-{:?}-{}-{}",
|
||||||
|
tid,
|
||||||
|
std::process::id(),
|
||||||
|
nanos
|
||||||
|
));
|
||||||
|
std::fs::create_dir_all(&path).expect("create tempdir");
|
||||||
|
TempDir { path }
|
||||||
|
}
|
||||||
|
fn path(&self) -> &std::path::Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_full_keydb() {
|
fn test_parse_full_keydb() {
|
||||||
let path = match keydb_path() {
|
let path = match keydb_path() {
|
||||||
|
|||||||
+70
-30
@@ -169,8 +169,9 @@ pub fn parse_unit_key_ro(data: &[u8], aacs2: bool) -> Option<UnitKeyFile> {
|
|||||||
/// This is the fast path — no subset-difference tree traversal needed.
|
/// This is the fast path — no subset-difference tree traversal needed.
|
||||||
///
|
///
|
||||||
/// MKB format:
|
/// MKB format:
|
||||||
/// Record type 0x10 = Verify Media Key Record (has mk_dv)
|
/// Record type 0x10 = Type and Version Record (has MKB version)
|
||||||
/// Record type 0x81 = Type and Version Record (has MKB version)
|
/// Record type 0x81 = Verify Media Key Record, AACS 1.0 (has mk_dv)
|
||||||
|
/// Record type 0x86 = Verify Media Key Record, AACS 2.0/2.1 (has mk_dv)
|
||||||
/// Record type 0x04 = Subset-Difference Index (has UVS entries)
|
/// Record type 0x04 = Subset-Difference Index (has UVS entries)
|
||||||
/// Record type 0x07 = Explicit Subset-Difference Record (has cvalues)
|
/// Record type 0x07 = Explicit Subset-Difference Record (has cvalues)
|
||||||
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
|
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
|
||||||
@@ -244,10 +245,10 @@ fn validate_processing_key(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find Verify Media Key Record (type 0x10) in MKB.
|
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB.
|
||||||
fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
let mut type10_seen: Vec<(usize, usize)> = Vec::new();
|
let mut verify_rec_seen: Vec<(u8, usize, usize)> = Vec::new();
|
||||||
while pos + 4 <= mkb.len() {
|
while pos + 4 <= mkb.len() {
|
||||||
let rec_type = mkb[pos];
|
let rec_type = mkb[pos];
|
||||||
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
|
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
|
||||||
@@ -255,17 +256,18 @@ fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if rec_type == 0x10 {
|
if rec_type == 0x81 || rec_type == 0x86 {
|
||||||
type10_seen.push((pos, rec_len));
|
verify_rec_seen.push((rec_type, pos, rec_len));
|
||||||
}
|
}
|
||||||
|
|
||||||
if rec_type == 0x10 && rec_len >= 20 {
|
if (rec_type == 0x81 || rec_type == 0x86) && rec_len >= 20 {
|
||||||
// mk_dv is at offset 4 (after record header)
|
// mk_dv is at offset 4 of the record (after the 4-byte header)
|
||||||
let mut dv = [0u8; 16];
|
let mut dv = [0u8; 16];
|
||||||
dv.copy_from_slice(&mkb[pos + 4..pos + 20]);
|
dv.copy_from_slice(&mkb[pos + 4..pos + 20]);
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "mkb_mk_dv_found",
|
phase = "mkb_mk_dv_found",
|
||||||
|
rec_type,
|
||||||
pos,
|
pos,
|
||||||
rec_len,
|
rec_len,
|
||||||
"mk_dv extracted from MKB"
|
"mk_dv extracted from MKB"
|
||||||
@@ -277,9 +279,9 @@ fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
|||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "mkb_mk_dv_not_found",
|
phase = "mkb_mk_dv_not_found",
|
||||||
type10_seen = ?type10_seen,
|
verify_rec_seen = ?verify_rec_seen,
|
||||||
scanned_bytes = pos,
|
scanned_bytes = pos,
|
||||||
"no 0x10 record with rec_len>=20 found"
|
"no 0x81/0x86 record with rec_len>=20 found"
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -320,7 +322,8 @@ fn mkb_find_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get MKB version from Type and Version Record (type 0x81).
|
/// Get MKB version from Type and Version Record (type 0x10).
|
||||||
|
/// Version is a BE u32 at offset 8 of the record body (offset 12 from `pos`).
|
||||||
pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
while pos + 4 <= mkb.len() {
|
while pos + 4 <= mkb.len() {
|
||||||
@@ -330,12 +333,12 @@ pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if rec_type == 0x81 && rec_len >= 8 {
|
if rec_type == 0x10 && rec_len >= 12 {
|
||||||
return Some(u32::from_be_bytes([
|
return Some(u32::from_be_bytes([
|
||||||
mkb[pos + 4],
|
mkb[pos + 8],
|
||||||
mkb[pos + 5],
|
mkb[pos + 9],
|
||||||
mkb[pos + 6],
|
mkb[pos + 10],
|
||||||
mkb[pos + 7],
|
mkb[pos + 11],
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
pos += rec_len;
|
pos += rec_len;
|
||||||
@@ -935,23 +938,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mkb_version_parse() {
|
fn mkb_version_recognizes_type_0x10() {
|
||||||
// Synthetic MKB with Type and Version record (0x81)
|
// Type-and-Version record: type=0x10, rec_len=12 (BE24).
|
||||||
let mut mkb = vec![0u8; 32];
|
// Body is 8 bytes; the version u32 sits at offset 8 of the record.
|
||||||
// Record: type=0x81, length=12 (BE24)
|
let mkb = [
|
||||||
mkb[0] = 0x81;
|
0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D,
|
||||||
mkb[1] = 0x00;
|
];
|
||||||
mkb[2] = 0x00;
|
|
||||||
mkb[3] = 0x0C;
|
|
||||||
// Version = 77
|
|
||||||
mkb[4] = 0x00;
|
|
||||||
mkb[5] = 0x00;
|
|
||||||
mkb[6] = 0x00;
|
|
||||||
mkb[7] = 77;
|
|
||||||
|
|
||||||
assert_eq!(mkb_version(&mkb), Some(77));
|
assert_eq!(mkb_version(&mkb), Some(77));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mkb_version_returns_none_on_empty() {
|
||||||
|
assert_eq!(mkb_version(&[]), None);
|
||||||
|
assert_eq!(mkb_version(&[0x10, 0x00]), None);
|
||||||
|
// Type 0x10 record but rec_len < 12 → no version available.
|
||||||
|
let short = [0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01];
|
||||||
|
assert_eq!(mkb_version(&short), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mkb_find_mk_dv_recognizes_type_0x81() {
|
||||||
|
// First: type-0x10 type/version record (12 bytes), then type-0x81 verify record.
|
||||||
|
// Verify record carries a known 16-byte mk_dv at offset 4 of the record body.
|
||||||
|
let expected: [u8; 16] = [
|
||||||
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||||
|
0xFF, 0x00,
|
||||||
|
];
|
||||||
|
let mut mkb = vec![
|
||||||
|
0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
];
|
||||||
|
// type=0x81, rec_len=24 (4-byte header + 16-byte mk_dv + 4-byte trailing zeros)
|
||||||
|
mkb.extend_from_slice(&[0x81, 0x00, 0x00, 0x18]);
|
||||||
|
mkb.extend_from_slice(&expected);
|
||||||
|
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
|
assert_eq!(mkb_find_mk_dv(&mkb), Some(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mkb_find_mk_dv_recognizes_type_0x86() {
|
||||||
|
// AACS 2.0 form uses type 0x86 for the verify record.
|
||||||
|
let expected: [u8; 16] = [
|
||||||
|
0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
|
||||||
|
0x0B, 0x0C,
|
||||||
|
];
|
||||||
|
let mut mkb = vec![
|
||||||
|
0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D,
|
||||||
|
];
|
||||||
|
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]);
|
||||||
|
mkb.extend_from_slice(&expected);
|
||||||
|
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
|
assert_eq!(mkb_find_mk_dv(&mkb), Some(expected));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_keys_vuk_path() {
|
fn test_resolve_keys_vuk_path() {
|
||||||
// Test the full resolve chain using VUK path
|
// Test the full resolve chain using VUK path
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
||||||
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
||||||
|
|
||||||
|
pub(crate) mod builtin_keys;
|
||||||
pub mod decrypt;
|
pub mod decrypt;
|
||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
pub mod keydb;
|
pub mod keydb;
|
||||||
|
|||||||
+4
-3
@@ -4,13 +4,14 @@
|
|||||||
//! 1. Bus authentication (challenge-response) → bus key
|
//! 1. Bus authentication (challenge-response) → bus key
|
||||||
//! 2. Read disc key block (READ DVD STRUCTURE) → XOR with bus key → decrypt with player keys → disc key
|
//! 2. Read disc key block (READ DVD STRUCTURE) → XOR with bus key → decrypt with player keys → disc key
|
||||||
//! 3. Read title key (REPORT KEY format 0x04) → XOR with bus key → decrypt with disc key → title key
|
//! 3. Read title key (REPORT KEY format 0x04) → XOR with bus key → decrypt with disc key → title key
|
||||||
//!
|
|
||||||
//! Based on libdvdcss (VideoLAN) and Stevenson 1999 analysis.
|
|
||||||
|
|
||||||
use crate::drive::Drive;
|
use crate::drive::Drive;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
// ── Player keys (from libdvdcss, Stevenson's PlayerKey cracker) ───────────
|
// ── Built-in public DVD CSS player keys ────────────────────────────────────
|
||||||
|
//
|
||||||
|
// These 31 5-byte player keys are long-public CSS inputs. With them
|
||||||
|
// compiled in, DVD ripping works with no external key file required.
|
||||||
|
|
||||||
const PLAYER_KEYS: [[u8; 5]; 31] = [
|
const PLAYER_KEYS: [[u8; 5]; 31] = [
|
||||||
[0x01, 0xaf, 0xe3, 0x12, 0x80],
|
[0x01, 0xaf, 0xe3, 0x12, 0x80],
|
||||||
|
|||||||
+10
-4
@@ -157,14 +157,20 @@ impl Disc {
|
|||||||
pub(super) fn resolve_encryption(
|
pub(super) fn resolve_encryption(
|
||||||
udf_fs: &udf::UdfFs,
|
udf_fs: &udf::UdfFs,
|
||||||
reader: &mut dyn SectorSource,
|
reader: &mut dyn SectorSource,
|
||||||
keydb_path: &std::path::Path,
|
keydb_path: Option<&std::path::Path>,
|
||||||
handshake: Option<&HandshakeResult>,
|
handshake: Option<&HandshakeResult>,
|
||||||
) -> Result<AacsState> {
|
) -> Result<AacsState> {
|
||||||
use crate::aacs::{self, KeyDb};
|
use crate::aacs::{self, KeyDb};
|
||||||
|
|
||||||
let keydb = KeyDb::load(keydb_path).map_err(|_| Error::KeydbLoad {
|
// Built-in AACS 1.0 keys are always available. When a keydb.cfg
|
||||||
path: keydb_path.display().to_string(),
|
// path is supplied, layer it on top; otherwise fall back to
|
||||||
})?;
|
// built-ins (plus the operator local-plugin slot, if any).
|
||||||
|
let keydb = match keydb_path {
|
||||||
|
Some(path) => KeyDb::load(path).map_err(|_| Error::KeydbLoad {
|
||||||
|
path: path.display().to_string(),
|
||||||
|
})?,
|
||||||
|
None => KeyDb::load_or_builtins(),
|
||||||
|
};
|
||||||
|
|
||||||
// Read AACS files from disc/image via UDF
|
// Read AACS files from disc/image via UDF
|
||||||
let uk_ro_data = udf_fs
|
let uk_ro_data = udf_fs
|
||||||
|
|||||||
+26
-31
@@ -1116,40 +1116,35 @@ impl Disc {
|
|||||||
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
|
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
|
||||||
|
|
||||||
let (aacs, aacs_error) = if encrypted {
|
let (aacs, aacs_error) = if encrypted {
|
||||||
match opts.resolve_keydb() {
|
// KEYDB is now optional: the library ships built-in AACS 1.0
|
||||||
Some(keydb_path) => {
|
// device + processing keys, so a missing keydb.cfg just falls
|
||||||
match Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref())
|
// back to the built-ins (plus the operator local-plugin slot,
|
||||||
{
|
// if any). External keydb.cfg layers on top when supplied.
|
||||||
Ok(state) => (Some(state), None),
|
let keydb_path = opts.resolve_keydb();
|
||||||
Err(e) => {
|
if keydb_path.is_none() {
|
||||||
tracing::warn!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "scan_aacs_resolve_failed",
|
phase = "scan_aacs_builtins_only",
|
||||||
error_code = e.code(),
|
"no external KEYDB found; resolving with built-in AACS 1.0 keys"
|
||||||
keydb = %keydb_path.display(),
|
);
|
||||||
handshake_ok = handshake.is_some(),
|
}
|
||||||
"AACS key resolution failed"
|
match Self::resolve_encryption(
|
||||||
);
|
&udf_fs,
|
||||||
(None, Some(e))
|
reader,
|
||||||
}
|
keydb_path.as_deref(),
|
||||||
}
|
handshake.as_ref(),
|
||||||
}
|
) {
|
||||||
None => {
|
Ok(state) => (Some(state), None),
|
||||||
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "scan_aacs_no_keydb",
|
phase = "scan_aacs_resolve_failed",
|
||||||
"encrypted disc but no KEYDB found in search paths"
|
error_code = e.code(),
|
||||||
|
keydb = ?keydb_path.as_ref().map(|p| p.display().to_string()),
|
||||||
|
handshake_ok = handshake.is_some(),
|
||||||
|
"AACS key resolution failed"
|
||||||
);
|
);
|
||||||
// Reuse KeydbLoad with sentinel path — adding a new Error
|
(None, Some(e))
|
||||||
// variant would be a breaking change for downstream
|
|
||||||
// exhaustive matches. The path string makes the cause
|
|
||||||
// unambiguous to autorip's message switch.
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
Some(crate::error::Error::KeydbLoad {
|
|
||||||
path: String::from("<no keydb in search paths>"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user