keydb: union stored UKs + VUK/MK-derived unit keys (dedup by value)
unit_keys_from now loads all stored unit keys AND the VUK/MK-derived ones, deduping by key value instead of first-hit, so a disc with partial stored UKs plus a VUK yields every declared CPS unit key. Plus keydb.cfg format round-trip (to_keydb_cfg inverse of parse).
This commit is contained in:
+79
-54
@@ -166,69 +166,61 @@ impl KeydbSource {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Terminal Unit Keys — directly usable, no derivation. Preserve the
|
// UNION every source of terminal keys, then dedup — never first-hit. A
|
||||||
// keydb's CPS numbering through the resolver's `+ 1` (idx = num - 1).
|
// stored `unit_keys` list can be PARTIAL (the keyupdater only ever sampled
|
||||||
if !entry.unit_keys.is_empty() {
|
// the CPS units reachable from a playlist, so an orphan unit's key may be
|
||||||
return entry
|
// missing), while the per-disc VUK boils EVERY declared CPS unit. Taking
|
||||||
.unit_keys
|
// the stored list alone (the old return-at-first-path) would shadow the
|
||||||
.iter()
|
// VUK and silently drop the orphan unit's key. So gather both and keep a
|
||||||
.map(|(num, key)| UnitKey {
|
// unique-by-key list: the read path tries every key per unit, so an extra
|
||||||
|
// or stale key is harmless — only a MISSING key hurts.
|
||||||
|
let mut keys: Vec<UnitKey> = Vec::new();
|
||||||
|
|
||||||
|
// 1. Terminal Unit Keys stored in the entry — directly usable, no
|
||||||
|
// derivation. Preserve the keydb's CPS numbering (idx = num - 1).
|
||||||
|
for (num, key) in &entry.unit_keys {
|
||||||
|
keys.push(UnitKey {
|
||||||
idx: num.saturating_sub(1),
|
idx: num.saturating_sub(1),
|
||||||
key: *key,
|
key: *key,
|
||||||
})
|
});
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The disc's encrypted title keys (from Unit_Key_RO.inf) — what every
|
// The disc's encrypted title keys (from Unit_Key_RO.inf) — what every
|
||||||
// VUK-or-deeper path decrypts into the terminal keys. Empty when the
|
// VUK-or-deeper path decrypts into the terminal keys. Empty when the scan
|
||||||
// scan captured no Unit_Key_RO.inf, in which case nothing can derive.
|
// captured no Unit_Key_RO.inf, in which case only the stored list (1)
|
||||||
let enc_title_keys = match ctx.enc_title_keys() {
|
// contributes.
|
||||||
Ok(k) => k,
|
let enc_title_keys = ctx.enc_title_keys().unwrap_or(&[]);
|
||||||
Err(_) => return Vec::new(),
|
if !enc_title_keys.is_empty() {
|
||||||
};
|
// 2. Per-disc VUK — one step, no VID needed; boils ALL declared units.
|
||||||
|
// 3. Else a Media Key path (stored MK / PK pool / DK pool) → VUK →
|
||||||
// 2. Per-disc VUK — one step, no VID needed (it directly decrypts the
|
// all declared units. The MK itself carries no VID, but the
|
||||||
// encrypted title keys).
|
// final `vuk_from_mk` needs one: physical (unlocker) VID first,
|
||||||
if let Some(vuk) = entry.vuk {
|
// else the entry's stored VID (`I` field), else cannot derive.
|
||||||
return uk_from_vuk(Vuk(vuk), enc_title_keys);
|
// Either branch yields the COMPLETE declared set, so we take the
|
||||||
}
|
// first that resolves (VUK preferred — cheapest).
|
||||||
|
let derived = if let Some(vuk) = entry.vuk {
|
||||||
// 3. Media Key path. Resolve a Media Key for THIS disc, then derive the
|
uk_from_vuk(Vuk(vuk), enc_title_keys)
|
||||||
// VUK + Unit Keys from it. Source order, cheapest-first:
|
} else {
|
||||||
// a. the disc's stored MK (hash hit) — already the Media Key.
|
|
||||||
// b. the keydb's Processing Key pool walked against this disc's MKB
|
|
||||||
// via `mk_from_pk` (Subset-Difference cvalue walk; no VID). This
|
|
||||||
// is the restored PK path — a leaked/precomputed PK resolves the
|
|
||||||
// Media Key directly for real discs.
|
|
||||||
// c. the device-key pool via `mk_from_dk` (the AACS-1.0 variant
|
|
||||||
// walk; needs the MKB and a VID, and has no in-tree integrator
|
|
||||||
// KCD so it errs for real discs today — kept for faithfulness).
|
|
||||||
// The MK itself (a/b) carries no VID, but the final `vuk_from_mk`
|
|
||||||
// needs one. Locked VID-per-path rule: physical (unlocker) VID first,
|
|
||||||
// else the keydb entry's stored VID (`I` field) for the ISO /
|
|
||||||
// non-physical path, else cannot derive.
|
|
||||||
let vid = ctx.vid().or_else(|| entry.vid.map(Vid));
|
let vid = ctx.vid().or_else(|| entry.vid.map(Vid));
|
||||||
let mkb = ctx.mkb().unwrap_or(&[]);
|
let mkb = ctx.mkb().unwrap_or(&[]);
|
||||||
|
|
||||||
let mk: Option<MediaKey> = entry
|
let mk: Option<MediaKey> = entry
|
||||||
.media_key
|
.media_key
|
||||||
.map(MediaKey)
|
.map(MediaKey)
|
||||||
// PK pool: validated against this disc's own MKB, no VID needed here.
|
|
||||||
.or_else(|| mk_from_pk(&db.processing_keys, mkb).ok())
|
.or_else(|| mk_from_pk(&db.processing_keys, mkb).ok())
|
||||||
// DK pool: mk_from_dk folds the VID into the variant walk; it needs
|
|
||||||
// the same VID the VUK step will use.
|
|
||||||
.or_else(|| vid.and_then(|v| mk_from_dk(&db.device_keys, mkb, v).ok()));
|
.or_else(|| vid.and_then(|v| mk_from_dk(&db.device_keys, mkb, v).ok()));
|
||||||
|
match (mk, vid) {
|
||||||
let Some(mk) = mk else {
|
(Some(mk), Some(vid)) => uk_from_vuk(vuk_from_mk(mk, vid), enc_title_keys),
|
||||||
return Vec::new();
|
// Locked VID-per-path rule: an MK with no VID cannot derive.
|
||||||
};
|
_ => Vec::new(),
|
||||||
let Some(vid) = vid else {
|
}
|
||||||
// Locked VID-per-path rule: an MK with no VID from either source
|
|
||||||
// cannot derive a VUK — never guess.
|
|
||||||
return Vec::new();
|
|
||||||
};
|
};
|
||||||
|
keys.extend(derived);
|
||||||
|
}
|
||||||
|
|
||||||
uk_from_vuk(vuk_from_mk(mk, vid), enc_title_keys)
|
// Unique by key value, first occurrence wins (stored numbering kept).
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
keys.retain(|u| seen.insert(u.key));
|
||||||
|
keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,15 +438,16 @@ mod tests {
|
|||||||
|
|
||||||
const HASH: &str = "0xaabb";
|
const HASH: &str = "0xaabb";
|
||||||
|
|
||||||
// ── KAT (a): disc with terminal Unit Keys ─────────────────────────────────
|
// ── KAT (a): disc with terminal Unit Keys, no enc_title_keys ──────────────
|
||||||
/// A hash hit carrying terminal unit keys is returned as-is — the committed
|
/// Stored terminal unit keys are returned with their CPS numbering preserved.
|
||||||
/// `(cps, key)` pairs are byte-identical to the keydb's stored numbering,
|
/// Here `enc_title_keys` is empty, so the VUK can't derive anything — only the
|
||||||
/// exactly what the OLD `Key::Unit(entry.unit_keys)` path committed.
|
/// stored list contributes, and it commits byte-identically to the stored
|
||||||
|
/// `(cps, key)` pairs.
|
||||||
#[test]
|
#[test]
|
||||||
fn kat_a_disc_with_unit_keys_is_terminal_and_preserves_cps_numbering() {
|
fn kat_a_disc_with_unit_keys_is_terminal_and_preserves_cps_numbering() {
|
||||||
let mut e = blank_entry(HASH);
|
let mut e = blank_entry(HASH);
|
||||||
e.unit_keys = vec![(1, [0xA0u8; 16]), (2, [0xB1u8; 16])];
|
e.unit_keys = vec![(1, [0xA0u8; 16]), (2, [0xB1u8; 16])];
|
||||||
// Even with a VUK present, the terminal UK must win (cheapest path).
|
// VUK present but no enc_title_keys → nothing to boil, stored stands.
|
||||||
e.vuk = Some([0x11u8; 16]);
|
e.vuk = Some([0x11u8; 16]);
|
||||||
let db = db_with(e, Vec::new());
|
let db = db_with(e, Vec::new());
|
||||||
|
|
||||||
@@ -466,6 +459,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Orphan-unit completeness (the real keydb bug): an entry stores only `uk1`
|
||||||
|
/// (the keyupdater sampled one reachable CPS unit) but ALSO carries the VUK,
|
||||||
|
/// which boils BOTH declared units. The old return-at-first-path handed back
|
||||||
|
/// just `[uk1]`, shadowing the VUK and silently dropping the orphan unit. The
|
||||||
|
/// union must return BOTH — the stored uk1 AND the VUK-derived second unit.
|
||||||
|
#[test]
|
||||||
|
fn union_partial_stored_plus_vuk_yields_all_declared_units() {
|
||||||
|
let vuk = [0x5Au8; 16];
|
||||||
|
let enc = vec![[0x31u8; 16], [0xCDu8; 16]]; // two declared CPS units
|
||||||
|
let derived = uk_from_vuk(Vuk(vuk), &enc); // [d0, d1]
|
||||||
|
|
||||||
|
let mut e = blank_entry(HASH);
|
||||||
|
e.unit_keys = vec![(1, [0xA0u8; 16])]; // PARTIAL: only uk1 stored
|
||||||
|
e.vuk = Some(vuk);
|
||||||
|
let db = db_with(e, Vec::new());
|
||||||
|
|
||||||
|
let got = KeydbSource::unit_keys_from(&db, &ctx(HASH, enc.clone(), None));
|
||||||
|
let got_keys: Vec<[u8; 16]> = got.iter().map(|u| u.key).collect();
|
||||||
|
assert!(
|
||||||
|
got_keys.contains(&[0xA0u8; 16]),
|
||||||
|
"the stored uk1 is kept"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got_keys.contains(&derived[1].key),
|
||||||
|
"the VUK-derived SECOND CPS unit is added, not shadowed by the partial stored list"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
got.len() >= 2,
|
||||||
|
"a partial stored list must no longer shadow the complete VUK"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── KAT (b): disc with VUK ────────────────────────────────────────────────
|
// ── KAT (b): disc with VUK ────────────────────────────────────────────────
|
||||||
/// A hash hit with only a VUK derives the terminal keys via `uk_from_vuk`
|
/// A hash hit with only a VUK derives the terminal keys via `uk_from_vuk`
|
||||||
/// over the disc's encrypted title keys — byte-identical to the OLD
|
/// over the disc's encrypted title keys — byte-identical to the OLD
|
||||||
|
|||||||
+1
-1
@@ -1475,7 +1475,7 @@ mod tests {
|
|||||||
let original = std::fs::read(&unit_path).unwrap();
|
let original = std::fs::read(&unit_path).unwrap();
|
||||||
assert_eq!(original.len(), libfreemkv::aacs::ALIGNED_UNIT_LEN);
|
assert_eq!(original.len(), libfreemkv::aacs::ALIGNED_UNIT_LEN);
|
||||||
assert!(
|
assert!(
|
||||||
libfreemkv::aacs::is_aacs_scrambled(&original),
|
libfreemkv::aacs::ts_sync_destroyed(&original),
|
||||||
"Unit should be encrypted"
|
"Unit should be encrypted"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user