Test hygiene: real AACS decrypt coverage, drop dead recovery fixtures
- Repoint three tautological AACS tests (which passed only via the unconditional-Err AACS arm of decrypt_sectors) at the real shipping path decrypt_sectors_mapped: out-of-range mapped key index, empty pool vs a non-empty map, and a scrambled encrypted trailing partial (the new guard) — plus a clear-trailing-partial pass-through and an explicit fail-loud safety-net test for AACS reaching the unmapped wrapper. - Add the missing end-to-end AACS round-trip through the decorator + key map (aacs_decorator_decrypts_encrypted_unit_via_map) and a mapless-AACS-fails-loud decorator test — the exact class of bug the TrueHD probe shipped. Fills the // TODO: AACS round-trip test gap. - Delete the now-dead recovery test fixtures (encrypt_aacs_unit_bad, AnyLbaUnit) left after the reactive key-fetch path was removed; keep encrypt_aacs_unit + FixedUnit, now used by the round-trip test.
This commit is contained in:
+78
-42
@@ -704,25 +704,29 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whole leading units plus a SCRAMBLED trailing partial (the malformed
|
/// Whole leading unit plus a SCRAMBLED trailing partial that is FLAGGED
|
||||||
/// danger case): an encrypted unit split across an extent boundary cannot be
|
/// encrypted in its clear seed (the malformed danger case): an encrypted unit
|
||||||
/// decrypted standalone. Passing it through as clear would be silent
|
/// split across an extent boundary cannot be CBC-decrypted standalone. The
|
||||||
/// corruption, so we must fail loud with `DecryptFailed`.
|
/// mapped decrypt must fail loud with `DecryptFailed` rather than emit the
|
||||||
|
/// ciphertext partial as clear. Exercises the real shipping path
|
||||||
|
/// (`decrypt_sectors_mapped`) and its trailing-partial guard.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_scrambled_trailing_partial_is_rejected() {
|
fn aacs_scrambled_trailing_partial_is_rejected() {
|
||||||
let mut keys = DecryptKeys::Aacs {
|
let keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
format: crate::disc::ContentFormat::BdTs,
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
};
|
};
|
||||||
// One full unit + a 4096-byte (two-sector) SCRAMBLED tail.
|
// One CLEAR leading unit (passes through) + a 4096-byte (two-sector) tail
|
||||||
let unit = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
// whose seed byte flags it encrypted, inside the mapped range.
|
||||||
let tail = scrambled_region(4096);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = unit;
|
let mut tail = scrambled_region(4096);
|
||||||
|
tail[0] |= 0xC0; // CPI bits → flagged encrypted on the partial
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
|
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 0)
|
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
|
||||||
.expect_err("scrambled trailing partial must be rejected");
|
let err = decrypt_sectors_mapped(&mut buf, &keys, 0, &map)
|
||||||
|
.expect_err("scrambled encrypted trailing partial must be rejected");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.code(),
|
err.code(),
|
||||||
crate::error::Error::DecryptFailed.code(),
|
crate::error::Error::DecryptFailed.code(),
|
||||||
@@ -730,6 +734,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A CLEAR trailing partial (encrypted flag NOT set) is a legitimate content
|
||||||
|
/// tail and must pass through, never trip the guard above.
|
||||||
|
#[test]
|
||||||
|
fn aacs_clear_trailing_partial_passes_through() {
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
|
read_data_key: None,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
};
|
||||||
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
|
let mut tail = clear_ts_region(4096);
|
||||||
|
tail[0] &= 0x3F; // ensure the CPI bits are clear
|
||||||
|
buf.extend_from_slice(&tail);
|
||||||
|
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
|
||||||
|
assert!(decrypt_sectors_mapped(&mut buf, &keys, 0, &map).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
// ── DecryptKeys::None and is_encrypted ─────────────────────────────────
|
// ── DecryptKeys::None and is_encrypted ─────────────────────────────────
|
||||||
|
|
||||||
/// DecryptKeys::None is a pure no-op: the buffer must be returned
|
/// DecryptKeys::None is a pure no-op: the buffer must be returned
|
||||||
@@ -960,45 +981,60 @@ mod tests {
|
|||||||
|
|
||||||
// ── AACS unit-key index selection ──────────────────────────────────────
|
// ── AACS unit-key index selection ──────────────────────────────────────
|
||||||
|
|
||||||
/// AACS decrypt with an out-of-range unit_key_idx must fail loud with
|
/// A map that selects a key index OUTSIDE the held pool must fail loud with
|
||||||
/// DecryptFailed — never silently fall back to a wrong key or pass
|
/// DecryptFailed — never silently apply a wrong key or pass ciphertext through.
|
||||||
/// encrypted data through as clear.
|
/// This validates `decrypt_sectors_mapped`'s up-front `key_indices()` bounds
|
||||||
|
/// check (the real shipping AACS decrypt path).
|
||||||
///
|
///
|
||||||
/// Grounding: `let uk = match unit_keys.get(unit_key_idx) { Some => ...,
|
/// Mutation: drop the `unit_keys.get(idx).is_none()` guard → the out-of-range
|
||||||
/// None => return Err(DecryptFailed) }`.
|
/// index would not error; this fails.
|
||||||
/// Mutation: change `unit_keys.get(unit_key_idx)` to `unit_keys.get(0)` or
|
|
||||||
/// `.unwrap_or` a default -> the out-of-range index would not error; this
|
|
||||||
/// fails.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_out_of_range_unit_key_idx_errors() {
|
fn aacs_mapped_out_of_range_key_idx_errors() {
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
|
read_data_key: None,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
};
|
||||||
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
|
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 5)]); // idx 5, pool holds 1 key
|
||||||
|
let err = decrypt_sectors_mapped(&mut buf, &keys, 0, &map)
|
||||||
|
.expect_err("map index 5 is out of range for a 1-key pool");
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
crate::error::Error::DecryptFailed.code(),
|
||||||
|
"out-of-range mapped key index must be DecryptFailed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A non-empty map over an EMPTY unit_keys pool has no key to satisfy its
|
||||||
|
/// selected index → DecryptFailed (via the same bounds check).
|
||||||
|
#[test]
|
||||||
|
fn aacs_mapped_empty_unit_keys_errors() {
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![],
|
||||||
|
read_data_key: None,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
};
|
||||||
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
|
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
|
||||||
|
let err = decrypt_sectors_mapped(&mut buf, &keys, 0, &map)
|
||||||
|
.expect_err("empty unit_keys cannot satisfy map idx 0");
|
||||||
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SAFETY NET: reaching the CSS/`None` wrapper (`decrypt_sectors`) with AACS
|
||||||
|
/// keys means a reader was built with no map — a bug. It must fail loud, never
|
||||||
|
/// apply a guessed key. (AACS decrypts exclusively via `decrypt_sectors_mapped`.)
|
||||||
|
#[test]
|
||||||
|
fn aacs_via_unmapped_decrypt_sectors_fails_loud() {
|
||||||
let mut keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
format: crate::disc::ContentFormat::BdTs,
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
let err = decrypt_sectors(&mut buf, &mut keys, 0)
|
||||||
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
.expect_err("AACS through the unmapped path must fail loud");
|
||||||
assert_eq!(
|
|
||||||
err.code(),
|
|
||||||
crate::error::Error::DecryptFailed.code(),
|
|
||||||
"out-of-range unit key index must be DecryptFailed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AACS with an empty unit_keys list and any index errors (no key to use).
|
|
||||||
///
|
|
||||||
/// Grounding: `unit_keys.get(0)` on an empty Vec is None -> DecryptFailed.
|
|
||||||
/// Mutation: defaulting to [0u8;16] on None would proceed; this fails.
|
|
||||||
#[test]
|
|
||||||
fn aacs_empty_unit_keys_errors() {
|
|
||||||
let mut keys = DecryptKeys::Aacs {
|
|
||||||
unit_keys: vec![],
|
|
||||||
read_data_key: None,
|
|
||||||
format: crate::disc::ContentFormat::BdTs,
|
|
||||||
};
|
|
||||||
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
|
||||||
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-56
@@ -792,47 +792,6 @@ mod tests {
|
|||||||
unit
|
unit
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`encrypt_aacs_unit`] but knocks out the TS sync on `bad_pkts` (kept as
|
|
||||||
/// NON-zero content, so they read as authored-bad packets, not padding) BEFORE
|
|
||||||
/// encryption — a unit the correct key still OPENS on its remaining good
|
|
||||||
/// packets, but that carries bad-encoded content the muxer must drop.
|
|
||||||
fn encrypt_aacs_unit_bad(unit_key: &[u8; 16], bad_pkts: &[usize]) -> Vec<u8> {
|
|
||||||
use aes::Aes128;
|
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
|
||||||
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
|
||||||
let mut off = 4;
|
|
||||||
while off < unit.len() {
|
|
||||||
unit[off] = 0x47;
|
|
||||||
off += 192;
|
|
||||||
}
|
|
||||||
for &p in bad_pkts {
|
|
||||||
let o = p * 192;
|
|
||||||
unit[o + 4] = 0x00; // no TS sync after decrypt
|
|
||||||
unit[o + 5] = 0xAB; // non-zero payload => real content, not padding
|
|
||||||
}
|
|
||||||
unit[0] |= 0xC0;
|
|
||||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
|
||||||
let derived = crate::aacs::crypto::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::crypto::AACS_IV;
|
|
||||||
let blocks = (crate::aacs::content::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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `into_inner` / `inner` / `inner_mut` must hand back the original
|
/// `into_inner` / `inner` / `inner_mut` must hand back the original
|
||||||
/// source unchanged. Grounding: the accessor methods.
|
/// source unchanged. Grounding: the accessor methods.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -863,22 +822,55 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A source that returns a fixed encrypted unit for ANY read — used to drive
|
/// END-TO-END AACS: an encrypted unit read through the decorator with a matching
|
||||||
/// the verify-only fetch + cache tests below.
|
/// [`AacsKeyMap`] comes back as the known plaintext. This is the round-trip that
|
||||||
struct AnyLbaUnit {
|
/// was previously only asserted through the (deleted) reactive path — the actual
|
||||||
unit: Vec<u8>,
|
/// shipping mapped-decrypt path had no decorator-level coverage.
|
||||||
|
#[test]
|
||||||
|
fn aacs_decorator_decrypts_encrypted_unit_via_map() {
|
||||||
|
let key = [0x5Au8; 16];
|
||||||
|
let unit = encrypt_aacs_unit(&key);
|
||||||
|
let src = FixedUnit { unit };
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0, key)],
|
||||||
|
read_data_key: None,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
};
|
||||||
|
let map = std::sync::Arc::new(crate::decrypt::AacsKeyMap::from_ranges(vec![(
|
||||||
|
0,
|
||||||
|
u32::MAX,
|
||||||
|
0,
|
||||||
|
)]));
|
||||||
|
let mut dec = DecryptingSectorSource::new(src, keys).with_key_map(map);
|
||||||
|
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);
|
||||||
|
// Decrypted: the TS sync 0x47 reappears at the BD-TS stride (offset 4, then
|
||||||
|
// every 192 bytes). If the map/keys were wrong the bytes would stay
|
||||||
|
// ciphertext and these syncs would be absent.
|
||||||
|
for off in (4..crate::aacs::content::ALIGNED_UNIT_LEN).step_by(192) {
|
||||||
|
assert_eq!(buf[off], 0x47, "TS sync recovered at offset {off}");
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An AACS decorator built WITHOUT a key map must fail loud on the first unit —
|
||||||
|
/// the map is mandatory for AACS (it decrypts only via the mapped path). Guards
|
||||||
|
/// the class of bug the TrueHD probe shipped (a mapless AACS `DecryptingSectorSource`).
|
||||||
|
#[test]
|
||||||
|
fn aacs_decorator_without_map_fails_loud() {
|
||||||
|
let key = [0x5Au8; 16];
|
||||||
|
let unit = encrypt_aacs_unit(&key);
|
||||||
|
let src = FixedUnit { unit };
|
||||||
|
let keys = DecryptKeys::Aacs {
|
||||||
|
unit_keys: vec![(0, key)],
|
||||||
|
read_data_key: None,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
};
|
||||||
|
let mut dec = DecryptingSectorSource::new(src, keys); // no with_key_map
|
||||||
|
let mut buf = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
|
let err = dec
|
||||||
|
.read_sectors(0, 3, &mut buf, false)
|
||||||
|
.expect_err("AACS decorator with no key map must fail loud");
|
||||||
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user