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 CLAUDE.md + README updated to describe the
three additive layers (built-ins → keydb.cfg → local_keys.cfg).
This commit is contained in:
2026-05-20 09:00:32 -07:00
parent e8c4df347f
commit 1b9db6e9a4
11 changed files with 588 additions and 73 deletions
+26 -31
View File
@@ -1116,40 +1116,35 @@ impl Disc {
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
let (aacs, aacs_error) = if encrypted {
match opts.resolve_keydb() {
Some(keydb_path) => {
match Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref())
{
Ok(state) => (Some(state), None),
Err(e) => {
tracing::warn!(
target: "freemkv::disc",
phase = "scan_aacs_resolve_failed",
error_code = e.code(),
keydb = %keydb_path.display(),
handshake_ok = handshake.is_some(),
"AACS key resolution failed"
);
(None, Some(e))
}
}
}
None => {
// KEYDB is now optional: the library ships built-in AACS 1.0
// device + processing keys, so a missing keydb.cfg just falls
// back to the built-ins (plus the operator local-plugin slot,
// if any). External keydb.cfg layers on top when supplied.
let keydb_path = opts.resolve_keydb();
if keydb_path.is_none() {
tracing::debug!(
target: "freemkv::disc",
phase = "scan_aacs_builtins_only",
"no external KEYDB found; resolving with built-in AACS 1.0 keys"
);
}
match Self::resolve_encryption(
&udf_fs,
reader,
keydb_path.as_deref(),
handshake.as_ref(),
) {
Ok(state) => (Some(state), None),
Err(e) => {
tracing::warn!(
target: "freemkv::disc",
phase = "scan_aacs_no_keydb",
"encrypted disc but no KEYDB found in search paths"
phase = "scan_aacs_resolve_failed",
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
// 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>"),
}),
)
(None, Some(e))
}
}
} else {