mux: fail iso:// with no usable AACS key instead of muxing garbage
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.
This commit is contained in:
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Box<dyn crate::pes::Stream>> {
|
||||
let parsed = parse_url(url);
|
||||
@@ -205,6 +221,23 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
||||
disc.decrypt_with(crate::disc::Key::Unit(opts.unit_keys.clone()))
|
||||
.map_err(|e| -> 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<R: std::io::Read + Send + 'static>(
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user