From b7405e2d27c366e209c1fd4f0b4eced7484e28b5 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 5 Jun 2026 05:06:15 -0700 Subject: [PATCH] mux: fail iso:// with no usable AACS key instead of muxing garbage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an AACS-encrypted ISO is muxed with decryption requested (not --raw) but key resolution yielded no usable key, input() proceeded to mux the still-encrypted stream — emitting ~100 MB of garbage (no TS syncs, demuxer emits nothing) and sometimes spinning for tens of minutes. Add a cheap result-check in resolve::input()'s Iso branch via the pure predicate aacs_key_missing(raw, has_aacs, keys): when decryption is requested AND the disc carries AACS state AND decrypt_keys() is None, return new Error::NoDiscKey { disc_hash } (E7022) before muxing. The 40-hex disc hash is sourced from AacsState::disc_hash. --raw and non-AACS (unencrypted / CSS) discs are unaffected. Unit-tested. --- src/error.rs | 13 ++++++++ src/mux/resolve.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/src/error.rs b/src/error.rs index afb2790..37eead9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -78,6 +78,7 @@ pub const E_AACS_MK_UNAVAILABLE: u16 = 7018; pub const E_AACS_VUK_NOT_IN_KEYDB: u16 = 7019; pub const E_DRIVE_PROFILE_MISSING: u16 = 7020; pub const E_VID_CDB_UNAVAILABLE: u16 = 7021; +pub const E_NO_DISC_KEY: u16 = 7022; // Keydb (8xxx) pub const E_KEYDB_CONNECT: u16 = 8000; @@ -253,6 +254,16 @@ pub enum Error { /// template (older profile blob, or a drive class without an OEM /// VID path). VidCdbUnavailable, + /// The disc is AACS-encrypted and decryption was requested, but key + /// resolution produced no usable key for it — so muxing would emit + /// undecryptable garbage. Distinct from [`Error::KeydbLoad`] (no keydb + /// file at all): a keydb may be present but lack an entry for this disc. + /// `disc_hash` is the 40-hex SHA1 of `Unit_Key_RO.inf` (no `0x` prefix) + /// so the application can name the disc; empty if the hash wasn't + /// captured at scan. + NoDiscKey { + disc_hash: String, + }, // Keydb (8xxx) KeydbConnect { @@ -345,6 +356,7 @@ impl Error { Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB, Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING, Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE, + Error::NoDiscKey { .. } => E_NO_DISC_KEY, Error::KeydbConnect { .. } => E_KEYDB_CONNECT, Error::KeydbHttp { .. } => E_KEYDB_HTTP, Error::KeydbInvalid => E_KEYDB_INVALID, @@ -471,6 +483,7 @@ impl std::fmt::Display for Error { Error::StreamUrlMissingPort { addr } => write!(f, "E{}: {}", self.code(), addr), Error::PesFrameTooLarge { size } => write!(f, "E{}: {}", self.code(), size), Error::IsoTooLarge { path } => write!(f, "E{}: {}", self.code(), path), + Error::NoDiscKey { disc_hash } => write!(f, "E{}: {}", self.code(), disc_hash), _ => write!(f, "E{}", self.code()), } } diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index e71fa8a..e8ee601 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -172,6 +172,22 @@ pub struct InputOptions { pub raw: bool, } +/// Decide whether an ISO mux must abort for lack of a usable AACS key. +/// +/// Returns `true` only when ALL hold: decryption is requested (`!raw`), the +/// disc carries AACS state (`has_aacs` — AACS-encrypted, not CSS/unencrypted), +/// and key resolution produced no usable key (`keys` is +/// [`crate::decrypt::DecryptKeys::None`]). In that case muxing would emit +/// undecryptable garbage, so the caller fails fast with [`Error::NoDiscKey`]. +/// +/// `--raw` (raw=true) always returns `false` — raw intentionally skips +/// decryption and needs no key. A non-AACS disc (`has_aacs=false`) always +/// returns `false`: unencrypted content has `None` keys legitimately, and CSS +/// DVDs resolve to `DecryptKeys::Css{..}` (never `None`). +fn aacs_key_missing(raw: bool, has_aacs: bool, keys: &crate::decrypt::DecryptKeys) -> bool { + !raw && has_aacs && matches!(keys, crate::decrypt::DecryptKeys::None) +} + /// Open a PES input stream (produces PES frames). pub fn input(url: &str, opts: &InputOptions) -> io::Result> { let parsed = parse_url(url); @@ -205,6 +221,23 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result io::Error { e.into() })?; } + // No-key guard: if decryption is requested (not --raw) and the disc + // is AACS-encrypted but key resolution yielded no usable key, FAIL + // here — muxing an undecryptable stream produces ~100 MB of garbage + // (encrypted m2ts → no TS syncs → demuxer emits nothing). A cheap + // result-check on `decrypt_keys()`; no probe decryption needed. + // CSS (DVD) decrypts from compiled keys (`decrypt_keys()` returns + // `Css{..}`, never `None`), so this gate is AACS-only via `disc.aacs`. + if aacs_key_missing(opts.raw, disc.aacs.is_some(), &disc.decrypt_keys()) { + // Surface the disc hash (40-hex, no `0x` prefix) so the caller + // can name the disc. Empty if scan didn't capture it. + let disc_hash = disc + .aacs + .as_ref() + .map(|a| a.disc_hash.trim_start_matches("0x").to_string()) + .unwrap_or_default(); + return Err(crate::error::Error::NoDiscKey { disc_hash }.into()); + } if disc.titles.is_empty() { return Err(crate::error::Error::NoStreams.into()); } @@ -457,3 +490,50 @@ fn build_m2ts_pipeline( pid_to_track, )) } + +#[cfg(test)] +mod tests { + use super::aacs_key_missing; + use crate::decrypt::DecryptKeys; + + fn aacs_keys() -> DecryptKeys { + DecryptKeys::Aacs { + unit_keys: vec![(1, [0x11u8; 16])], + read_data_key: None, + } + } + + fn css_keys() -> DecryptKeys { + DecryptKeys::Css { + title_key: [0u8; 5], + } + } + + #[test] + fn encrypted_no_key_aborts() { + // AACS disc, decryption requested, resolver yielded no key → abort. + assert!(aacs_key_missing(false, true, &DecryptKeys::None)); + } + + #[test] + fn encrypted_with_key_proceeds() { + // AACS disc with a usable key → proceed. + assert!(!aacs_key_missing(false, true, &aacs_keys())); + } + + #[test] + fn not_encrypted_proceeds() { + // No AACS state: unencrypted (None keys) and CSS (Css keys) both OK. + assert!(!aacs_key_missing(false, false, &DecryptKeys::None)); + assert!(!aacs_key_missing(false, false, &css_keys())); + } + + #[test] + fn raw_never_aborts() { + // --raw skips decryption — must never hit the no-key abort, even on an + // AACS disc with no key resolved. + assert!(!aacs_key_missing(true, true, &DecryptKeys::None)); + assert!(!aacs_key_missing(true, true, &aacs_keys())); + assert!(!aacs_key_missing(true, false, &DecryptKeys::None)); + } +}