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_AACS_VUK_NOT_IN_KEYDB: u16 = 7019;
|
||||||
pub const E_DRIVE_PROFILE_MISSING: u16 = 7020;
|
pub const E_DRIVE_PROFILE_MISSING: u16 = 7020;
|
||||||
pub const E_VID_CDB_UNAVAILABLE: u16 = 7021;
|
pub const E_VID_CDB_UNAVAILABLE: u16 = 7021;
|
||||||
|
pub const E_NO_DISC_KEY: u16 = 7022;
|
||||||
|
|
||||||
// Keydb (8xxx)
|
// Keydb (8xxx)
|
||||||
pub const E_KEYDB_CONNECT: u16 = 8000;
|
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
|
/// template (older profile blob, or a drive class without an OEM
|
||||||
/// VID path).
|
/// VID path).
|
||||||
VidCdbUnavailable,
|
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)
|
// Keydb (8xxx)
|
||||||
KeydbConnect {
|
KeydbConnect {
|
||||||
@@ -345,6 +356,7 @@ impl Error {
|
|||||||
Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB,
|
Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB,
|
||||||
Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING,
|
Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING,
|
||||||
Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE,
|
Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE,
|
||||||
|
Error::NoDiscKey { .. } => E_NO_DISC_KEY,
|
||||||
Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
|
Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
|
||||||
Error::KeydbHttp { .. } => E_KEYDB_HTTP,
|
Error::KeydbHttp { .. } => E_KEYDB_HTTP,
|
||||||
Error::KeydbInvalid => E_KEYDB_INVALID,
|
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::StreamUrlMissingPort { addr } => write!(f, "E{}: {}", self.code(), addr),
|
||||||
Error::PesFrameTooLarge { size } => write!(f, "E{}: {}", self.code(), size),
|
Error::PesFrameTooLarge { size } => write!(f, "E{}: {}", self.code(), size),
|
||||||
Error::IsoTooLarge { path } => write!(f, "E{}: {}", self.code(), path),
|
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()),
|
_ => write!(f, "E{}", self.code()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,22 @@ pub struct InputOptions {
|
|||||||
pub raw: bool,
|
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).
|
/// Open a PES input stream (produces PES frames).
|
||||||
pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
|
pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::Stream>> {
|
||||||
let parsed = parse_url(url);
|
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()))
|
disc.decrypt_with(crate::disc::Key::Unit(opts.unit_keys.clone()))
|
||||||
.map_err(|e| -> io::Error { e.into() })?;
|
.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() {
|
if disc.titles.is_empty() {
|
||||||
return Err(crate::error::Error::NoStreams.into());
|
return Err(crate::error::Error::NoStreams.into());
|
||||||
}
|
}
|
||||||
@@ -457,3 +490,50 @@ fn build_m2ts_pipeline<R: std::io::Read + Send + 'static>(
|
|||||||
pid_to_track,
|
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