aacs: libredrive raw-read VID path + revert v0.25.9 built-ins + walker fix follow-through
Three coherent threads landing for v0.25.11:
1. Libredrive raw-read VID path. When Mt1959::do_unlock sees both the
MMkv active-mode marker at [12..16] and the LbDr mode-ID marker at
[16..20], Drive::is_libredrive_active() returns true and
do_handshake skips the AACS cert dance — VID is retrieved via
READ_DISC_STRUCTURE format 0x80 with AGID=0 and bus encryption is
already off. This unblocks UHD ripping on drives whose leaked host
cert is on the AACS HRL.
- platform/mt1959/mod.rs: detection + active flag + 4 unit tests.
- platform/mod.rs: PlatformDriver::is_libredrive_active trait method.
- drive/mod.rs: Drive::is_libredrive_active accessor.
- disc/encrypt.rs: do_handshake branches on the flag; new
read_volume_id_libredrive helper. Return type widened to
(Option<HandshakeResult>, Option<Error>) so callers see which
specific failure happened.
- disc/mod.rs: scan_with plumbs the new tuple through and preserves
handshake errors as disc.aacs_error.
2. Revert v0.25.9 built-in AACS keys + plugin slot. Single source of
AACS truth: keydb.cfg. The compiled-in DKs/PKs were a slim
convenience that didn't move the hard problem (no v77+ DKs) and
added a maintenance surface. Plugin slot was overlapping
functionality with the main keydb.
- Deleted src/aacs/builtin_keys.rs (4 DKs + 3 PKs).
- Removed KeyDb::with_builtins, load_or_builtins, merge_from,
merge_local_plugin, local_plugin_path, internal dedup helpers.
KeyDb::empty kept for unit-test use.
- KeyDb::load reverts to pre-0.25.9 form: read file or return I/O
error; no fallback.
- disc::encrypt::resolve_encryption keydb_path back to required
(&Path), not Option<&Path>.
- disc::scan_with surfaces KeydbLoad { path: "<no keydb in search
paths>" } sentinel when encrypted + no keydb — same sentinel
autorip's message switch already handles.
- CSS player keys in src/css/auth.rs stay compiled in; they're
1999-era public inputs separate from AACS and pre-date the 0.25.9
additions.
3. Walker fix follow-through (libaacs-parity validate_processing_key,
cvalues 0x07-then-0x05 preference, path-2/3/4 short-circuit on
zero VID) + NIST AES-CMAC KAT + VID MAC round-trip / mutation /
zero-rejection tests.
5 new Error variants for finer-grained AACS failure reporting:
AacsHostCertRejected (E7015), AacsLibredriveUnsupported (E7016),
AacsVidUnavailable (E7017), AacsMkUnavailable (E7018),
AacsVukNotInKeydb (E7019). Lets CLIs/UIs render which piece of the
AACS chain failed instead of always saying "no keys."
This commit is contained in:
+5
-263
@@ -54,16 +54,6 @@ pub struct DiscEntry {
|
||||
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.
|
||||
pub(crate) fn parse_hex(s: &str) -> Option<Vec<u8>> {
|
||||
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
|
||||
@@ -99,7 +89,8 @@ pub(crate) fn parse_hex20(s: &str) -> Option<[u8; 20]> {
|
||||
}
|
||||
|
||||
impl KeyDb {
|
||||
/// Construct an empty KeyDb.
|
||||
/// Construct an empty KeyDb. Used by unit tests; production code
|
||||
/// reaches a populated KeyDb via [`KeyDb::load`] or [`KeyDb::parse`].
|
||||
pub fn empty() -> Self {
|
||||
KeyDb {
|
||||
device_keys: Vec::new(),
|
||||
@@ -109,64 +100,6 @@ impl KeyDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn parse(data: &str) -> Self {
|
||||
let mut db = KeyDb {
|
||||
@@ -230,47 +163,10 @@ impl KeyDb {
|
||||
db
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Load a KEYDB.cfg from disk.
|
||||
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
|
||||
let mut db = Self::with_builtins();
|
||||
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));
|
||||
}
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
Ok(Self::parse(&data))
|
||||
}
|
||||
|
||||
/// Look up a disc by its hash. Returns the VUK if found.
|
||||
@@ -506,160 +402,6 @@ mod tests {
|
||||
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]
|
||||
fn test_parse_full_keydb() {
|
||||
let path = match keydb_path() {
|
||||
|
||||
Reference in New Issue
Block a user