L3 review r4 (B-1, m-a, m-b, m-c): judge an mpg:// pack by its first PES header

- ps_scrambled_at walks leading packets that carry no PES header (the
  system header, and every stream_id Table 2-17 exempts: MS-31, new) by
  their length, then requires a start code, the PES '10' marker and a
  header inside the clear first 0x80 bytes: a map-first clear pack is
  clear, and a scrambled PES after the map is found at its own flags.
  Only a pack's first PES is judged (documented).
- crack_key_scan_with takes the flags-offset function: the disc path
  passes is_scrambled_pack at 0x14 and cracks the sector as before; only
  the mpg:// scan presents a moved flags byte to the keyless crack.
- The mpg:// crack's rustdoc names ps_scrambled_at, not an unstuffed test.
This commit is contained in:
Matthew Jackson
2026-09-28 07:25:53 -07:00
parent 5fe32e1d13
commit 949113b8f3
4 changed files with 53 additions and 24 deletions
+40 -24
View File
@@ -176,18 +176,34 @@ pub(crate) fn resolve_dvd_title_key(
Ok(())
}
/// Where a 13818-1 pack's first PES flags byte sits: `0x0E + pack_stuffing_length + 6`
/// (0x14 unstuffed), `Some` only when that PES is scrambled (m1). The `mpg://` file's test.
/// Where a 13818-1 pack's first PES header flags byte sits, `Some` only when that PES is
/// scrambled: the `mpg://` file's test (m1, B-1). Leading packets with no PES header (a
/// system header, a map, padding) are walked by their length; only the first PES is judged.
pub(crate) fn ps_scrambled_at(sector: &[u8]) -> Option<usize> {
use crate::consts::pes_stream_id::{PADDING_STREAM, PRIVATE_STREAM_2, SYSTEM_HEADER};
if sector.len() < 2048 || sector[..4] != PACK_START || sector[4] >> 6 != 0b01 {
return None;
}
let at = 0x14 + usize::from(sector[0x0D] & 0x07);
let sid = sector[at - 3];
(!matches!(sid, SYSTEM_HEADER | PADDING_STREAM | PRIVATE_STREAM_2)
&& (sector[at] >> 4) & 0x03 != 0)
.then_some(at)
let mut p = 0x0E + usize::from(sector[0x0D] & 0x07);
loop {
let h = sector.get(p..p + 7)?;
if h[..3] != [0, 0, 1] {
return None;
}
match h[3] {
// MS-31: "if (stream_id != program_stream_map && stream_id != padding_stream &&
// stream_id != private_stream_2 && stream_id != ECM && stream_id != EMM && …"
// (0xBB, the system header, is no PES packet at all).
0xBB | 0xBC | 0xBE | 0xBF | 0xF0 | 0xF1 | 0xF2 | 0xF8 | 0xFF => {
p += 6 + usize::from(u16::from_be_bytes([h[4], h[5]]));
}
0xBD..=0xFE => {
let at = p + 6;
// CSS leaves bytes before 0x80 clear; a header past them cannot be read.
return (at < 0x80 && h[6] >> 6 == 0b10 && (h[6] >> 4) & 0x03 != 0).then_some(at);
}
_ => return None,
}
}
}
/// [`ps_scrambled_at`] as a test.
@@ -209,15 +225,15 @@ fn with_flags_at_0x14<T>(sector: &mut [u8], at: usize, f: impl FnOnce(&mut [u8])
out
}
/// The `mpg://` file's crack (design §4 step 2.1, D3): raw = false, over the strict
/// unstuffed-pack test; the resolved keys, `DecryptKeys::None` when the file is clear.
/// The `mpg://` file's crack (design §4 step 2.1, D3): raw = false, judging each pack by
/// [`ps_scrambled_at`]; the resolved keys, `DecryptKeys::None` when the file is clear.
pub(crate) fn resolve_ps_file_title_key(
reader: &mut dyn SectorSource,
extents: &[Extent],
batch_sectors: u16,
halt: Option<&crate::halt::Halt>,
) -> std::io::Result<crate::decrypt::DecryptKeys> {
match crack_key_scan_with(reader, extents, batch_sectors, halt, is_scrambled_ps_pack) {
match crack_key_scan_with(reader, extents, batch_sectors, halt, ps_scrambled_at) {
CrackOutcome::Cracked(state) => Ok(crate::decrypt::DecryptKeys::Css {
title_key: state.title_key,
}),
@@ -243,16 +259,19 @@ fn crack_key_scan(
batch_sectors: u16,
halt: Option<&crate::halt::Halt>,
) -> CrackOutcome {
crack_key_scan_with(reader, extents, batch_sectors, halt, is_scrambled_pack)
crack_key_scan_with(reader, extents, batch_sectors, halt, |s| {
is_scrambled_pack(s).then_some(0x14)
})
}
// The crack scan with its scramble test (`is_scrambled_ps_pack` for mpg:// files).
// The crack scan with its test: `scrambled_at` gives a scrambled pack's flags offset (always
// 0x14 on the disc path; `ps_scrambled_at` for mpg:// files).
fn crack_key_scan_with(
reader: &mut dyn SectorSource,
extents: &[Extent],
batch_sectors: u16,
halt: Option<&crate::halt::Halt>,
scrambled: fn(&[u8]) -> bool,
scrambled_at: fn(&[u8]) -> Option<usize>,
) -> CrackOutcome {
#[cfg(test)]
CRACK_SCANS.with(|n| n.set(n.get() + 1));
@@ -337,17 +356,14 @@ fn crack_key_scan_with(
// HARDENED pack-gated check: a clear stub sector with
// stray bits at 0x14 must NOT count as scramble evidence,
// or an unencrypted title falsely reports E7023.
if scrambled(sect) {
if let Some(at) = scrambled_at(sect) {
saw_scrambled = true;
// A stuffed mpg:// pack's flags sit past 0x14 (m1).
let key = match ps_scrambled_at(sect).filter(|&at| at != 0x14) {
Some(at) => {
let mut copy = sect.to_vec();
with_flags_at_0x14(&mut copy, at, |c| {
keyless::crack_title_key(c)
})
}
None => keyless::crack_title_key(sect),
// An mpg:// pack's flags may sit past 0x14 (m1); the disc's never do.
let key = if at == 0x14 {
keyless::crack_title_key(sect)
} else {
let mut copy = sect.to_vec();
with_flags_at_0x14(&mut copy, at, |c| keyless::crack_title_key(c))
};
if let Some(key) = key {
return CrackOutcome::Cracked(CssState {
+11
View File
@@ -311,6 +311,16 @@ pub const MS_30_PCM_DVD_HEADER: SpecQuote = SpecQuote {
text: "stream->lpcm_header[0] = 0x0c; stream->lpcm_header[1] = (freq << 4) | (((st->codecpar->bits_per_coded_sample - 16) / 4) << 6) | st->codecpar->ch_layout.nb_channels - 1; stream->lpcm_header[2] = 0x80;",
};
pub const MS_31_PES_HEADER_STREAM_IDS: SpecQuote = SpecQuote {
id: "MS-31",
kind: QuoteKind::Normative,
source: H222,
section: "§2.4.3.6 Table 2-17 PES packet",
locator: "h222:2973-2982",
url: H222_URL,
text: "if (stream_id != program_stream_map && stream_id != padding_stream && stream_id != private_stream_2 && stream_id != ECM && stream_id != EMM && stream_id != program_stream_directory && stream_id != DSMCC_stream && stream_id != ITU-T Rec. H.222.1 type E stream) { '10' 2 bslbf PES_scrambling_control 2 bslbf",
};
/// Every `MS-n` quote, in ID order.
pub const ALL: &[&SpecQuote] = &[
&MS_1_PACK,
@@ -343,4 +353,5 @@ pub const ALL: &[&SpecQuote] = &[
&MS_28_PRIVATE_DESCRIPTOR,
&MS_29_PRIVATE_STREAM_1_HEADERS,
&MS_30_PCM_DVD_HEADER,
&MS_31_PES_HEADER_STREAM_IDS,
];
+1
View File
@@ -76,3 +76,4 @@ MS-27 1011 1100 1 program_stream_map 1011 1101 2 private_stream_1 1011 1110 padd
MS-28 A range of private descriptors may be defined by the user. These descriptors shall commence with descriptor_tag and descriptor_length fields. For private descriptors, the value of descriptor_tag may take the values 64-55 as identified in Table 2-39. These descriptors may be placed within a program_stream_map()
MS-29 if (startcode == PRIVATE_STREAM_1) { avio_w8(ctx->pb, id); if (id >= 0xa0) { /* LPCM (XXX: check nb_frames) */ avio_w8(ctx->pb, 7); avio_wb16(ctx->pb, 4); /* skip 3 header bytes */ avio_w8(ctx->pb, stream->lpcm_header[0]); avio_w8(ctx->pb, stream->lpcm_header[1]); avio_w8(ctx->pb, stream->lpcm_header[2]); } else if (id >= 0x40) { /* AC-3 */ avio_w8(ctx->pb, nb_frames); avio_wb16(ctx->pb, trailer_size + 1); } }
MS-30 stream->lpcm_header[0] = 0x0c; stream->lpcm_header[1] = (freq << 4) | (((st->codecpar->bits_per_coded_sample - 16) / 4) << 6) | st->codecpar->ch_layout.nb_channels - 1; stream->lpcm_header[2] = 0x80;
MS-31 if (stream_id != program_stream_map && stream_id != padding_stream && stream_id != private_stream_2 && stream_id != ECM && stream_id != EMM && stream_id != program_stream_directory && stream_id != DSMCC_stream && stream_id != ITU-T Rec. H.222.1 type E stream) { '10' 2 bslbf PES_scrambling_control 2 bslbf
+1
View File
@@ -73,3 +73,4 @@ src/mux/codec/lpcm.rs dvd_header MS-30
src/mux/mpg/scan.rs parse_map MS-8
src/mux/mpg/scan.rs parse_map MS-9
src/mux/mpg/scan.rs scan MS-23
src/css/mod.rs ps_scrambled_at MS-31