use crate::decrypt::{DecryptKeys, decrypt_sectors, decrypt_sectors_in_content};
use crate::error::Result;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::SectorSource;
pub type KeyFetch = std::sync::Arc<dyn Fn(&[Vec<u8>]) -> Vec<[u8; 16]> + Send + Sync>;
const MAX_FETCH_CALLS: usize = 16;
const MAX_FETCH_SAMPLES: usize = 8;
const MAX_DIAG_UNITS_PER_READ: usize = 4;
pub const DECRYPT_VERIFY_READ: bool = true;
pub struct DecryptingSectorSource<S: SectorSource> {
inner: S,
keys: DecryptKeys,
unit_key_idx: usize,
unit_base: u32,
decrypt_dropped: Arc<AtomicU64>,
fetch: Option<KeyFetch>,
fetch_spent: bool,
fetch_calls: usize,
verify_only: bool,
content_ranges: Option<Arc<[(u32, u32)]>>,
scratch: Vec<u8>,
}
impl<S: SectorSource> DecryptingSectorSource<S> {
pub fn new(inner: S, keys: DecryptKeys) -> Self {
Self {
inner,
keys,
unit_key_idx: 0,
unit_base: 0,
decrypt_dropped: Arc::new(AtomicU64::new(0)),
fetch: None,
fetch_spent: false,
fetch_calls: 0,
verify_only: false,
content_ranges: None,
scratch: Vec::new(),
}
}
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
self.content_ranges = Some(ranges);
self
}
pub fn verify_only(mut self) -> Self {
self.verify_only = true;
self
}
pub fn decrypt_loss(&self) -> Arc<AtomicU64> {
Arc::clone(&self.decrypt_dropped)
}
pub fn with_unit_key_idx(mut self, idx: usize) -> Self {
self.unit_key_idx = idx;
self
}
pub fn with_key_fetch(mut self, cb: KeyFetch) -> Self {
self.fetch = Some(cb);
self
}
pub fn set_keys(&mut self, keys: DecryptKeys) {
self.keys = keys;
}
pub fn inner(&self) -> &S {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut S {
&mut self.inner
}
pub fn into_inner(self) -> S {
self.inner
}
fn decrypt_buf(
buf: &mut [u8],
keys: &mut DecryptKeys,
unit_key_idx: usize,
lba: u32,
content: Option<&[(u32, u32)]>,
) -> Result<usize> {
match content {
Some(ranges) => decrypt_sectors_in_content(buf, keys, unit_key_idx, lba, ranges),
None => decrypt_sectors(buf, keys, unit_key_idx),
}
}
fn fetch_failed_units(
&mut self,
buf: &mut [u8],
lba: u32,
content: Option<&[(u32, u32)]>,
prev_dropped: usize,
) -> usize {
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
let mut samples: Vec<Vec<u8>> = Vec::new();
for chunk in buf.chunks_exact(unit_len) {
if crate::aacs::aacs_unit_needs_decrypt(chunk) {
samples.push(chunk.to_vec());
if samples.len() >= MAX_FETCH_SAMPLES {
break;
}
}
}
if samples.is_empty() {
return prev_dropped;
}
self.fetch_calls += 1;
let fresh = match self.fetch.as_ref() {
Some(cb) => cb(&samples),
None => return prev_dropped,
};
let mut added = 0usize;
if let DecryptKeys::Aacs { unit_keys, .. } = &mut self.keys {
for k in fresh {
if !unit_keys.iter().any(|(_, have)| *have == k) {
let idx = unit_keys.len() as u32;
unit_keys.push((idx, k));
added += 1;
}
}
}
if added == 0 {
self.fetch_spent = true;
return prev_dropped;
}
Self::decrypt_buf(buf, &mut self.keys, self.unit_key_idx, lba, content)
.unwrap_or(prev_dropped)
}
fn diagnose_decrypt_failure(
base_lba: u32,
buf: &[u8],
read_ms: u64,
content: Option<&[(u32, u32)]>,
keys: &DecryptKeys,
) {
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
let unit_sectors = (unit_len / 2048) as u32;
let (unit_keys, rdk) = match keys {
DecryptKeys::Aacs {
unit_keys,
read_data_key,
} => (unit_keys, *read_data_key),
_ => return,
};
let mut emitted = 0usize;
for (i, chunk) in buf.chunks_exact(unit_len).enumerate() {
if emitted >= MAX_DIAG_UNITS_PER_READ {
break;
}
let unit_lba = base_lba.saturating_add(i as u32 * unit_sectors);
let in_content = match content {
Some(r) => crate::decrypt::lba_in_ranges(unit_lba, r),
None => true,
};
if !in_content || !crate::aacs::aacs_unit_needs_decrypt(chunk) {
continue;
}
let all_zero = chunk.iter().all(|&b| b == 0);
let ts_sync = crate::aacs::ts_sync_count(chunk);
let ts_total = crate::aacs::ts_packet_total(chunk);
let mut seen = [false; 256];
for &b in chunk {
seen[b as usize] = true;
}
let distinct = seen.iter().filter(|&&x| x).count();
let mut best_sync = ts_sync;
for (_, k) in unit_keys.iter() {
let mut attempt = chunk.to_vec();
if let Some(ref rdk_key) = rdk {
crate::aacs::decrypt_bus(&mut attempt, rdk_key);
}
crate::aacs::decrypt_unit(&mut attempt, k);
let s = crate::aacs::ts_sync_count(&attempt);
if s > best_sync {
best_sync = s;
}
}
let head: String = chunk[..16].iter().map(|b| format!("{b:02x}")).collect();
tracing::warn!(
target: "freemkv::decrypt",
lba = unit_lba,
in_content,
read_ms,
all_zero,
ts_sync,
ts_total,
distinct,
best_sync,
keys_held = unit_keys.len(),
head,
"decrypt-verify fail"
);
emitted += 1;
}
}
}
impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
fn capacity_sectors(&self) -> u32 {
self.inner.capacity_sectors()
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
if matches!(self.keys, DecryptKeys::Aacs { .. })
&& !crate::aacs::is_unit_aligned(lba, self.unit_base)
{
return Err(crate::error::Error::DecryptFailed);
}
let read_t0 = std::time::Instant::now();
let n = self.inner.read_sectors(lba, count, buf, recovery)?;
let read_ms = read_t0.elapsed().as_millis() as u64;
let content = self.content_ranges.clone(); let content_ref = content.as_deref();
let fetch_viable =
!self.fetch_spent && self.fetch.is_some() && self.fetch_calls < MAX_FETCH_CALLS;
let dropped = if self.verify_only {
let mut scratch = std::mem::take(&mut self.scratch);
scratch.clear();
scratch.extend_from_slice(&buf[..n]);
let mut d = match Self::decrypt_buf(
&mut scratch,
&mut self.keys,
self.unit_key_idx,
lba,
content_ref,
) {
Ok(d) => d,
Err(e) => {
self.scratch = scratch;
return Err(e);
}
};
if d > 0 && fetch_viable {
d = self.fetch_failed_units(&mut scratch, lba, content_ref, d);
}
self.scratch = scratch;
d
} else {
let mut d = Self::decrypt_buf(
&mut buf[..n],
&mut self.keys,
self.unit_key_idx,
lba,
content_ref,
)?;
if d > 0 && fetch_viable {
d = self.fetch_failed_units(&mut buf[..n], lba, content_ref, d);
}
d
};
if dropped > 0 {
self.decrypt_dropped
.fetch_add(dropped as u64, Ordering::Relaxed);
if DECRYPT_VERIFY_READ {
let diag: &[u8] = if self.verify_only {
&self.scratch
} else {
&buf[..n]
};
Self::diagnose_decrypt_failure(
lba,
diag,
read_ms,
self.content_ranges.as_deref(),
&self.keys,
);
return Err(crate::error::Error::DecryptFailed);
}
}
Ok(n)
}
fn set_speed(&mut self, kbs: u16) {
self.inner.set_speed(kbs)
}
fn set_unit_base(&mut self, lba: u32) {
self.unit_base = lba;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
struct PatternedSource {
capacity: u32,
}
impl PatternedSource {
fn fill(lba: u32, count: u16, buf: &mut [u8]) {
let bytes = count as usize * 2048;
for (i, slot) in buf[..bytes].iter_mut().enumerate() {
let abs = lba as u64 * 2048 + i as u64;
*slot = ((abs.wrapping_mul(2654435761) >> 16) & 0xff) as u8;
}
}
}
impl SectorSource for PatternedSource {
fn capacity_sectors(&self) -> u32 {
self.capacity
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
Self::fill(lba, count, buf);
Ok(count as usize * 2048)
}
}
#[test]
fn passthrough_with_no_keys() {
let src = PatternedSource { capacity: 16 };
let mut wrapped = DecryptingSectorSource::new(src, DecryptKeys::None);
assert_eq!(wrapped.capacity_sectors(), 16);
let mut got = vec![0u8; 4 * 2048];
let n = wrapped.read_sectors(3, 4, &mut got, false).unwrap();
assert_eq!(n, 4 * 2048);
let mut expected = vec![0u8; 4 * 2048];
PatternedSource::fill(3, 4, &mut expected);
assert_eq!(got, expected);
}
#[test]
fn passthrough_set_speed_delegates() {
struct SpeedRecorder {
last: Option<u16>,
}
impl SectorSource for SpeedRecorder {
fn capacity_sectors(&self) -> u32 {
0
}
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
_buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
Ok(0)
}
fn set_speed(&mut self, kbs: u16) {
self.last = Some(kbs);
}
}
let mut wrapped =
DecryptingSectorSource::new(SpeedRecorder { last: None }, DecryptKeys::None);
wrapped.set_speed(7200);
assert_eq!(wrapped.inner().last, Some(7200));
}
use std::sync::{Arc, Mutex};
struct ShortReportSource {
report_n: usize,
}
impl ShortReportSource {
fn fill_one(buf: &mut [u8]) {
for (i, b) in buf.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(29).wrapping_add(3);
}
buf[0x14] = 0x30; }
}
impl SectorSource for ShortReportSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
for s in 0..count as usize {
Self::fill_one(&mut buf[s * 2048..(s + 1) * 2048]);
}
Ok(self.report_n)
}
}
struct ArgRecorder {
calls: Arc<Mutex<Vec<(u32, u16, bool)>>>,
}
impl SectorSource for ArgRecorder {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
self.calls.lock().unwrap().push((lba, count, recovery));
let bytes = count as usize * 2048;
buf[..bytes].fill(0);
Ok(bytes)
}
}
struct FailingSource;
impl SectorSource for FailingSource {
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
_buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
Err(crate::error::Error::IoError {
source: std::io::Error::from(std::io::ErrorKind::TimedOut),
})
}
}
#[test]
fn css_unscrambled_sector_passes_through() {
struct FixedSector {
template: [u8; 2048],
}
impl SectorSource for FixedSector {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
for s in 0..count as usize {
buf[s * 2048..(s + 1) * 2048].copy_from_slice(&self.template);
}
Ok(bytes)
}
}
let mut template = [0u8; 2048];
for (i, b) in template.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(13).wrapping_add(7);
}
template[0x14] = 0x00;
let expected = template;
let mut wrapped = DecryptingSectorSource::new(
FixedSector { template },
DecryptKeys::Css {
title_key: [0x11, 0x22, 0x33, 0x44, 0x55],
},
);
let mut got = [0u8; 2048];
let n = wrapped.read_sectors(0, 1, &mut got, false).unwrap();
assert_eq!(n, 2048);
assert_eq!(
got, expected,
"unscrambled CSS sector (flags=0) must pass through untouched"
);
}
#[test]
fn decrypt_span_bounded_by_reported_n() {
let mut wrapped = DecryptingSectorSource::new(
ShortReportSource { report_n: 0 },
DecryptKeys::Css {
title_key: [1, 2, 3, 4, 5],
},
);
let mut expected = vec![0u8; 2048];
ShortReportSource::fill_one(&mut expected);
let mut got = vec![0u8; 2048];
let n = wrapped.read_sectors(5, 1, &mut got, false).unwrap();
assert_eq!(n, 0, "decorator must return the inner source's n");
assert_eq!(
got, expected,
"with n=0 the decrypt span is empty; buffer must be untouched"
);
assert_eq!(got[0x14] & 0x30, 0x30, "scramble flags must remain set");
}
#[test]
fn args_forwarded_verbatim() {
let calls = Arc::new(Mutex::new(Vec::new()));
let mut wrapped = DecryptingSectorSource::new(
ArgRecorder {
calls: calls.clone(),
},
DecryptKeys::None,
);
let mut buf = vec![0u8; 2 * 2048];
wrapped.read_sectors(12345, 2, &mut buf, true).unwrap();
wrapped.read_sectors(0, 1, &mut buf, false).unwrap();
assert_eq!(
*calls.lock().unwrap(),
vec![(12345, 2, true), (0, 1, false)],
"lba/count/recovery must pass through unchanged"
);
}
#[test]
fn inner_read_error_propagates() {
let mut wrapped = DecryptingSectorSource::new(FailingSource, DecryptKeys::None);
let mut buf = vec![0u8; 2048];
let r = wrapped.read_sectors(0, 1, &mut buf, false);
let err = r.expect_err("inner error must propagate");
let io: std::io::Error = err.into();
assert_eq!(io.kind(), std::io::ErrorKind::TimedOut);
}
#[test]
fn aacs_missing_unit_key_errors() {
let src = PatternedSource { capacity: 16 };
let mut wrapped = DecryptingSectorSource::new(
src,
DecryptKeys::Aacs {
unit_keys: Vec::new(),
read_data_key: None,
},
);
let mut buf = vec![0u8; 2048];
let r = wrapped.read_sectors(0, 1, &mut buf, false);
let err = r.expect_err("missing unit key must error, not pass through encrypted");
assert_eq!(
err.code(),
crate::error::Error::DecryptFailed.code(),
"must surface DecryptFailed"
);
}
struct ClearUnitSource;
impl SectorSource for ClearUnitSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
buf[..bytes].fill(0);
let mut off = 4usize;
while off < bytes {
buf[off] = 0x47;
off += 192;
}
Ok(bytes)
}
}
#[test]
fn with_unit_key_idx_selects_key() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0u32, [0u8; 16])],
read_data_key: None,
};
let mut buf = vec![0u8; 3 * 2048];
let mut bad =
DecryptingSectorSource::new(ClearUnitSource, keys.clone()).with_unit_key_idx(2);
assert!(
bad.read_sectors(0, 3, &mut buf, false).is_err(),
"out-of-range unit_key_idx must fail the lookup"
);
let mut good = DecryptingSectorSource::new(ClearUnitSource, keys).with_unit_key_idx(0);
let mut buf2 = vec![0u8; 3 * 2048];
let n = good.read_sectors(0, 3, &mut buf2, false).unwrap();
assert_eq!(n, 3 * 2048);
assert_eq!(
buf2[4], 0x47,
"clear unit must be left intact under valid idx"
);
}
#[test]
fn set_keys_swaps_active_keys() {
struct ScrambledSector {
template: [u8; 2048],
}
impl SectorSource for ScrambledSector {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
for s in 0..count as usize {
buf[s * 2048..(s + 1) * 2048].copy_from_slice(&self.template);
}
Ok(bytes)
}
}
let mut template = [0u8; 2048];
for (i, b) in template.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(29).wrapping_add(3);
}
template[0x14] = 0x30; let pristine = template;
let mut wrapped =
DecryptingSectorSource::new(ScrambledSector { template }, DecryptKeys::None);
let mut got = [0u8; 2048];
wrapped.read_sectors(0, 1, &mut got, false).unwrap();
assert_eq!(
got, pristine,
"None keys must pass the sector through unchanged"
);
assert_eq!(
got[0x14] & 0x30,
0x30,
"None must leave the scramble flags set"
);
wrapped.set_keys(DecryptKeys::Css {
title_key: [0xa1, 0xb2, 0xc3, 0xd4, 0xe5],
});
let mut got2 = [0u8; 2048];
wrapped.read_sectors(0, 1, &mut got2, false).unwrap();
assert_eq!(
got2[0x14] & 0x30,
0x00,
"CSS descramble must clear the scramble-control bits"
);
assert_ne!(
&got2[128..2048],
&pristine[128..2048],
"CSS descramble must alter the encrypted data region"
);
}
#[test]
fn aacs_unaligned_start_lba_rejected() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0u32, [0u8; 16])],
read_data_key: None,
};
for lba in [1u32, 2, 4, 5, 32, 64] {
let mut wrapped = DecryptingSectorSource::new(ClearUnitSource, keys.clone());
let mut buf = vec![0u8; 3 * 2048];
let r = wrapped.read_sectors(lba, 3, &mut buf, false);
let err = r.expect_err("unaligned AACS start LBA must reject");
assert_eq!(
err.code(),
crate::error::Error::DecryptFailed.code(),
"lba {lba} (% 3 = {}) must reject with DecryptFailed",
lba % 3
);
}
for lba in [0u32, 3, 33, 66] {
let mut wrapped = DecryptingSectorSource::new(ClearUnitSource, keys.clone());
let mut buf = vec![0u8; 3 * 2048];
let n = wrapped
.read_sectors(lba, 3, &mut buf, false)
.unwrap_or_else(|_| panic!("aligned lba {lba} must pass the guard"));
assert_eq!(n, 3 * 2048);
}
}
#[test]
fn aacs_gate_is_clip_anchored_not_absolute() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0u32, [0u8; 16])],
read_data_key: None,
};
let base = 64u32;
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);
}
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"
);
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"
);
}
}
#[test]
fn css_start_lba_not_unit_gated() {
let mut wrapped = DecryptingSectorSource::new(
ClearUnitSource,
DecryptKeys::Css {
title_key: [0u8; 5],
},
);
let mut buf = vec![0u8; 2048];
let n = wrapped.read_sectors(1, 1, &mut buf, false).unwrap();
assert_eq!(n, 2048, "CSS reads must not be unit-alignment gated");
}
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
let mut off = 4;
while off < unit.len() {
unit[off] = 0x47;
off += 192;
}
unit[0] |= 0xC0;
let header: [u8; 16] = unit[..16].try_into().unwrap();
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
let mut k = [0u8; 16];
for i in 0..16 {
k[i] = derived[i] ^ header[i];
}
let cipher = Aes128::new(GenericArray::from_slice(&k));
let mut prev = crate::aacs::decrypt::AACS_IV;
let blocks = (crate::aacs::ALIGNED_UNIT_LEN - 16) / 16;
for i in 0..blocks {
let o = 16 + i * 16;
for j in 0..16 {
unit[o + j] ^= prev[j];
}
let mut blk = GenericArray::clone_from_slice(&unit[o..o + 16]);
cipher.encrypt_block(&mut blk);
unit[o..o + 16].copy_from_slice(&blk);
prev.copy_from_slice(&unit[o..o + 16]);
}
unit
}
#[test]
fn decrypt_loss_counter_accumulates_undecryptable_units() {
let real_key = [0x33u8; 16];
let wrong_key = [0x44u8; 16];
struct EncUnitSource {
unit: Vec<u8>,
}
impl SectorSource for EncUnitSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
assert_eq!(bytes, self.unit.len(), "test reads one whole unit");
buf[..bytes].copy_from_slice(&self.unit);
Ok(bytes)
}
}
let unit = encrypt_aacs_unit(&real_key);
let mut wrapped = DecryptingSectorSource::new(
EncUnitSource { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong_key)],
read_data_key: None,
},
);
let loss = wrapped.decrypt_loss();
assert_eq!(loss.load(Ordering::Relaxed), 0, "starts at zero");
let mut buf = vec![0u8; 3 * 2048];
let err = wrapped
.read_sectors(0, 3, &mut buf, false)
.expect_err("DECRYPT_VERIFY_READ: an undecryptable AACS unit fails the read loud");
assert!(
matches!(err, crate::error::Error::DecryptFailed),
"undecryptable unit errors with DecryptFailed, got {err:?}"
);
assert_eq!(
loss.load(Ordering::Relaxed),
crate::aacs::ALIGNED_UNIT_LEN as u64,
"the undecryptable unit is tallied as loss before the read errors"
);
assert!(
wrapped.read_sectors(0, 3, &mut buf, false).is_err(),
"the same bad unit fails the read again"
);
assert_eq!(
loss.load(Ordering::Relaxed),
2 * crate::aacs::ALIGNED_UNIT_LEN as u64,
"loss must accumulate across reads"
);
let mut good = DecryptingSectorSource::new(
EncUnitSource { unit },
DecryptKeys::Aacs {
unit_keys: vec![(0, real_key)],
read_data_key: None,
},
);
let good_loss = good.decrypt_loss();
good.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(
good_loss.load(Ordering::Relaxed),
0,
"a decryptable unit must not register any loss"
);
}
#[test]
fn key_fetch_recovers_unit_with_a_fresh_key() {
let real_key = [0x5au8; 16]; let wrong_key = [0x11u8; 16];
struct EncUnitSource {
unit: Vec<u8>,
}
impl SectorSource for EncUnitSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
buf[..bytes].copy_from_slice(&self.unit);
Ok(bytes)
}
}
let unit = encrypt_aacs_unit(&real_key);
let seen: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
let seen_cb = Arc::clone(&seen);
let fetch: super::KeyFetch = std::sync::Arc::new(move |samples: &[Vec<u8>]| {
seen_cb.lock().unwrap().extend_from_slice(samples);
vec![real_key]
});
let mut wrapped = DecryptingSectorSource::new(
EncUnitSource { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong_key)],
read_data_key: None,
},
)
.with_key_fetch(fetch);
let loss = wrapped.decrypt_loss();
let mut buf = vec![0u8; 3 * 2048];
wrapped.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(
loss.load(Ordering::Relaxed),
0,
"fetch supplied the key → the unit decrypts → zero loss"
);
let got = seen.lock().unwrap();
assert_eq!(
got.len(),
1,
"callback must be invoked once with the failing unit"
);
assert!(
crate::aacs::ts_sync_destroyed(&got[0]),
"the sample handed to the callback is the still-scrambled ciphertext"
);
assert_eq!(
got[0], unit,
"the exact on-disc unit is forwarded for fetch"
);
let mut nocb = DecryptingSectorSource::new(
EncUnitSource { unit },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong_key)],
read_data_key: None,
},
);
let nocb_loss = nocb.decrypt_loss();
let mut buf2 = vec![0u8; 3 * 2048];
assert!(
nocb.read_sectors(0, 3, &mut buf2, false).is_err(),
"without a fetch callback the undecryptable unit fails the read (DECRYPT_VERIFY_READ)"
);
assert_eq!(
nocb_loss.load(Ordering::Relaxed),
crate::aacs::ALIGNED_UNIT_LEN as u64,
"without a fetch callback the undecryptable unit is loss"
);
}
#[test]
fn inner_accessors_round_trip() {
let src = PatternedSource { capacity: 42 };
let mut wrapped = DecryptingSectorSource::new(src, DecryptKeys::None);
assert_eq!(wrapped.inner().capacity_sectors(), 42);
assert_eq!(wrapped.inner_mut().capacity_sectors(), 42);
let recovered = wrapped.into_inner();
assert_eq!(recovered.capacity_sectors(), 42);
}
#[test]
fn verify_only_checks_without_mutating_and_fails_on_undecryptable() {
let real_key = [0x33u8; 16];
let wrong_key = [0x44u8; 16];
struct EncUnitSource {
unit: Vec<u8>,
}
impl SectorSource for EncUnitSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
buf[..bytes].copy_from_slice(&self.unit);
Ok(bytes)
}
}
let unit = encrypt_aacs_unit(&real_key);
let mut bad = DecryptingSectorSource::new(
EncUnitSource { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong_key)],
read_data_key: None,
},
)
.verify_only();
let mut buf = vec![0u8; 3 * 2048];
let err = bad
.read_sectors(0, 3, &mut buf, false)
.expect_err("verify-only: an undecryptable unit must fail the read");
assert!(matches!(err, crate::error::Error::DecryptFailed));
assert_eq!(
buf, unit,
"verify-only must NOT mutate buf — ISO stays ciphertext"
);
let mut good = DecryptingSectorSource::new(
EncUnitSource { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, real_key)],
read_data_key: None,
},
)
.verify_only();
let mut buf2 = vec![0u8; 3 * 2048];
good.read_sectors(0, 3, &mut buf2, false)
.expect("verify-only: a decryptable unit reads OK");
assert_eq!(
buf2, unit,
"verify-only leaves ciphertext in buf even when the unit decrypts"
);
}
#[test]
fn verify_only_content_gate_passes_clear_filesystem_fails_content() {
struct ScrambledSource;
impl SectorSource for ScrambledSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
for (i, b) in buf[..bytes].iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(31);
}
let mut off = 4;
while off < bytes {
buf[off] = 0xA5; off += 192;
}
let mut u = 0;
while u < bytes {
buf[u] |= 0xC0;
u += crate::aacs::ALIGNED_UNIT_LEN;
}
Ok(bytes)
}
}
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
};
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(1002u32, 99u32)]);
let mut dec = DecryptingSectorSource::new(ScrambledSource, keys)
.verify_only()
.with_content_ranges(ranges);
let mut buf = vec![0u8; 3 * 2048];
dec.read_sectors(0, 3, &mut buf, false)
.expect("a clear filesystem region must read OK — no false decrypt-fail");
let err = dec
.read_sectors(1002, 3, &mut buf, false)
.expect_err("an undecryptable content unit must fail the read");
assert!(matches!(err, crate::error::Error::DecryptFailed));
}
struct FixedUnit {
unit: Vec<u8>,
}
impl SectorSource for FixedUnit {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
buf[..bytes].copy_from_slice(&self.unit);
Ok(bytes)
}
}
#[test]
fn verify_only_content_gate_decryptable_unit_keeps_ciphertext() {
let key = [0x5a; 16];
let unit = encrypt_aacs_unit(&key);
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 3u32)]); let mut dec = DecryptingSectorSource::new(
FixedUnit { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
},
)
.verify_only()
.with_content_ranges(ranges);
let mut buf = vec![0u8; 3 * 2048];
dec.read_sectors(0, 3, &mut buf, false)
.expect("a decryptable content unit reads OK");
assert_eq!(
buf, unit,
"verify-only keeps ciphertext even when the unit decrypts"
);
}
#[test]
fn verify_only_without_content_map_is_ungated() {
struct ScrambledSource;
impl SectorSource for ScrambledSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_r: bool,
) -> Result<usize> {
let b = count as usize * 2048;
for (i, x) in buf[..b].iter_mut().enumerate() {
*x = (i as u8).wrapping_mul(31);
}
let mut o = 4;
while o < b {
buf[o] = 0xA5;
o += 192;
}
let mut u = 0;
while u < b {
buf[u] |= 0xC0; u += crate::aacs::ALIGNED_UNIT_LEN;
}
Ok(b)
}
}
let mut dec = DecryptingSectorSource::new(
ScrambledSource,
DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
},
)
.verify_only(); let mut buf = vec![0u8; 3 * 2048];
let err = dec
.read_sectors(0, 3, &mut buf, false)
.expect_err("ungated verify fails on scrambled bytes (legacy / mux behaviour)");
assert!(matches!(err, crate::error::Error::DecryptFailed));
}
#[test]
fn inplace_decrypt_content_gate_passes_clear_decrypts_content() {
let key = [0x5a; 16];
let cipher_unit = encrypt_aacs_unit(&key);
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(1002u32, 99u32)]); let mut dec = DecryptingSectorSource::new(
FixedUnit {
unit: cipher_unit.clone(),
},
DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
},
)
.with_content_ranges(ranges);
let mut buf = vec![0u8; 3 * 2048];
dec.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(
buf, cipher_unit,
"a non-content read is passed through, not decrypted"
);
let mut buf2 = vec![0u8; 3 * 2048];
dec.read_sectors(1002, 3, &mut buf2, false).unwrap();
assert_ne!(
buf2, cipher_unit,
"an in-content read is decrypted in place"
);
assert_eq!(buf2[4], 0x47, "decrypted content carries the TS sync byte");
}
struct AnyLbaUnit {
unit: Vec<u8>,
}
impl SectorSource for AnyLbaUnit {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_r: bool,
) -> Result<usize> {
let b = count as usize * 2048;
buf[..b].copy_from_slice(&self.unit);
Ok(b)
}
}
#[test]
fn verify_only_fetch_recovers_caches_and_keeps_ciphertext() {
let real_key = [0x5au8; 16]; let wrong_key = [0x11u8; 16]; let unit = encrypt_aacs_unit(&real_key);
let calls = Arc::new(Mutex::new(0usize));
let calls_cb = Arc::clone(&calls);
let fetch: super::KeyFetch = std::sync::Arc::new(move |samples: &[Vec<u8>]| {
*calls_cb.lock().unwrap() += 1;
assert!(!samples.is_empty(), "fetch receives the failing units");
vec![real_key]
});
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 6u32)]); let mut dec = DecryptingSectorSource::new(
AnyLbaUnit { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong_key)],
read_data_key: None,
},
)
.verify_only()
.with_content_ranges(ranges)
.with_key_fetch(fetch);
let mut buf = vec![0u8; 3 * 2048];
dec.read_sectors(0, 3, &mut buf, false)
.expect("fetch recovers the orphan unit's key");
assert_eq!(buf, unit, "verify-only keeps ciphertext even after a fetch");
assert_eq!(*calls.lock().unwrap(), 1, "fetch called exactly once");
let mut buf2 = vec![0u8; 3 * 2048];
dec.read_sectors(3, 3, &mut buf2, false)
.expect("cached key serves the next unit");
assert_eq!(
*calls.lock().unwrap(),
1,
"cache hit — the fetch callback must NOT fire again"
);
}
#[test]
fn verify_only_fetch_exhausted_still_hard_fails() {
let real_key = [0x5au8; 16];
let wrong = [0x11u8; 16];
let unit = encrypt_aacs_unit(&real_key);
let fetch: super::KeyFetch = std::sync::Arc::new(|_: &[Vec<u8>]| Vec::new());
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(0u32, 3u32)]);
let mut dec = DecryptingSectorSource::new(
FixedUnit { unit: unit.clone() },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong)],
read_data_key: None,
},
)
.verify_only()
.with_content_ranges(ranges)
.with_key_fetch(fetch);
let mut buf = vec![0u8; 3 * 2048];
let err = dec
.read_sectors(0, 3, &mut buf, false)
.expect_err("a fetch that returns no key must still fail the read");
assert!(matches!(err, crate::error::Error::DecryptFailed));
}
#[test]
fn verify_only_fetch_not_called_outside_content() {
let real_key = [0x5au8; 16];
let wrong = [0x11u8; 16];
let unit = encrypt_aacs_unit(&real_key);
let calls = Arc::new(Mutex::new(0usize));
let calls_cb = Arc::clone(&calls);
let fetch: super::KeyFetch = std::sync::Arc::new(move |_: &[Vec<u8>]| {
*calls_cb.lock().unwrap() += 1;
vec![real_key]
});
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(1002u32, 99u32)]);
let mut dec = DecryptingSectorSource::new(
AnyLbaUnit { unit },
DecryptKeys::Aacs {
unit_keys: vec![(0, wrong)],
read_data_key: None,
},
)
.verify_only()
.with_content_ranges(ranges)
.with_key_fetch(fetch);
let mut buf = vec![0u8; 3 * 2048];
dec.read_sectors(0, 3, &mut buf, false)
.expect("non-content scrambled-looking bytes read OK (gated out)");
assert_eq!(
*calls.lock().unwrap(),
0,
"fetch must NOT fire for a non-content unit"
);
}
}