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:
Matthew Jackson
2026-10-04 11:31:30 -07:00
parent 318a1d6768
commit 5d3bc2edb3
6 changed files with 323 additions and 79 deletions
+121 -41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+4
View File
@@ -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;