mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
decrypt: require an explicit AACS unit base and refuse off-grid units
DecryptingSectorSource no longer defaults to the disc-LBA-0 grid: an AACS content read with no set_unit_base fails DecryptFailed. apply_aacs_map refuses a CPI-flagged BD-TS unit whose clear seed lacks the TS sync (cut off its file grid).
This commit is contained in:
@@ -87,6 +87,16 @@ pub fn aacs_unit_seed_encrypted(unit: &[u8], format: crate::disc::ContentFormat)
|
||||
}
|
||||
}
|
||||
|
||||
/// Was `unit` cut on its file's unit grid? An encrypted BD-TS unit's clear seed
|
||||
/// always starts a source packet, so its TS sync (0x47) sits at byte 4; a chunk
|
||||
/// read off the grid starts mid-unit on ciphertext. Clear units, and `MpegPs`
|
||||
/// (seed layout unverified), always pass.
|
||||
pub(crate) fn aacs_unit_on_grid(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
|
||||
format != crate::disc::ContentFormat::BdTs
|
||||
|| !aacs_unit_seed_encrypted(unit, format)
|
||||
|| unit.get(4) == Some(&TS_SYNC)
|
||||
}
|
||||
|
||||
/// True when an aligned unit is flagged encrypted AND still looks scrambled
|
||||
/// (structure not yet restored) — genuine encrypted content NOT yet decrypted.
|
||||
///
|
||||
|
||||
@@ -446,6 +446,12 @@ fn apply_aacs_map(
|
||||
}
|
||||
return;
|
||||
};
|
||||
// Off-grid guard: a real unit's clear seed carries its sync byte, so a flagged
|
||||
// chunk without it was cut across two units (wrong unit base) — fail loud.
|
||||
if !aacs::content::aacs_unit_on_grid(chunk, format) {
|
||||
verify_failed.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
// PHASE GATE (FMTS forensic segment): the segment interleaves two variants
|
||||
// at the unit level. Decrypt ONLY our parity; leave the alternate half as
|
||||
// ciphertext (the muxer drops untouched ciphertext cleanly — no garble).
|
||||
|
||||
@@ -212,6 +212,7 @@ pub fn probe_and_remap<S: SectorSource + ?Sized>(
|
||||
let mut buf = vec![0u8; count as usize * 2048];
|
||||
// `recovery=false`: a single best-effort attempt — the probe must never
|
||||
// stall the mux or hammer a marginal drive. On any error, bail to ordinal.
|
||||
reader.set_unit_base(ext.start_lba); // HD DVD AACS: anchor at the title head
|
||||
let n = match reader.read_sectors(ext.start_lba, count, &mut buf, false) {
|
||||
Ok(n) => n,
|
||||
Err(_) => return,
|
||||
|
||||
+1
-1
@@ -269,7 +269,7 @@ impl DiscStream {
|
||||
// `reader` is already wrapped above (DecryptKeys::None makes it a
|
||||
// pass-through). Reset the unit base the probe read advanced so the
|
||||
// first fill_extents read starts cleanly.
|
||||
reader.set_unit_base(0);
|
||||
reader.clear_unit_base();
|
||||
|
||||
// B1 resync gates: one per stream, video flagged so the gate only
|
||||
// drop-to-keyframes video (audio/subtitle always admit). Computed before
|
||||
|
||||
+1
-1
@@ -1774,7 +1774,7 @@ pub fn build_iso_pipeline<S: SectorSource + Send + 'static>(
|
||||
}
|
||||
}
|
||||
crate::disc::dvd_audio_probe::probe_and_remap(&mut decrypting, &mut title);
|
||||
decrypting.set_unit_base(0);
|
||||
decrypting.clear_unit_base();
|
||||
|
||||
let prefetched = crate::sector::PrefetchedSectorSource::new_with_events(
|
||||
decrypting,
|
||||
|
||||
@@ -76,11 +76,11 @@ pub struct DecryptingSectorSource<S: SectorSource> {
|
||||
/// extent `start_lba` that AACS aligned units are anchored at. The unit-
|
||||
/// alignment gate measures `lba` relative to THIS, not absolute disc LBA 0,
|
||||
/// so a clip whose `start_lba` is not 3-aligned still gates correctly. Set
|
||||
/// per-extent by the mux read paths via [`set_unit_base`]; defaults to 0
|
||||
/// (absolute alignment) for callers that read from a 3-aligned base.
|
||||
/// per-extent via [`set_unit_base`]. `None` (the default): there is no implicit
|
||||
/// disc-LBA-0 grid, so an AACS content read fails loud until a base is set.
|
||||
///
|
||||
/// [`set_unit_base`]: Self::set_unit_base
|
||||
unit_base: u32,
|
||||
unit_base: Option<u32>,
|
||||
/// Encrypted-content map: every stream file's sorted/merged `(start_lba,
|
||||
/// sector_count)` (e.g. [`Disc::stream_content_ranges`](crate::Disc::stream_content_ranges)).
|
||||
/// When `Some`, a unit outside these ranges is clear and passed through
|
||||
@@ -113,7 +113,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
||||
Self {
|
||||
inner,
|
||||
keys,
|
||||
unit_base: 0,
|
||||
unit_base: None,
|
||||
content_ranges: None,
|
||||
key_map: None,
|
||||
}
|
||||
@@ -156,6 +156,12 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
||||
self.keys = keys;
|
||||
}
|
||||
|
||||
/// Drop the unit base: AACS content reads fail loud until
|
||||
/// [`set_unit_base`](SectorSource::set_unit_base) anchors the next one.
|
||||
pub fn clear_unit_base(&mut self) {
|
||||
self.unit_base = None;
|
||||
}
|
||||
|
||||
/// Borrow the inner source. Useful for tests and for adapters
|
||||
/// that want to introspect the underlying drive / file without
|
||||
/// unwrapping the decorator.
|
||||
@@ -223,12 +229,14 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
||||
let content = self.content_ranges.clone();
|
||||
let content_ref = content.as_deref();
|
||||
|
||||
// Defense-in-depth: AACS units are 3-sector aligned; misalignment silently mis-
|
||||
// decrypts, so reject loud (DecryptFailed) before reading — gated vs `unit_base`
|
||||
// (not raw `lba % 3`), only when the span TOUCHES content (else UDF/nav passes through).
|
||||
// AACS units are 3-sector aligned on each file's grid; a content read with no
|
||||
// base, or off it, silently mis-decrypts, so reject loud before reading. Only
|
||||
// spans that TOUCH content are gated (UDF/nav passes through).
|
||||
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
||||
&& span_touches_content(content_ref, lba, count)
|
||||
&& !crate::aacs::content::is_unit_aligned(lba, self.unit_base)
|
||||
&& !self
|
||||
.unit_base
|
||||
.is_some_and(|b| crate::aacs::content::is_unit_aligned(lba, b))
|
||||
{
|
||||
return Err(crate::error::Error::DecryptFailed);
|
||||
}
|
||||
@@ -262,7 +270,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
||||
}
|
||||
|
||||
fn set_unit_base(&mut self, lba: u32) {
|
||||
self.unit_base = lba;
|
||||
self.unit_base = Some(lba);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,6 +749,7 @@ mod tests {
|
||||
0,
|
||||
)]));
|
||||
let mut dec = DecryptingSectorSource::new(src, keys).with_key_map(map);
|
||||
dec.set_unit_base(0);
|
||||
let mut buf = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let n = dec.read_sectors(0, 3, &mut buf, false).unwrap();
|
||||
assert_eq!(n, crate::aacs::content::ALIGNED_UNIT_LEN);
|
||||
@@ -767,6 +776,7 @@ mod tests {
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
};
|
||||
let mut dec = DecryptingSectorSource::new(src, keys); // no with_key_map
|
||||
dec.set_unit_base(0);
|
||||
let mut buf = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let err = dec
|
||||
.read_sectors(0, 3, &mut buf, false)
|
||||
@@ -860,6 +870,7 @@ mod tests {
|
||||
)]));
|
||||
let mut dec =
|
||||
DecryptingSectorSource::new(CountingSource(reads.clone()), keys).with_key_map(map);
|
||||
dec.set_unit_base(0);
|
||||
let mut buf = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let err = dec
|
||||
.read_sectors(1, 3, &mut buf, false)
|
||||
@@ -879,4 +890,75 @@ mod tests {
|
||||
assert!(dec.read_sectors(3, 3, &mut buf, false).is_err());
|
||||
assert_eq!(reads.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// A file whose first sector is LBA 1001 (1001 % 3 == 2): two encrypted units on
|
||||
// its own grid (1001, 1004), zeroes elsewhere.
|
||||
struct MisalignedFile(Vec<u8>);
|
||||
const FILE_LBA: u32 = 1001;
|
||||
impl SectorSource for MisalignedFile {
|
||||
fn read_sectors(&mut self, lba: u32, c: u16, b: &mut [u8], _r: bool) -> Result<usize> {
|
||||
for i in 0..c as usize {
|
||||
let rel = (lba as usize + i).wrapping_sub(FILE_LBA as usize) * 2048;
|
||||
let dst = &mut b[i * 2048..(i + 1) * 2048];
|
||||
match self.0.get(rel..rel + 2048) {
|
||||
Some(src) => dst.copy_from_slice(src),
|
||||
None => dst.fill(0),
|
||||
}
|
||||
}
|
||||
Ok(c as usize * 2048)
|
||||
}
|
||||
}
|
||||
|
||||
fn misaligned_file_source() -> DecryptingSectorSource<MisalignedFile> {
|
||||
let key = [0x5Au8; 16];
|
||||
let mut data = encrypt_aacs_unit(&key);
|
||||
data.extend(encrypt_aacs_unit(&key));
|
||||
let keys = DecryptKeys::Aacs {
|
||||
unit_keys: vec![(0, key)],
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
};
|
||||
let map = Arc::new(crate::decrypt::AacsKeyMap::from_ranges(vec![(
|
||||
FILE_LBA,
|
||||
FILE_LBA + 6,
|
||||
0,
|
||||
)]));
|
||||
DecryptingSectorSource::new(MisalignedFile(data), keys)
|
||||
.with_key_map(map)
|
||||
.with_content_ranges(Arc::from(vec![(FILE_LBA, 6u32)]))
|
||||
}
|
||||
|
||||
// No default grid: an AACS content read before any `set_unit_base` fails loud,
|
||||
// never decrypts on the disc-LBA-0 grid (1002 % 3 == 0 once passed the gate).
|
||||
#[test]
|
||||
fn aacs_content_read_without_unit_base_fails_loud() {
|
||||
let mut dec = misaligned_file_source();
|
||||
let mut buf = vec![0u8; 2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let r = dec.read_sectors(FILE_LBA + 1, 6, &mut buf, false);
|
||||
assert!(
|
||||
matches!(r, Err(crate::error::Error::DecryptFailed)),
|
||||
"no unit base must be DecryptFailed, got {r:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// A wrong explicit base (disc grid) cuts chunks across real units: their seed has
|
||||
// no TS sync at byte 4, so the decrypt refuses instead of returning garbage.
|
||||
#[test]
|
||||
fn aacs_read_on_wrong_unit_grid_fails_loud() {
|
||||
let mut dec = misaligned_file_source();
|
||||
let mut buf = vec![0u8; 2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
dec.set_unit_base(FILE_LBA + 1);
|
||||
let r = dec.read_sectors(FILE_LBA + 1, 6, &mut buf, false);
|
||||
assert!(
|
||||
matches!(r, Err(crate::error::Error::DecryptFailed)),
|
||||
"an off-grid read must be DecryptFailed, got Ok"
|
||||
);
|
||||
|
||||
// The file's own grid decrypts both units byte-exact.
|
||||
dec.set_unit_base(FILE_LBA);
|
||||
dec.read_sectors(FILE_LBA, 6, &mut buf, false)
|
||||
.expect("file-grid read decrypts");
|
||||
let mut want = clear_aacs_unit();
|
||||
want.extend(clear_aacs_unit());
|
||||
assert_eq!(buf, want);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user