From a9195824ffb88c9ce880258f7c2cb044ba05f004 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:41:27 -0700 Subject: [PATCH] Trim MKB to real record length; empty unit-keys means no keys - mkb_content_len walks the MKB record stream and returns where it ends, so callers can drop the trailing zero padding. MKB files are allocated to a fixed size (~128 MiB) with records at the front; both MKB_RO and MKB_RW can be padded. read_aacs_inputs* now trim to the real records (~few MB) instead of shipping ~128 MiB of zeros. - decrypt_keys() returns None when unit_keys is empty: an AACS state that carries only a VID (out-of-band resolution, pre-key) is 'encrypted, no keys', not a usable empty key set. --- src/aacs/keys.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/aacs/mod.rs | 4 ++-- src/disc/mod.rs | 33 +++++++++++++++++++++++++-------- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index 63c65af..85e74da 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -532,6 +532,27 @@ fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option> { None } +/// Real content length of an MKB: the byte offset where the record stream +/// ends. MKB files (especially `MKB_RW.inf`, but `MKB_RO.inf` too on some +/// discs) are allocated to a fixed size — often ~128 MiB — with the records at +/// the front and the rest zero padding. Walking records (type+len) and stopping +/// at the first padding byte (`type == 0` / zero-length / overrun) gives the +/// actual size so callers can trim off megabytes of zeros before sending or +/// archiving. Returns `mkb.len()` only if the whole buffer parsed as records. +pub fn mkb_content_len(mkb: &[u8]) -> usize { + let mut pos = 0; + while pos + 4 <= mkb.len() { + let rec_type = mkb[pos]; + let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize; + // A zero type, a zero/short length, or an overrun = records done, padding begun. + if rec_type == 0x00 || rec_len < 4 || pos + rec_len > mkb.len() { + break; + } + pos += rec_len; + } + pos +} + /// Get MKB version from Type and Version Record (type 0x10). /// Version is a BE u32 at offset 8 of the record body (offset 12 from `pos`). pub fn mkb_version(mkb: &[u8]) -> Option { @@ -1374,6 +1395,26 @@ mod tests { assert_eq!(mkb_version(&mkb), Some(77)); } + #[test] + fn mkb_content_len_trims_trailing_padding() { + // Two real records (0x10 type/version + 0x86 verify), then 128 KiB of + // zero padding (the fixed-region tail). Content length must stop at the + // end of the records, not include the padding. + let mut mkb = vec![ + 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, + ]; + mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]); + mkb.extend_from_slice(&[0xAB; 16]); + mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + let records_len = mkb.len(); + mkb.extend(std::iter::repeat(0u8).take(128 * 1024)); // padding + assert_eq!(mkb_content_len(&mkb), records_len); + // No padding → returns the full length. + assert_eq!(mkb_content_len(&mkb[..records_len]), records_len); + // Empty → 0. + assert_eq!(mkb_content_len(&[]), 0); + } + #[test] fn mkb_version_returns_none_on_empty() { assert_eq!(mkb_version(&[]), None); diff --git a/src/aacs/mod.rs b/src/aacs/mod.rs index 0033383..1d67836 100644 --- a/src/aacs/mod.rs +++ b/src/aacs/mod.rs @@ -31,8 +31,8 @@ pub use keys::probe; pub use keys::{ AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key, derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex, - mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, - resolve_keys_v2, resolve_keys_v21, + mkb_content_len, mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, + resolve_keys_v1, resolve_keys_v2, resolve_keys_v21, }; pub use provider::KeyProvider; pub use variants::{ diff --git a/src/disc/mod.rs b/src/disc/mod.rs index da15e20..fc41933 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -1173,13 +1173,18 @@ impl Disc { .read_file(&mut reader, "/AACS/Unit_Key_RO.inf") .or_else(|_| udf_fs.read_file(&mut reader, "/AACS/DUPLICATE/Unit_Key_RO.inf")) .map_err(|_| Error::AacsNoKeys)?; - // Prefer MKB_RO: it's the real, correctly-sized MKB. MKB_RW is a - // fixed ~128 MiB rewritable region that is mostly zero padding — reading - // it ships 124 MiB of nothing. Fall back to RW only if RO is absent. - let mkb = udf_fs + // Prefer MKB_RO, fall back to MKB_RW, then TRIM to the real record + // length. Both files are allocated to a fixed ~128 MiB and zero-padded, + // so reading either ships up to ~124 MiB of nothing — trim to the + // record stream so callers send/store a few MB, not 128 MiB. + let mut mkb = udf_fs .read_file(&mut reader, "/AACS/MKB_RO.inf") .or_else(|_| udf_fs.read_file(&mut reader, "/AACS/MKB_RW.inf")) .map_err(|_| Error::AacsNoKeys)?; + let n = crate::aacs::mkb_content_len(&mkb); + if n > 0 && n < mkb.len() { + mkb.truncate(n); + } Ok((inf, mkb)) } @@ -1194,13 +1199,18 @@ impl Disc { .read_file(&mut reader, "/AACS/Unit_Key_RO.inf") .or_else(|_| udf_fs.read_file(&mut reader, "/AACS/DUPLICATE/Unit_Key_RO.inf")) .map_err(|_| Error::AacsNoKeys)?; - // Prefer MKB_RO: it's the real, correctly-sized MKB. MKB_RW is a - // fixed ~128 MiB rewritable region that is mostly zero padding — reading - // it ships 124 MiB of nothing. Fall back to RW only if RO is absent. - let mkb = udf_fs + // Prefer MKB_RO, fall back to MKB_RW, then TRIM to the real record + // length. Both files are allocated to a fixed ~128 MiB and zero-padded, + // so reading either ships up to ~124 MiB of nothing — trim to the + // record stream so callers send/store a few MB, not 128 MiB. + let mut mkb = udf_fs .read_file(&mut reader, "/AACS/MKB_RO.inf") .or_else(|_| udf_fs.read_file(&mut reader, "/AACS/MKB_RW.inf")) .map_err(|_| Error::AacsNoKeys)?; + let n = crate::aacs::mkb_content_len(&mkb); + if n > 0 && n < mkb.len() { + mkb.truncate(n); + } Ok((inf, mkb)) } @@ -1471,6 +1481,13 @@ impl Disc { /// Used by disc-to-ISO and other full-disc operations. pub fn decrypt_keys(&self) -> crate::decrypt::DecryptKeys { if let Some(ref aacs) = self.aacs { + // An AACS state with NO unit keys is "encrypted, no keys" — e.g. + // the VID-only state from out-of-band resolution before a Unit Key + // is supplied. Report None so callers treat it as missing keys + // (not a usable, empty key set). + if aacs.unit_keys.is_empty() { + return crate::decrypt::DecryptKeys::None; + } crate::decrypt::DecryptKeys::Aacs { unit_keys: aacs.unit_keys.clone(), read_data_key: aacs.read_data_key,