mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
aacs: find a plain /AACS HD DVD dir, one stride-version resolver, report Class II MKBs
- HD DVD: the AACS directory is any root directory holding MKBROM.AACS, whatever its name. Both encrypted "300" HD DVDs use a plain /AACS (mirror AACS_BAK), which the old "name ends in !" rule missed, so they could not be keyed. A *_BAK mirror is used only when no primary copy exists. - Unit_Key_RO.inf stride: one resolver (content certificate, then a pre-recorded MKB's type, then index.bdmv INDX0300, then UHD) for the scan, read_aacs_inputs and resolve_candidate, which now takes the version instead of re-deriving it from the MKB with a V10 default. - A Class II MKB (0x000A1003) keeps its media key data in record 0x0c, which the PK/DK derivation does not read: check_mkb_class reports it as MkbClassError (E7109) and derivation stops with a warning instead of failing silently.
This commit is contained in:
+121
-41
@@ -30,8 +30,42 @@ pub(crate) struct MkbTables<'a> {
|
||||
cvalues: &'a [u8],
|
||||
}
|
||||
|
||||
/// Why an MKB cannot be derived through at all, as opposed to no key matching it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MkbClassError {
|
||||
/// Class II (`0x000A1003`): its Media Key Data is record `0x0c`, which the classical
|
||||
/// PK/DK derivation (record `0x05`) does not read.
|
||||
Unsupported(MkbType),
|
||||
}
|
||||
|
||||
impl MkbClassError {
|
||||
/// The stable numeric error code ([`E_MKB_CLASS_UNSUPPORTED`](crate::error::E_MKB_CLASS_UNSUPPORTED)).
|
||||
pub fn code(&self) -> u16 {
|
||||
crate::error::E_MKB_CLASS_UNSUPPORTED
|
||||
}
|
||||
}
|
||||
|
||||
/// `Err` when this MKB's class has no Media Key Data the PK/DK derivation can read, so a
|
||||
/// caller can report it instead of "no key matched". `Ok` for every other (or no) MKB.
|
||||
pub fn check_mkb_class(mkb: &[u8]) -> Result<(), MkbClassError> {
|
||||
match mkb_type(mkb) {
|
||||
Some(t @ MkbType::ClassII) => Err(MkbClassError::Unsupported(t)),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MkbTables<'a> {
|
||||
pub(crate) fn parse(mkb: &'a [u8]) -> Option<Self> {
|
||||
if let Err(e) = check_mkb_class(mkb) {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "mkb_class",
|
||||
error_code = e.code(),
|
||||
"Class II MKB (0x000A1003) keeps its media key data in record 0x0c; PK/DK \
|
||||
derivation reads only 0x05 and cannot derive a media key from it"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
mk_dv: mkb_find_mk_dv(mkb)?,
|
||||
uvs: find_record_slice(mkb, REC_SUBSET_DIFFERENCE)?,
|
||||
@@ -511,8 +545,9 @@ impl std::fmt::Debug for ResolvedChain {
|
||||
///
|
||||
/// Runs the deterministic derivation DOWNWARD to the disc's terminal unit keys:
|
||||
/// `DK → MK → VUK → UKs`, `PK → MK → VUK → UKs`, `MK → VUK → UKs`,
|
||||
/// `VUK → UKs`, or `UK → itself`, parsing `Unit_Key_RO.inf` at the version the
|
||||
/// disc's MKB declares so a multi-CPS disc yields all its unit keys.
|
||||
/// `VUK → UKs`, or `UK → itself`, parsing `Unit_Key_RO.inf` at `version` (from
|
||||
/// [`resolve_aacs_version`](crate::aacs::mkb::resolve_aacs_version), the resolver the scan
|
||||
/// uses) so a multi-CPS disc yields all its unit keys at the right stride.
|
||||
///
|
||||
/// PURE DERIVATION: no sampling, no validation, no position recovery. Returns `None` only when
|
||||
/// derivation itself cannot proceed.
|
||||
@@ -521,14 +556,11 @@ pub fn resolve_candidate(
|
||||
mkb: &[u8],
|
||||
unit_key_ro: &[u8],
|
||||
vid: Option<Vid>,
|
||||
version: AacsVersion,
|
||||
) -> Option<ResolvedChain> {
|
||||
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number.
|
||||
// Derive the stride version from the disc's own MKB, then defer to the shared
|
||||
// `derive_unit_keys` (the one place a VUK unwraps the unit keys).
|
||||
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number, through
|
||||
// the shared `derive_unit_keys` (the one place a VUK unwraps the unit keys).
|
||||
let boil = |vuk: Vuk| -> Option<Vec<(u32, [u8; 16])>> {
|
||||
let version = mkb_type(mkb)
|
||||
.map(|t| t.generation())
|
||||
.unwrap_or(AacsVersion::V10);
|
||||
// BD/UHD Unit_Key_RO.inf or HD DVD VTKF000.AACS — dispatched by magic.
|
||||
let ukf = parse_title_keys(unit_key_ro, version)?;
|
||||
if ukf.encrypted_keys.is_empty() {
|
||||
@@ -541,9 +573,6 @@ pub fn resolve_candidate(
|
||||
KeyCandidate::Uk(uk) => {
|
||||
// `uk.idx` is positional; surface the declared CPS-unit number like the other arms,
|
||||
// falling back to the position if the file does not parse or lacks that slot.
|
||||
let version = mkb_type(mkb)
|
||||
.map(|t| t.generation())
|
||||
.unwrap_or(AacsVersion::V10);
|
||||
let declared = parse_title_keys(unit_key_ro, version)
|
||||
.and_then(|f| f.encrypted_keys.get(uk.idx as usize).map(|k| k.0))
|
||||
.unwrap_or(uk.idx);
|
||||
@@ -602,6 +631,27 @@ mod resolve_candidate_tests {
|
||||
use super::*;
|
||||
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||
|
||||
// A Class II MKB is reported as an unsupported class (typed, E7109), not as "no key
|
||||
// matched": derivation stops before reading its tables. A pre-recorded MKB passes.
|
||||
#[test]
|
||||
fn a_class_ii_mkb_is_an_explicit_unsupported_class() {
|
||||
let mkb = |t: u32| {
|
||||
let mut m = vec![0x10, 0x00, 0x00, 0x0C];
|
||||
m.extend_from_slice(&t.to_be_bytes());
|
||||
m.extend_from_slice(&[0, 0, 0, 1]);
|
||||
m
|
||||
};
|
||||
let class2 = mkb(MKB_TYPE_10_CLASS_II);
|
||||
let err = check_mkb_class(&class2).expect_err("class II is unsupported");
|
||||
assert_eq!(err, MkbClassError::Unsupported(MkbType::ClassII));
|
||||
assert_eq!(err.code(), crate::error::E_MKB_CLASS_UNSUPPORTED);
|
||||
assert!(MkbTables::parse(&class2).is_none());
|
||||
assert_eq!(derive_media_key_from_pk(&class2, &[[0u8; 16]]), None);
|
||||
assert_eq!(check_mkb_class(&mkb(MKB_TYPE_4_PRERECORDED)), Ok(()));
|
||||
assert_eq!(check_mkb_class(&mkb(MKB_20_CATEGORY_C)), Ok(()));
|
||||
assert_eq!(check_mkb_class(&[]), Ok(()));
|
||||
}
|
||||
|
||||
// km_verifies gates every candidate Media Key; mutation testing found `-> true` surviving
|
||||
// all 2,556 tests, i.e. unverified verification.
|
||||
#[test]
|
||||
@@ -699,7 +749,8 @@ mod resolve_candidate_tests {
|
||||
let vuk = Vuk([0x33u8; 16]);
|
||||
let encs = [[0x11u8; 16], [0x22u8; 16], [0x44u8; 16]];
|
||||
let inf = synth_inf(&encs);
|
||||
let r = resolve_candidate(&KeyCandidate::Vuk(vuk), &[], &inf, None).expect("vuk derives");
|
||||
let r = resolve_candidate(&KeyCandidate::Vuk(vuk), &[], &inf, None, AacsVersion::V10)
|
||||
.expect("vuk derives");
|
||||
let cps: Vec<u32> = r.unit_keys.iter().map(|(c, _)| *c).collect();
|
||||
assert_eq!(
|
||||
cps,
|
||||
@@ -721,7 +772,8 @@ mod resolve_candidate_tests {
|
||||
#[test]
|
||||
fn resolve_candidate_uk_is_itself() {
|
||||
let uk = UnitKey::new(2, [0x9u8; 16]);
|
||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &[], None).expect("uk is terminal");
|
||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &[], None, AacsVersion::V10)
|
||||
.expect("uk is terminal");
|
||||
assert_eq!(r.unit_keys, vec![(2, uk.key)]);
|
||||
assert!(r.vuk.is_none() && r.mk.is_none());
|
||||
}
|
||||
@@ -731,7 +783,8 @@ mod resolve_candidate_tests {
|
||||
fn resolve_candidate_uk_reports_the_declared_cps_unit_number() {
|
||||
let inf = synth_inf(&[[0x11u8; 16], [0x22u8; 16]]);
|
||||
let uk = UnitKey::new(1, [0x9u8; 16]);
|
||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &inf, None).expect("terminal");
|
||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &inf, None, AacsVersion::V10)
|
||||
.expect("terminal");
|
||||
assert_eq!(r.unit_keys, vec![(2, uk.key)]);
|
||||
}
|
||||
|
||||
@@ -747,7 +800,7 @@ mod resolve_candidate_tests {
|
||||
}
|
||||
let at = |idx: u32| {
|
||||
let uk = UnitKey::new(idx, [0x9u8; 16]);
|
||||
resolve_candidate(&KeyCandidate::Uk(uk), &[], &vtkf, None)
|
||||
resolve_candidate(&KeyCandidate::Uk(uk), &[], &vtkf, None, AacsVersion::V10)
|
||||
.expect("terminal")
|
||||
.unit_keys[0]
|
||||
.0
|
||||
@@ -756,14 +809,27 @@ mod resolve_candidate_tests {
|
||||
assert_eq!(at(1), 5, "the file's number, not idx + 1");
|
||||
assert_eq!(at(7), 7, "no such slot: the position");
|
||||
let uk = UnitKey::new(3, [0x9u8; 16]);
|
||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &[0u8; 4], None).unwrap();
|
||||
let r = resolve_candidate(
|
||||
&KeyCandidate::Uk(uk),
|
||||
&[],
|
||||
&[0u8; 4],
|
||||
None,
|
||||
AacsVersion::V10,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r.unit_keys[0].0, 3, "unparseable file: the position");
|
||||
}
|
||||
|
||||
/// MK/PK/DK paths derive the VUK from a VID; without one, derivation stops.
|
||||
#[test]
|
||||
fn resolve_candidate_mk_requires_vid() {
|
||||
let r = resolve_candidate(&KeyCandidate::Mk(MediaKey([1u8; 16])), &[], &[], None);
|
||||
let r = resolve_candidate(
|
||||
&KeyCandidate::Mk(MediaKey([1u8; 16])),
|
||||
&[],
|
||||
&[],
|
||||
None,
|
||||
AacsVersion::V10,
|
||||
);
|
||||
assert!(r.is_none(), "MK path returns None without a VID");
|
||||
}
|
||||
|
||||
@@ -817,8 +883,14 @@ mod resolve_candidate_tests {
|
||||
let enc = aes_ecb_encrypt(&vuk, &plain_uk);
|
||||
let inf = synth_inf(std::slice::from_ref(&enc));
|
||||
|
||||
let r = resolve_candidate(&KeyCandidate::Pk(ProcessingKey(pk)), &mkb, &inf, Some(vid))
|
||||
.expect("planted PK resolves the full chain");
|
||||
let r = resolve_candidate(
|
||||
&KeyCandidate::Pk(ProcessingKey(pk)),
|
||||
&mkb,
|
||||
&inf,
|
||||
Some(vid),
|
||||
AacsVersion::V10,
|
||||
)
|
||||
.expect("planted PK resolves the full chain");
|
||||
assert_eq!(r.mk, Some(MediaKey(mk)), "PK recovers the planted MK");
|
||||
assert_eq!(r.unit_keys.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -982,12 +1054,11 @@ mod resolve_candidate_tests {
|
||||
assert_ne!(v10.encrypted_keys[1].1, v20.encrypted_keys[1].1);
|
||||
}
|
||||
|
||||
/// `resolve_candidate` derives the inf parse stride from the disc's OWN MKB
|
||||
/// (`mkb_type(mkb).generation()`), NOT a fixed default: a V20 (Category C)
|
||||
/// MKB boils the second unit key at the 64-byte stride, while an absent MKB
|
||||
/// falls back to V10's 48-byte stride.
|
||||
/// `resolve_candidate` parses the inf at the version it is given (from the shared
|
||||
/// `resolve_aacs_version`), not at its own guess from the MKB: an unreadable MKB on a
|
||||
/// UHD still reads the second key at +64, and a V10 cert outranks a 2.0 MKB.
|
||||
#[test]
|
||||
fn resolve_candidate_boils_at_the_mkb_declared_stride() {
|
||||
fn resolve_candidate_boils_at_the_resolved_version_stride() {
|
||||
// A two-key inf whose 2nd key differs by stride (see the parse test).
|
||||
let uk_pos = 0x20usize;
|
||||
let key0 = uk_pos + 48;
|
||||
@@ -1011,23 +1082,26 @@ mod resolve_candidate_tests {
|
||||
Some(AacsVersion::V20),
|
||||
"fixture check: this MKB declares AACS 2.0"
|
||||
);
|
||||
let r20 = resolve_candidate(&KeyCandidate::Vuk(vuk), &v20_mkb, &inf, None)
|
||||
.expect("V20 MKB boils the inf");
|
||||
let boil = |mkb: &[u8], version| {
|
||||
resolve_candidate(&KeyCandidate::Vuk(vuk), mkb, &inf, None, version)
|
||||
.expect("boils the inf")
|
||||
.unit_keys[1]
|
||||
.1
|
||||
};
|
||||
let at64 = decrypt_unit_key(&vuk.0, &[0x20; 16]);
|
||||
let at48 = decrypt_unit_key(&vuk.0, &[0x10; 16]);
|
||||
// The stride follows the version handed in, never a re-read of the MKB: a UHD whose
|
||||
// MKB is unreadable (empty) still reads key 2 at +64 ...
|
||||
let uhd = resolve_aacs_version(None, &[], Some(true));
|
||||
assert_eq!(boil(&[], uhd), at64);
|
||||
// ... and when the certificate (V10) disagrees with the MKB (2.0), the cert wins.
|
||||
let bd = resolve_aacs_version(Some(1), &v20_mkb, Some(true));
|
||||
assert_eq!(bd, AacsVersion::V10);
|
||||
assert_eq!(boil(&v20_mkb, bd), at48);
|
||||
assert_eq!(
|
||||
r20.unit_keys[1].1,
|
||||
decrypt_unit_key(&vuk.0, &[0x20; 16]),
|
||||
"the V20 MKB drives the 64-byte stride, reading the 2nd key at +64"
|
||||
boil(&v20_mkb, resolve_aacs_version(None, &v20_mkb, None)),
|
||||
at64
|
||||
);
|
||||
|
||||
// No MKB → mkb_type is None → the V10 48-byte fallback stride.
|
||||
let r10 = resolve_candidate(&KeyCandidate::Vuk(vuk), &[], &inf, None)
|
||||
.expect("absent MKB falls back to V10");
|
||||
assert_eq!(
|
||||
r10.unit_keys[1].1,
|
||||
decrypt_unit_key(&vuk.0, &[0x10; 16]),
|
||||
"an absent MKB falls back to the V10 48-byte stride, reading +48"
|
||||
);
|
||||
assert_ne!(r20.unit_keys[1].1, r10.unit_keys[1].1);
|
||||
}
|
||||
|
||||
/// PIN: a `Mk` candidate is PURE DERIVATION — `resolve_candidate` does NOT
|
||||
@@ -1059,8 +1133,14 @@ mod resolve_candidate_tests {
|
||||
let enc = [0x9Au8; 16];
|
||||
let inf = synth_inf(std::slice::from_ref(&enc));
|
||||
|
||||
let r = resolve_candidate(&KeyCandidate::Mk(wrong_mk), &mkb, &inf, Some(vid))
|
||||
.expect("resolve_candidate boils regardless of MK-vs-MKB validity");
|
||||
let r = resolve_candidate(
|
||||
&KeyCandidate::Mk(wrong_mk),
|
||||
&mkb,
|
||||
&inf,
|
||||
Some(vid),
|
||||
AacsVersion::V10,
|
||||
)
|
||||
.expect("resolve_candidate boils regardless of MK-vs-MKB validity");
|
||||
// The chain is derived straight from the (wrong) MK, no gate applied.
|
||||
let expected_vuk = derive_vuk(&wrong_mk.0, &vid.0);
|
||||
assert_eq!(
|
||||
|
||||
+58
-1
@@ -148,6 +148,29 @@ impl AacsVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// The AACS generation that sets the `Unit_Key_RO.inf` stride, from the strongest signal
|
||||
/// present: the content certificate's major version, then a pre-recorded MKB's type, then
|
||||
/// `index.bdmv` (`INDX0300` is UHD). With none of them, UHD's 64-byte stride. The one
|
||||
/// resolver for every path that parses the unit-key file, so they cannot disagree.
|
||||
pub fn resolve_aacs_version(
|
||||
cert_major: Option<u8>,
|
||||
mkb: &[u8],
|
||||
index_is_uhd: Option<bool>,
|
||||
) -> AacsVersion {
|
||||
if let Some(major) = cert_major {
|
||||
return AacsVersion::from_major(major);
|
||||
}
|
||||
if let Some(t @ (MkbType::Prerecorded | MkbType::CategoryC20 | MkbType::CategoryC21)) =
|
||||
mkb_type(mkb)
|
||||
{
|
||||
return t.generation();
|
||||
}
|
||||
match index_is_uhd {
|
||||
Some(false) => AacsVersion::V10,
|
||||
_ => AacsVersion::V20,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB.
|
||||
/// 0x81: `[C]` §3.2.5.1.4. 0x86 (AACS 2.x): `[RE]` — not in the public spec (from real 2.x MKBs).
|
||||
pub(crate) fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||
@@ -187,7 +210,8 @@ pub(crate) fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||
}
|
||||
|
||||
// Find the Media Key Data Record (cvalues table) in an MKB. `[C]` §3.2.4 / §3.2.5.1.7. 0x05 is
|
||||
// the only cvalue table (libaacs `mkb_cvalues`); 0x07 is the Subset-Difference Index.
|
||||
// the pre-recorded / Category C table (libaacs `mkb_cvalues`); a Class II MKB carries 0x0c
|
||||
// instead (see `check_mkb_class`). 0x07 is the Subset-Difference Index.
|
||||
pub(crate) fn mkb_find_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||
find_record_body(mkb, REC_MEDIA_KEY_DATA)
|
||||
}
|
||||
@@ -335,6 +359,39 @@ pub fn mkb_is_uhd(mkb: &[u8]) -> Option<bool> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// One stride resolver: cert, then a pre-recorded MKB's type, then index.bdmv, then UHD.
|
||||
// Each later signal is consulted only when every earlier one is missing.
|
||||
#[test]
|
||||
fn resolve_aacs_version_ranks_cert_then_mkb_then_index() {
|
||||
let rec = |t: u32| {
|
||||
let mut m = vec![0x10, 0x00, 0x00, 0x0C];
|
||||
m.extend_from_slice(&t.to_be_bytes());
|
||||
m.extend_from_slice(&[0, 0, 0, 1]);
|
||||
m
|
||||
};
|
||||
let (bd, uhd21) = (rec(MKB_TYPE_4_PRERECORDED), rec(MKB_21_CATEGORY_C));
|
||||
let class2 = rec(MKB_TYPE_10_CLASS_II);
|
||||
use AacsVersion::*;
|
||||
// The certificate wins over a disagreeing MKB and index.
|
||||
assert_eq!(
|
||||
resolve_aacs_version(Some(AACS_MAJOR_BD), &uhd21, Some(true)),
|
||||
V10
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_aacs_version(Some(AACS_MAJOR_UHD), &bd, Some(false)),
|
||||
V20
|
||||
);
|
||||
// No cert: the MKB type wins over a disagreeing index.
|
||||
assert_eq!(resolve_aacs_version(None, &bd, Some(true)), V10);
|
||||
assert_eq!(resolve_aacs_version(None, &uhd21, Some(false)), V21);
|
||||
// No cert, no pre-recorded MKB type: index.bdmv decides.
|
||||
assert_eq!(resolve_aacs_version(None, &[], Some(false)), V10);
|
||||
assert_eq!(resolve_aacs_version(None, &class2, Some(false)), V10);
|
||||
assert_eq!(resolve_aacs_version(None, &[], Some(true)), V20);
|
||||
// Nothing at all: UHD's 64-byte stride.
|
||||
assert_eq!(resolve_aacs_version(None, &[], None), V20);
|
||||
}
|
||||
|
||||
/// One MKB record: 1 type byte + big-endian 24-bit total length + body.
|
||||
fn rec(rec_type: u8, body: &[u8]) -> Vec<u8> {
|
||||
let len = 4 + body.len();
|
||||
|
||||
+87
-8
@@ -45,16 +45,22 @@ pub enum AacsRole {
|
||||
ContentCert,
|
||||
}
|
||||
|
||||
// The HD DVD AACS directory in a parsed UDF tree, if present. Identified structurally (name
|
||||
// ends `!`, contains MKBROM.AACS), not by a hardcoded name.
|
||||
// The HD DVD AACS directory in a parsed UDF tree, if present: any root directory holding
|
||||
// MKBROM.AACS, whatever its name (`ANY!`, `AAC!`, plain `AACS` on "300"). A `*_BAK` mirror
|
||||
// is used only when no primary copy exists.
|
||||
pub(crate) fn find_hddvd_aacs_dir(udf: &crate::udf::UdfFs) -> Option<&crate::udf::DirEntry> {
|
||||
udf.root.entries.iter().find(|e| {
|
||||
let mut dirs = udf.root.entries.iter().filter(|e| {
|
||||
e.is_dir
|
||||
&& e.name.ends_with('!')
|
||||
&& e.entries
|
||||
.iter()
|
||||
.any(|c| !c.is_dir && c.name.eq_ignore_ascii_case("MKBROM.AACS"))
|
||||
})
|
||||
});
|
||||
let is_bak = |e: &crate::udf::DirEntry| e.name.to_ascii_uppercase().ends_with("_BAK");
|
||||
let first = dirs.next()?;
|
||||
if !is_bak(first) {
|
||||
return Some(first);
|
||||
}
|
||||
dirs.find(|e| !is_bak(e)).or(Some(first))
|
||||
}
|
||||
|
||||
// Ordered candidate paths for an AACS key role: fixed BD/UHD `/AACS/…` paths
|
||||
@@ -252,6 +258,7 @@ mod tests {
|
||||
&[],
|
||||
&[],
|
||||
None,
|
||||
super::mkb::AacsVersion::V10,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,7 +307,7 @@ mod tests {
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
|
||||
// Discovered structurally (ends in '!', holds MKBROM.AACS) — the real
|
||||
// Discovered structurally (holds MKBROM.AACS) — the real
|
||||
// AACS dir, never the `_BAK` mirror.
|
||||
let dir = super::find_hddvd_aacs_dir(&udf).expect("aacs dir");
|
||||
assert_eq!(dir.name, "AAC!");
|
||||
@@ -330,8 +337,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Both the `!`-suffix AND MKBROM.AACS presence are required (conjunction, not disjunction)
|
||||
// — else the HD DVD path resolves key files under a dir holding none.
|
||||
// "300" HD DVDs keep MKBROM/VTKF/CONTENT_CERT in a plain `/AACS` (mirror `AACS_BAK`):
|
||||
// found by MKBROM.AACS, not the name, and the mirror is skipped even when listed first.
|
||||
#[test]
|
||||
fn a_plain_aacs_directory_holding_mkbrom_is_the_hddvd_aacs_directory() {
|
||||
use crate::udf::fixture::*;
|
||||
let mut disc = MemDisc::new();
|
||||
let dir = |name: &str, lba: u32, files| DirSpec {
|
||||
name: name.to_string(),
|
||||
icb_lba: lba,
|
||||
dir_data_lba: lba + 1,
|
||||
files,
|
||||
subdirs: vec![],
|
||||
};
|
||||
let root = DirSpec {
|
||||
name: String::new(),
|
||||
icb_lba: 10,
|
||||
dir_data_lba: 11,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![
|
||||
dir(
|
||||
"AACS_BAK",
|
||||
30,
|
||||
vec![file("MKBROM.AACS", 110, 6000, 4096, true)],
|
||||
),
|
||||
dir(
|
||||
"AACS",
|
||||
20,
|
||||
vec![
|
||||
file("MKBROM.AACS", 100, 5000, 4096, true),
|
||||
file("CONTENT_CERT.AACS", 101, 5100, 2048, true),
|
||||
file("VTKF000.AACS", 102, 5200, 2048, true),
|
||||
],
|
||||
),
|
||||
],
|
||||
};
|
||||
build_udf_skeleton(&mut disc, 10);
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
assert_eq!(super::find_hddvd_aacs_dir(&udf).expect("dir").name, "AACS");
|
||||
let mkb = super::role_paths(&udf, super::AacsRole::Mkb);
|
||||
assert_eq!(mkb.last().unwrap(), "/AACS/MKBROM.AACS");
|
||||
let uk = super::role_paths(&udf, super::AacsRole::UnitKey);
|
||||
assert_eq!(uk.last().unwrap(), "/AACS/VTKF000.AACS");
|
||||
}
|
||||
|
||||
// A mirror alone is still used: a damaged primary must not leave the disc unkeyable.
|
||||
#[test]
|
||||
fn a_lone_bak_mirror_is_used_when_no_primary_exists() {
|
||||
use crate::udf::fixture::*;
|
||||
let mut disc = MemDisc::new();
|
||||
let root = DirSpec {
|
||||
name: String::new(),
|
||||
icb_lba: 10,
|
||||
dir_data_lba: 11,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![DirSpec {
|
||||
name: "ANY!_BAK".to_string(),
|
||||
icb_lba: 20,
|
||||
dir_data_lba: 21,
|
||||
files: vec![file("MKBROM.AACS", 100, 5000, 4096, true)],
|
||||
subdirs: vec![],
|
||||
}],
|
||||
};
|
||||
build_udf_skeleton(&mut disc, 10);
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
assert_eq!(
|
||||
super::find_hddvd_aacs_dir(&udf).expect("dir").name,
|
||||
"ANY!_BAK"
|
||||
);
|
||||
}
|
||||
|
||||
// MKBROM.AACS presence is required — else the HD DVD path resolves key files under a
|
||||
// dir holding none.
|
||||
#[test]
|
||||
fn a_bang_suffixed_directory_without_mkbrom_is_not_the_aacs_directory() {
|
||||
use crate::udf::fixture::*;
|
||||
|
||||
+35
-9
@@ -127,12 +127,21 @@ pub(crate) fn handshake_class_error(e: &Error) -> Option<Error> {
|
||||
}
|
||||
}
|
||||
|
||||
// `Some(true)` for an `INDX0300` index.bdmv, `Some(false)` for another INDX version, `None`
|
||||
// when there is no readable index (HD DVD, damage).
|
||||
pub(super) fn index_is_uhd(udf_fs: &udf::UdfFs, reader: &mut dyn SectorSource) -> Option<bool> {
|
||||
let index = udf_fs
|
||||
.read_file_prefix(reader, "/BDMV/index.bdmv", 8)
|
||||
.ok()?;
|
||||
let version = index.get(..8)?.strip_prefix(b"INDX")?;
|
||||
Some(version == b"0300")
|
||||
}
|
||||
|
||||
// Positive UHD evidence independent of the cert: index.bdmv version "0300"
|
||||
// (BD-ROM Part 3) or an AACS2 MKB. Unknown is `false`.
|
||||
fn disc_is_uhd(udf_fs: &udf::UdfFs, reader: &mut dyn SectorSource) -> bool {
|
||||
use crate::aacs::mkb::{AacsVersion, mkb_type};
|
||||
let index = udf_fs.read_file_prefix(reader, "/BDMV/index.bdmv", 8);
|
||||
if index.is_ok_and(|d| d.get(..8) == Some(&b"INDX0300"[..])) {
|
||||
if index_is_uhd(udf_fs, reader) == Some(true) {
|
||||
return true;
|
||||
}
|
||||
udf_fs
|
||||
@@ -205,11 +214,6 @@ pub(super) fn capture(
|
||||
}
|
||||
};
|
||||
let cc = cc_raw.as_deref().and_then(aacs::inf::parse_content_cert);
|
||||
// No-cert default = UHD (V20 stride), matching `read_aacs_version`.
|
||||
let version = cc
|
||||
.as_ref()
|
||||
.map(|c| c.version.major())
|
||||
.unwrap_or(aacs::mkb::AACS_MAJOR_UHD);
|
||||
// Fail safe: an unparseable cert, or none on a live disc known to be UHD, may mean bus encryption.
|
||||
let bus_encryption = match &cc {
|
||||
Some(c) => c.bus_encryption,
|
||||
@@ -229,6 +233,12 @@ pub(super) fn capture(
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
// Stride version through the shared resolver (cert, then MKB type, then index.bdmv),
|
||||
// the same one `read_aacs_version` uses.
|
||||
let index = index_is_uhd(udf_fs, reader);
|
||||
let version =
|
||||
aacs::mkb::resolve_aacs_version(cc.as_ref().map(|c| c.version.major()), &mkb, index)
|
||||
.major();
|
||||
Ok(AacsCapture {
|
||||
uk_ro,
|
||||
cert_bee: cc.map(|c| c.bus_encryption),
|
||||
@@ -829,8 +839,24 @@ mod tests {
|
||||
assert!(st.bus_encryption, "cert bus_encryption bit must propagate");
|
||||
}
|
||||
|
||||
// No content cert → version defaults to UHD (major 2), matching read_aacs_version (audit
|
||||
// #4). bus_encryption false (unreadable → off).
|
||||
// No content cert: the stride version comes from the shared resolver, so an INDX0200
|
||||
// index makes it a BD (major 1) rather than the UHD default.
|
||||
#[test]
|
||||
fn vid_only_no_cert_takes_the_version_from_index_bdmv() {
|
||||
let mut disc = MemDisc::new();
|
||||
let uk = [AacsFile {
|
||||
name: "Unit_Key_RO.inf",
|
||||
icb_lba: 60,
|
||||
data_lba: 5000,
|
||||
contents: vec![0xAB; 32],
|
||||
}];
|
||||
let udf = build_aacs_fs_with_index(&mut disc, &uk, Some(b"INDX0200"));
|
||||
let st = vid_only(&udf, &mut disc, &BusOutcome::FileOrIso).expect("state");
|
||||
assert_eq!(st.version, aacs::mkb::AACS_MAJOR_BD);
|
||||
}
|
||||
|
||||
// No content cert, MKB or index → version defaults to UHD (major 2), matching
|
||||
// read_aacs_version (audit #4). bus_encryption false (unreadable → off).
|
||||
#[test]
|
||||
fn vid_only_no_cert_defaults_version_uhd() {
|
||||
let mut disc = MemDisc::new();
|
||||
|
||||
+18
-20
@@ -518,10 +518,9 @@ pub struct Extent {
|
||||
pub sector_count: u32,
|
||||
}
|
||||
|
||||
// THE ONE definition of "structurally AACS-encrypted" — shared by the fast identify and the
|
||||
// full scan so they can't silently desync. Structural, not cryptographic; HD DVD's `X!` dir
|
||||
// is found by the same discovery the key-file reads use. Only root `AACS/` counts: the key
|
||||
// readers (and libaacs) read nothing from `/BDMV/AACS`, and no disc has one.
|
||||
// THE ONE definition of "structurally AACS-encrypted", shared by identify and the full scan.
|
||||
// Root `AACS/` or the HD DVD dir the key reads use; never `/BDMV/AACS`, which no key reader
|
||||
// (nor libaacs) reads and no disc has.
|
||||
pub(crate) fn aacs_dir_present(udf_fs: &crate::udf::UdfFs) -> bool {
|
||||
udf_fs.find_dir("/AACS").is_some() || crate::aacs::find_hddvd_aacs_dir(udf_fs).is_some()
|
||||
}
|
||||
@@ -2239,32 +2238,31 @@ impl Disc {
|
||||
|p| udf_fs.read_file(reader, p),
|
||||
)?;
|
||||
let mkb = Self::read_mkb_content(reader, udf_fs)?;
|
||||
let version = Self::read_aacs_version(reader, udf_fs);
|
||||
let version = Self::read_aacs_version(reader, udf_fs, &mkb);
|
||||
Ok((inf, mkb, version))
|
||||
}
|
||||
|
||||
// AACS major version from the content certificate; drives the Unit_Key_RO.inf parse stride.
|
||||
// Defaults to UHD (V20, 64-byte stride) when unreadable.
|
||||
fn read_aacs_version(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs) -> u8 {
|
||||
match crate::aacs::read_first(
|
||||
// AACS major version driving the Unit_Key_RO.inf parse stride: the content certificate,
|
||||
// then the MKB type, then index.bdmv, through the shared `resolve_aacs_version`.
|
||||
fn read_aacs_version(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs, mkb: &[u8]) -> u8 {
|
||||
let cert_major = crate::aacs::read_first(
|
||||
&crate::aacs::role_paths(udf_fs, crate::aacs::AacsRole::ContentCert),
|
||||
|p| udf_fs.read_file(reader, p),
|
||||
)
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(crate::aacs::inf::parse_content_cert)
|
||||
{
|
||||
Some(c) => c.version.major(),
|
||||
None => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_version",
|
||||
"no readable AACS content certificate; defaulting to the V20/UHD \
|
||||
Unit_Key_RO stride (a VUK-from-server path would otherwise mis-stride)"
|
||||
);
|
||||
crate::aacs::mkb::AACS_MAJOR_UHD
|
||||
}
|
||||
.map(|c| c.version.major());
|
||||
if cert_major.is_none() {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_version",
|
||||
"no readable AACS content certificate; Unit_Key_RO stride from the MKB type \
|
||||
or index.bdmv"
|
||||
);
|
||||
}
|
||||
let index = encrypt::index_is_uhd(udf_fs, reader);
|
||||
crate::aacs::mkb::resolve_aacs_version(cert_major, mkb, index).major()
|
||||
}
|
||||
|
||||
// Reads the AACS MKB's real record stream — NOT its ~128 MiB zero padding. Reads a bounded,
|
||||
|
||||
@@ -150,6 +150,10 @@ pub const E_MKB_VARIANT_ONLINE_CHALLENGE: u16 = 7104;
|
||||
pub const E_MKB_VARIANT_TABLE_UNAVAILABLE: u16 = 7106;
|
||||
pub const E_MKB_VARIANT_VKD_RANGE: u16 = 7107;
|
||||
pub const E_MKB_VARIANT_VERIFY_FAILED: u16 = 7108;
|
||||
/// The disc's MKB is Class II (`0x000A1003`): its Media Key Data is record `0x0c`, not the
|
||||
/// classical `0x05` the PK/DK derivation reads, so no key can be derived from it
|
||||
/// (`MkbClassError`).
|
||||
pub const E_MKB_CLASS_UNSUPPORTED: u16 = 7109;
|
||||
|
||||
// Keydb (8xxx)
|
||||
pub const E_KEYDB_CONNECT: u16 = 8000;
|
||||
|
||||
Reference in New Issue
Block a user