libfreemkv: clip-anchored AACS unit gate + consolidate key mechanism
The AACS unit-alignment gate measured `lba % 3` against absolute disc LBA 0, but aligned units are anchored at each clip's encrypted-region start. A clip whose start_lba is not 3-aligned had its readable units wrongly rejected with "Decryption failed" (the big-title-only failure on some Blu-rays). One canonical clip-anchored helper (`aacs::is_unit_aligned`) is now the single source of truth for the decrypt-on-read gate; both mux read paths set the per-extent `unit_base = start_lba` via a new `SectorSource::set_unit_base`. Also moves key *mechanism* into the library: the encrypted sample reader (`read_encrypted_units`) and the candidate-key resolution loop (`resolve_and_apply`) now live here, so a key source is purely a lookup. Regression test covers a clip based at a non-3-aligned LBA.
This commit is contained in:
@@ -13,6 +13,26 @@ pub(crate) const AACS_IV: [u8; 16] = [
|
|||||||
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
|
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
|
||||||
pub const ALIGNED_UNIT_LEN: usize = 6144;
|
pub const ALIGNED_UNIT_LEN: usize = 6144;
|
||||||
|
|
||||||
|
/// An AACS aligned unit spans this many 2048-byte sectors (3).
|
||||||
|
pub const ALIGNED_UNIT_SECTORS: u32 = (ALIGNED_UNIT_LEN / SECTOR_LEN) as u32;
|
||||||
|
|
||||||
|
/// Whether `lba` sits on an AACS aligned-unit boundary, measured **relative to
|
||||||
|
/// the encrypted region's base LBA** (`unit_base` = the clip/extent `start_lba`,
|
||||||
|
/// NOT absolute disc LBA 0).
|
||||||
|
///
|
||||||
|
/// AACS aligned units (6144 B / 3 sectors) are anchored at the start of each
|
||||||
|
/// clip's encrypted region, so a read must begin a whole number of units past
|
||||||
|
/// that base for `decrypt_sectors` (which anchors units at buffer offset 0) to
|
||||||
|
/// align the CBC correctly. This is the SINGLE source of truth for the test —
|
||||||
|
/// the decrypt-on-read gate, the inline and highway mux read paths, and the
|
||||||
|
/// key-validation sample reader all key off this, never absolute `lba % 3`. A
|
||||||
|
/// disc whose clip `start_lba` is not itself 3-aligned would otherwise mis-gate
|
||||||
|
/// (reject readable units, then report "Decryption failed") on exactly the
|
||||||
|
/// titles whose clips land off a 3-boundary.
|
||||||
|
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
|
||||||
|
lba.wrapping_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0
|
||||||
|
}
|
||||||
|
|
||||||
/// Size of one sector.
|
/// Size of one sector.
|
||||||
const SECTOR_LEN: usize = 2048;
|
const SECTOR_LEN: usize = 2048;
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -24,8 +24,9 @@ pub mod variants;
|
|||||||
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
||||||
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
||||||
pub use decrypt::{
|
pub use decrypt::{
|
||||||
ALIGNED_UNIT_LEN, UnitKeyResult, decrypt_bus, decrypt_unit, decrypt_unit_full,
|
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, decrypt_bus, decrypt_unit,
|
||||||
decrypt_unit_try_keys, is_aacs_scrambled, ts_packet_total, ts_sync_count, unit_key_validates,
|
decrypt_unit_full, decrypt_unit_try_keys, is_aacs_scrambled, is_unit_aligned, ts_packet_total,
|
||||||
|
ts_sync_count, unit_key_validates,
|
||||||
};
|
};
|
||||||
pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
||||||
pub use keys::probe;
|
pub use keys::probe;
|
||||||
|
|||||||
@@ -122,6 +122,100 @@ pub trait KeySource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drive `sources` until one key decrypts `disc`. Loops [`KeySource::next_key`]
|
||||||
|
/// and hands each candidate to [`crate::Disc::decrypt_with`] (which validates it
|
||||||
|
/// against `inputs.samples` and only mutates the disc on success), returning
|
||||||
|
/// `true` at the first key that decrypts and `false` once every source is
|
||||||
|
/// exhausted — the genuine "no key for this disc". THE shared key-resolution
|
||||||
|
/// loop: every application uses it instead of re-rolling the candidate/retry
|
||||||
|
/// logic, so the "no key" verdict is identical everywhere.
|
||||||
|
///
|
||||||
|
/// Lives in the library, not a key-source crate: resolution is decryption
|
||||||
|
/// *mechanism* — it validates candidates against disc content via
|
||||||
|
/// `Disc::decrypt_with` — and the library owns all mechanism. A key source only
|
||||||
|
/// hands out candidate keys; what's done with them is not its concern.
|
||||||
|
pub fn resolve_and_apply(
|
||||||
|
sources: &mut dyn KeySource,
|
||||||
|
inputs: &DiscInputs,
|
||||||
|
disc: &mut crate::Disc,
|
||||||
|
) -> bool {
|
||||||
|
while let Some(key) = sources.next_key(inputs) {
|
||||||
|
if disc.decrypt_with(key, &inputs.samples).is_ok() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read up to `n` ENCRYPTED 6144-byte aligned units from `title`'s body, raw (no
|
||||||
|
/// decrypt) — the content samples that populate [`DiscInputs::samples`] for a
|
||||||
|
/// key server to validate a candidate against, and that [`resolve_and_apply`]
|
||||||
|
/// hands to [`crate::Disc::decrypt_with`].
|
||||||
|
///
|
||||||
|
/// Lives in the library, not a key-source crate: reading the disc and carving
|
||||||
|
/// AACS units is decryption *mechanism* (unit geometry anchored at each extent's
|
||||||
|
/// `start_lba`), which the library owns. A key source is *handed* these bytes
|
||||||
|
/// via `DiscInputs.samples`; it never reads the disc itself.
|
||||||
|
///
|
||||||
|
/// "Encrypted" is decided by [`crate::aacs::is_aacs_scrambled`] — the SAME
|
||||||
|
/// predicate the decrypt gate uses — so all sides agree. A clip opens with clear
|
||||||
|
/// navigation units (PAT/PMT, menus); only the feature body is scrambled, and a
|
||||||
|
/// clear unit proves nothing, so this collects only scrambled ones, sampling the
|
||||||
|
/// largest extent at its midpoint forward.
|
||||||
|
pub fn read_encrypted_units(
|
||||||
|
reader: &mut dyn crate::sector::SectorSource,
|
||||||
|
title: &crate::disc::DiscTitle,
|
||||||
|
n: usize,
|
||||||
|
) -> Vec<Vec<u8>> {
|
||||||
|
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, is_aacs_scrambled};
|
||||||
|
const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap
|
||||||
|
const MAX_CHUNKS_PER_EXTENT: u32 = 4; // ~60 units scanned at each extent's midpoint
|
||||||
|
|
||||||
|
let mut out: Vec<Vec<u8>> = Vec::new();
|
||||||
|
for ext in &title.extents {
|
||||||
|
let total_units = ext.sector_count / ALIGNED_UNIT_SECTORS;
|
||||||
|
if total_units == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut unit = total_units / 2; // midpoint (past the clear nav at the head)
|
||||||
|
for _ in 0..MAX_CHUNKS_PER_EXTENT {
|
||||||
|
if unit >= total_units {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let units_this = CHUNK_UNITS.min(total_units - unit);
|
||||||
|
// Saturate: start_lba comes from attacker-controlled UDF/MPLS
|
||||||
|
// extents; a malformed extent near u32::MAX would otherwise panic
|
||||||
|
// (debug) or wrap to a wrong LBA (release). An over-capacity LBA then
|
||||||
|
// fails cleanly via the read_sectors().is_err() break below.
|
||||||
|
let lba = ext
|
||||||
|
.start_lba
|
||||||
|
.saturating_add(unit.saturating_mul(ALIGNED_UNIT_SECTORS));
|
||||||
|
let count = (units_this * ALIGNED_UNIT_SECTORS) as u16;
|
||||||
|
let mut buf = vec![0u8; count as usize * 2048];
|
||||||
|
// `false` = no recovery retries; the reader is the raw drive/file
|
||||||
|
// (no decrypt decorator), so these are the on-disc encrypted bytes.
|
||||||
|
if reader.read_sectors(lba, count, &mut buf, false).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for i in 0..units_this as usize {
|
||||||
|
let o = i * ALIGNED_UNIT_LEN;
|
||||||
|
if o + ALIGNED_UNIT_LEN > buf.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let u = &buf[o..o + ALIGNED_UNIT_LEN];
|
||||||
|
if is_aacs_scrambled(u) {
|
||||||
|
out.push(u.to_vec());
|
||||||
|
if out.len() >= n {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unit += units_this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+1
-1
@@ -194,7 +194,7 @@ pub use disc::{
|
|||||||
KeyOrigin, LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate,
|
KeyOrigin, LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate,
|
||||||
ScanOptions, Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage,
|
ScanOptions, Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage,
|
||||||
};
|
};
|
||||||
pub use keysource::{DiscInputs, KeySource};
|
pub use keysource::{DiscInputs, KeySource, read_encrypted_units, resolve_and_apply};
|
||||||
|
|
||||||
// ─── Streams ────────────────────────────────────────────────────────────────
|
// ─── Streams ────────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
|
|||||||
+7
-2
@@ -359,6 +359,11 @@ impl DiscStream {
|
|||||||
// Saturate for consistency with the rest of the file's arithmetic.
|
// Saturate for consistency with the rest of the file's arithmetic.
|
||||||
let lba = ext_start.saturating_add(self.current_offset);
|
let lba = ext_start.saturating_add(self.current_offset);
|
||||||
|
|
||||||
|
// AACS aligned units are anchored at this extent's start LBA — gate the
|
||||||
|
// decrypt-on-read source relative to it (clip-anchored), not absolute
|
||||||
|
// disc LBA 0. No-op for CSS / None.
|
||||||
|
self.reader.set_unit_base(ext_start);
|
||||||
|
|
||||||
// Adaptive sizer: start at current (preferred until a failure), shrink
|
// Adaptive sizer: start at current (preferred until a failure), shrink
|
||||||
// on failure, advance on success. One 5s read attempt per try — no
|
// on failure, advance on success. One 5s read attempt per try — no
|
||||||
// retry loops, no sleeps. On size-1 failure, skip or error.
|
// retry loops, no sleeps. On size-1 failure, skip or error.
|
||||||
@@ -465,8 +470,8 @@ impl DiscStream {
|
|||||||
lba,
|
lba,
|
||||||
sectors
|
sectors
|
||||||
);
|
);
|
||||||
let rec =
|
let rec = self
|
||||||
self.reader
|
.reader
|
||||||
.read_sectors(lba, sectors, &mut self.read_buf[..bytes], true);
|
.read_sectors(lba, sectors, &mut self.read_buf[..bytes], true);
|
||||||
if let Ok(&got) = rec.as_ref() {
|
if let Ok(&got) = rec.as_ref() {
|
||||||
debug_assert!(got <= bytes, "recovery read over-reported byte count");
|
debug_assert!(got <= bytes, "recovery read over-reported byte count");
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ pub struct DecryptingSectorSource<S: SectorSource> {
|
|||||||
inner: S,
|
inner: S,
|
||||||
keys: DecryptKeys,
|
keys: DecryptKeys,
|
||||||
unit_key_idx: usize,
|
unit_key_idx: usize,
|
||||||
|
/// Base LBA of the encrypted region currently being read — the clip /
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// [`set_unit_base`]: Self::set_unit_base
|
||||||
|
unit_base: u32,
|
||||||
/// Cumulative bytes of scrambled AACS units that no key could decrypt.
|
/// Cumulative bytes of scrambled AACS units that no key could decrypt.
|
||||||
/// `decrypt_sectors` restores those bytes to their original ciphertext (so a
|
/// `decrypt_sectors` restores those bytes to their original ciphertext (so a
|
||||||
/// clear nav-file is never corrupted), but for genuine encrypted content the
|
/// clear nav-file is never corrupted), but for genuine encrypted content the
|
||||||
@@ -56,6 +65,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
inner,
|
inner,
|
||||||
keys,
|
keys,
|
||||||
unit_key_idx: 0,
|
unit_key_idx: 0,
|
||||||
|
unit_base: 0,
|
||||||
decrypt_dropped: Arc::new(AtomicU64::new(0)),
|
decrypt_dropped: Arc::new(AtomicU64::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,17 +128,21 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
recovery: bool,
|
recovery: bool,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
// Defense-in-depth: AACS aligned units are 3 sectors (6144 bytes) and
|
// Defense-in-depth: AACS aligned units are 3 sectors (6144 bytes) and
|
||||||
// `decrypt_sectors` anchors units at buffer offset 0. A read whose START
|
// `decrypt_sectors` anchors units at buffer offset 0. A read that does
|
||||||
// LBA is not unit-aligned (lba % 3 != 0) would decrypt every unit under
|
// not begin a whole number of units past the encrypted region's base
|
||||||
// the wrong CBC/unit alignment and silently mis-decrypt. Reject loud
|
// (`unit_base`, the clip/extent start_lba) would decrypt every unit
|
||||||
// (DecryptFailed) BEFORE reading rather than ever mis-decrypting — callers
|
// under the wrong CBC/unit alignment and silently mis-decrypt. Reject
|
||||||
// (e.g. the multipass sweep) must issue unit-aligned reads.
|
// loud (DecryptFailed) BEFORE reading rather than ever mis-decrypting.
|
||||||
if matches!(self.keys, DecryptKeys::Aacs { .. }) {
|
// The gate is measured RELATIVE to `unit_base` (set per-extent by the
|
||||||
const UNIT_SECTORS: u32 = (crate::aacs::ALIGNED_UNIT_LEN / 2048) as u32; // 3
|
// mux read paths via `set_unit_base`), never absolute `lba % 3` — a clip
|
||||||
if lba % UNIT_SECTORS != 0 {
|
// whose start_lba is not itself 3-aligned must still gate on its own
|
||||||
|
// units (else its readable units are wrongly rejected → "Decryption
|
||||||
|
// failed" on exactly those titles).
|
||||||
|
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
||||||
|
&& !crate::aacs::is_unit_aligned(lba, self.unit_base)
|
||||||
|
{
|
||||||
return Err(crate::error::Error::DecryptFailed);
|
return Err(crate::error::Error::DecryptFailed);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let n = self.inner.read_sectors(lba, count, buf, recovery)?;
|
let n = self.inner.read_sectors(lba, count, buf, recovery)?;
|
||||||
// Apply the crate-wide AACS/CSS/None decrypt entry point in-place
|
// Apply the crate-wide AACS/CSS/None decrypt entry point in-place
|
||||||
// over the bytes just read. No-op for DecryptKeys::None. The returned
|
// over the bytes just read. No-op for DecryptKeys::None. The returned
|
||||||
@@ -146,6 +160,10 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
fn set_speed(&mut self, kbs: u16) {
|
fn set_speed(&mut self, kbs: u16) {
|
||||||
self.inner.set_speed(kbs)
|
self.inner.set_speed(kbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_unit_base(&mut self, lba: u32) {
|
||||||
|
self.unit_base = lba;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -638,6 +656,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clip-anchored gate (the Watership Down "Decryption failed" regression):
|
||||||
|
/// AACS aligned units are anchored at the clip's encrypted-region start
|
||||||
|
/// (`unit_base`), NOT absolute disc LBA 0. A clip whose `start_lba` is not
|
||||||
|
/// itself 3-aligned must gate on ITS OWN units, so the clip's base LBA
|
||||||
|
/// (which the old `lba % 3` gate wrongly rejected) now passes, and only
|
||||||
|
/// reads off the clip-relative unit grid reject.
|
||||||
|
#[test]
|
||||||
|
fn aacs_gate_is_clip_anchored_not_absolute() {
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0u32, [0u8; 16])],
|
||||||
|
read_data_key: None,
|
||||||
|
};
|
||||||
|
// base = 64 (abs % 3 == 1): the non-3-aligned clip start that triggered
|
||||||
|
// the bug. The old absolute gate rejected every read here; the clip-
|
||||||
|
// anchored gate must accept the clip's own unit grid.
|
||||||
|
let base = 64u32;
|
||||||
|
|
||||||
|
// Clip-relative aligned starts (base + {0,3,6,30}) pass.
|
||||||
|
for off in [0u32, 3, 6, 30] {
|
||||||
|
let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone());
|
||||||
|
w.set_unit_base(base);
|
||||||
|
let mut buf = vec![0u8; 3 * 2048];
|
||||||
|
let n = w
|
||||||
|
.read_sectors(base + off, 3, &mut buf, false)
|
||||||
|
.unwrap_or_else(|_| panic!("clip-relative aligned lba {} must pass", base + off));
|
||||||
|
assert_eq!(n, 3 * 2048);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The clip's base LBA itself (abs % 3 == 1) — the exact read the old gate
|
||||||
|
// wrongly rejected — must now decrypt.
|
||||||
|
let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone());
|
||||||
|
w.set_unit_base(base);
|
||||||
|
let mut buf = vec![0u8; 3 * 2048];
|
||||||
|
assert!(
|
||||||
|
w.read_sectors(base, 3, &mut buf, false).is_ok(),
|
||||||
|
"a clip starting at a non-3-aligned LBA must decrypt from its own base"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clip-relative MISaligned starts (base + {1,2,4,5}) still reject.
|
||||||
|
for off in [1u32, 2, 4, 5] {
|
||||||
|
let mut w = DecryptingSectorSource::new(ClearUnitSource, keys.clone());
|
||||||
|
w.set_unit_base(base);
|
||||||
|
let mut buf = vec![0u8; 3 * 2048];
|
||||||
|
let err = w
|
||||||
|
.read_sectors(base + off, 3, &mut buf, false)
|
||||||
|
.expect_err("clip-relative unaligned start must reject");
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
crate::error::Error::DecryptFailed.code(),
|
||||||
|
"base+{off} is off the clip-relative unit grid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The unit-alignment guard is AACS-only. A CSS decrypting read (per-sector,
|
/// The unit-alignment guard is AACS-only. A CSS decrypting read (per-sector,
|
||||||
/// stateless — DVDs) must NOT be gated on a 3-sector boundary: a single
|
/// stateless — DVDs) must NOT be gated on a 3-sector boundary: a single
|
||||||
/// sector at lba 1 must read fine. Grounding: the guard is inside
|
/// sector at lba 1 must read fine. Grounding: the guard is inside
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ pub trait SectorSource: Send {
|
|||||||
/// Optional speed control for sources that map to a physical
|
/// Optional speed control for sources that map to a physical
|
||||||
/// drive. No-op for everything else.
|
/// drive. No-op for everything else.
|
||||||
fn set_speed(&mut self, _kbs: u16) {}
|
fn set_speed(&mut self, _kbs: u16) {}
|
||||||
|
|
||||||
|
/// Set the base LBA an AACS unit-alignment gate measures against — the
|
||||||
|
/// `start_lba` of the extent/clip about to be read. Aligned AACS units
|
||||||
|
/// (6144 B / 3 sectors) are anchored at each clip's encrypted-region start,
|
||||||
|
/// so a decrypt-on-read source gates `lba` relative to this base, not
|
||||||
|
/// absolute disc LBA 0. Mux read paths call this when they advance to a new
|
||||||
|
/// extent. No-op for everything except [`DecryptingSectorSource`], the only
|
||||||
|
/// source that applies the unit-alignment gate.
|
||||||
|
///
|
||||||
|
/// [`DecryptingSectorSource`]: crate::sector::DecryptingSectorSource
|
||||||
|
fn set_unit_base(&mut self, _lba: u32) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forwarding impls so `Box<dyn SectorSource>` and `&mut dyn SectorSource`
|
// Forwarding impls so `Box<dyn SectorSource>` and `&mut dyn SectorSource`
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ impl PrefetchedSectorSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let extent = &extents[ext_idx];
|
let extent = &extents[ext_idx];
|
||||||
|
// AACS aligned units are anchored at THIS extent's start
|
||||||
|
// LBA, so tell the decrypt-on-read source to gate relative
|
||||||
|
// to it (clip-anchored), not absolute disc LBA 0. A no-op
|
||||||
|
// for non-decrypting / CSS / None sources.
|
||||||
|
reader.set_unit_base(extent.start_lba);
|
||||||
let remaining = extent.sector_count.saturating_sub(offset);
|
let remaining = extent.sector_count.saturating_sub(offset);
|
||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
ext_idx += 1;
|
ext_idx += 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user