use aes::Aes128;
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
pub(crate) const AACS_IV: [u8; 16] = [
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
];
pub const ALIGNED_UNIT_LEN: usize = 6144;
pub const ALIGNED_UNIT_SECTORS: u32 = (ALIGNED_UNIT_LEN / SECTOR_BYTES) as u32;
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
lba.saturating_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0
}
use crate::consts::SECTOR_BYTES;
use crate::consts::BD_SOURCE_PACKET_BYTES;
const TS_SYNC: u8 = 0x47;
pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(GenericArray::from_slice(key));
let mut block = GenericArray::clone_from_slice(data);
cipher.encrypt_block(&mut block);
let mut out = [0u8; 16];
out.copy_from_slice(&block);
out
}
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(GenericArray::from_slice(key));
let mut block = GenericArray::clone_from_slice(data);
cipher.decrypt_block(&mut block);
let mut out = [0u8; 16];
out.copy_from_slice(&block);
out
}
pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
debug_assert!(
data.len() % 16 == 0,
"aes_cbc_decrypt requires a block-aligned slice"
);
let cipher = Aes128::new(GenericArray::from_slice(key));
let num_blocks = data.len() / 16;
for i in (0..num_blocks).rev() {
let offset = i * 16;
let prev = if i == 0 {
AACS_IV
} else {
let mut p = [0u8; 16];
p.copy_from_slice(&data[(i - 1) * 16..i * 16]);
p
};
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
cipher.decrypt_block(&mut block);
for j in 0..16 {
data[offset + j] = block[j] ^ prev[j];
}
}
}
pub fn ts_sync_destroyed(unit: &[u8]) -> bool {
unit.len() >= ALIGNED_UNIT_LEN && !ts_syncs_intact(unit)
}
pub fn aacs_unit_encrypted(unit: &[u8]) -> bool {
unit.len() >= ALIGNED_UNIT_LEN && (unit[0] & 0xC0) != 0
}
pub fn aacs_unit_needs_decrypt(unit: &[u8]) -> bool {
aacs_unit_encrypted(unit) && ts_sync_destroyed(unit)
}
pub fn ts_sync_count(unit: &[u8]) -> usize {
let mut count = 0;
let mut offset = 4;
while offset < unit.len() {
if unit[offset] == TS_SYNC {
count += 1;
}
offset += BD_SOURCE_PACKET_BYTES;
}
count
}
pub fn ts_packet_total(unit: &[u8]) -> usize {
unit.len() / BD_SOURCE_PACKET_BYTES
}
fn ts_syncs_intact(unit: &[u8]) -> bool {
ts_sync_count(unit) > ts_packet_total(unit) / 2
}
pub fn unit_is_clean_ts(unit: &[u8]) -> bool {
if unit.len() < ALIGNED_UNIT_LEN {
return false;
}
let mut i = 0;
while i < ALIGNED_UNIT_LEN {
if unit[i + 4] != TS_SYNC {
return false;
}
i += BD_SOURCE_PACKET_BYTES;
}
true
}
pub fn unit_is_clean_ps(unit: &[u8]) -> bool {
if unit.len() < ALIGNED_UNIT_LEN {
return false;
}
const PACK_START: [u8; 4] = [0x00, 0x00, 0x01, 0xBA];
let mut o = 0;
while o < ALIGNED_UNIT_LEN {
if unit[o..o + 4] != PACK_START {
return false;
}
o += SECTOR_BYTES; }
true
}
pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
decrypt_unit_checked(unit, unit_key, unit_is_clean_ts)
}
pub fn decrypt_unit_checked(
unit: &mut [u8],
unit_key: &[u8; 16],
accept: fn(&[u8]) -> bool,
) -> bool {
if unit.len() < ALIGNED_UNIT_LEN {
return false;
}
if !aacs_unit_encrypted(unit) {
return true; }
let mut header = [0u8; 16];
header.copy_from_slice(&unit[..16]);
let derived = aes_ecb_encrypt(unit_key, &header);
let mut decrypt_key = [0u8; 16];
for i in 0..16 {
decrypt_key[i] = derived[i] ^ header[i];
}
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
accept(unit)
}
pub fn unit_key_validates(unit: &[u8], unit_key: &[u8; 16]) -> bool {
if unit.len() < ALIGNED_UNIT_LEN {
return false;
}
let mut header = [0u8; 16];
header.copy_from_slice(&unit[..16]);
let derived = aes_ecb_encrypt(unit_key, &header);
let mut decrypt_key = [0u8; 16];
for i in 0..16 {
decrypt_key[i] = derived[i] ^ header[i];
}
const SYNC_PAYLOAD_OFF: usize = 196;
let region_off = SYNC_PAYLOAD_OFF - 16; let blk = region_off / 16; let byte = region_off % 16; let c11 = 16 + blk * 16; let cipher = Aes128::new(GenericArray::from_slice(&decrypt_key));
let mut b = GenericArray::clone_from_slice(&unit[c11..c11 + 16]);
cipher.decrypt_block(&mut b);
let prev = unit[c11 - 16 + byte]; if b[byte] ^ prev != TS_SYNC {
return false;
}
let mut full = [0u8; ALIGNED_UNIT_LEN];
full.copy_from_slice(&unit[..ALIGNED_UNIT_LEN]);
decrypt_unit(&mut full, unit_key)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnitKeyResult {
AlreadyClear,
DecryptedWith(usize),
}
pub fn decrypt_unit_try_keys(unit: &mut [u8], unit_keys: &[[u8; 16]]) -> Option<UnitKeyResult> {
if !aacs_unit_encrypted(unit) {
return Some(UnitKeyResult::AlreadyClear);
}
let mut original = [0u8; ALIGNED_UNIT_LEN];
original.copy_from_slice(&unit[..ALIGNED_UNIT_LEN]);
for (i, key) in unit_keys.iter().enumerate() {
unit[..ALIGNED_UNIT_LEN].copy_from_slice(&original);
if decrypt_unit(unit, key) {
return Some(UnitKeyResult::DecryptedWith(i));
}
}
unit[..ALIGNED_UNIT_LEN].copy_from_slice(&original);
None
}
pub fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) {
for sector_start in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_BYTES) {
if sector_start + SECTOR_BYTES > unit.len() {
break;
}
aes_cbc_decrypt(
read_data_key,
&mut unit[sector_start + 16..sector_start + SECTOR_BYTES],
);
}
}
pub fn decrypt_unit_full(
unit: &mut [u8],
unit_key: &[u8; 16],
read_data_key: Option<&[u8; 16]>,
) -> bool {
if !ts_sync_destroyed(unit) {
return true;
}
if let Some(rdk) = read_data_key {
decrypt_bus(unit, rdk);
}
decrypt_unit(unit, unit_key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aes_ecb_roundtrip() {
let key = [
0x15u8, 0x66, 0x5F, 0x98, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C,
];
let plain = [0x41u8; 16];
let enc = aes_ecb_encrypt(&key, &plain);
let dec = aes_ecb_decrypt(&key, &enc);
assert_eq!(dec, plain);
}
#[test]
fn is_unit_aligned_relative_to_base() {
assert!(is_unit_aligned(100, 100), "base itself is aligned");
assert!(is_unit_aligned(103, 100), "one unit past base is aligned");
assert!(is_unit_aligned(106, 100));
assert!(!is_unit_aligned(101, 100));
assert!(!is_unit_aligned(102, 100));
assert!(is_unit_aligned(101, 101), "non-3-aligned base is aligned");
assert!(is_unit_aligned(104, 101));
assert!(!is_unit_aligned(102, 101));
}
#[test]
fn is_unit_aligned_lba_below_base_is_well_defined() {
assert!(
is_unit_aligned(99, 100),
"lba just below base must not wrap"
);
assert!(is_unit_aligned(98, 100));
assert!(is_unit_aligned(0, 100));
assert!(is_unit_aligned(u32::MAX, u32::MAX)); assert!(
is_unit_aligned(0, u32::MAX),
"max base, lba 0 must saturate to 0"
);
}
#[test]
fn test_decrypt_unit_unencrypted() {
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
let mut off = 4;
while off < ALIGNED_UNIT_LEN {
unit[off] = TS_SYNC;
off += BD_SOURCE_PACKET_BYTES;
}
let key = [0u8; 16];
assert!(!ts_sync_destroyed(&unit));
assert!(decrypt_unit(&mut unit, &key));
}
#[test]
fn ts_packet_total_no_off_by_one() {
let unit = vec![0u8; ALIGNED_UNIT_LEN];
assert_eq!(ts_packet_total(&unit), 32);
let visited = (4..ALIGNED_UNIT_LEN)
.step_by(BD_SOURCE_PACKET_BYTES)
.count();
assert_eq!(visited, ts_packet_total(&unit));
}
#[test]
fn scramble_detection_at_16_32_boundary() {
let set_syncs = |n: usize| {
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
let mut off = 4;
let mut placed = 0;
while off < ALIGNED_UNIT_LEN && placed < n {
unit[off] = TS_SYNC;
off += BD_SOURCE_PACKET_BYTES;
placed += 1;
}
unit
};
assert_eq!(ts_sync_count(&set_syncs(16)), 16);
assert_eq!(ts_sync_count(&set_syncs(17)), 17);
assert!(ts_sync_destroyed(&set_syncs(16)));
assert!(!ts_sync_destroyed(&set_syncs(17)));
}
#[test]
fn scramble_detection_extremes() {
let mut clear = vec![0u8; ALIGNED_UNIT_LEN];
let mut off = 4;
while off < ALIGNED_UNIT_LEN {
clear[off] = TS_SYNC;
off += BD_SOURCE_PACKET_BYTES;
}
assert_eq!(ts_sync_count(&clear), 32);
assert!(
!ts_sync_destroyed(&clear),
"fully-clear unit → not scrambled"
);
let scrambled = vec![0u8; ALIGNED_UNIT_LEN];
assert_eq!(ts_sync_count(&scrambled), 0);
assert!(ts_sync_destroyed(&scrambled), "no syncs → scrambled");
}
#[test]
fn test_aes_cbc_roundtrip() {
let key = [
0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
0xFF, 0x00,
];
let original = vec![0x42u8; 128]; let mut data = original.clone();
fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) {
let cipher = Aes128::new(GenericArray::from_slice(key));
let mut prev = super::AACS_IV;
let num_blocks = data.len() / 16;
for i in 0..num_blocks {
let offset = i * 16;
for j in 0..16 {
data[offset + j] ^= prev[j];
}
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
cipher.encrypt_block(&mut block);
data[offset..offset + 16].copy_from_slice(&block);
prev.copy_from_slice(&data[offset..offset + 16]);
}
}
aes_cbc_encrypt(&key, &mut data);
assert_ne!(data, original);
super::aes_cbc_decrypt(&key, &mut data);
assert_eq!(data, original); }
#[test]
fn test_decrypt_unit_synthetic() {
let unit_key = [0xAAu8; 16];
let mut plain = vec![0u8; ALIGNED_UNIT_LEN];
let mut offset = 4;
while offset < ALIGNED_UNIT_LEN {
plain[offset] = TS_SYNC;
offset += BD_SOURCE_PACKET_BYTES;
}
plain[0] |= 0xC0;
let header: [u8; 16] = plain[..16].try_into().unwrap();
let derived = aes_ecb_encrypt(&unit_key, &header);
let mut encrypt_key = [0u8; 16];
for i in 0..16 {
encrypt_key[i] = derived[i] ^ header[i];
}
let cipher = Aes128::new(GenericArray::from_slice(&encrypt_key));
let mut prev = AACS_IV;
let num_blocks = (ALIGNED_UNIT_LEN - 16) / 16;
for i in 0..num_blocks {
let off = 16 + i * 16;
for j in 0..16 {
plain[off + j] ^= prev[j];
}
let mut block = GenericArray::clone_from_slice(&plain[off..off + 16]);
cipher.encrypt_block(&mut block);
plain[off..off + 16].copy_from_slice(&block);
prev.copy_from_slice(&plain[off..off + 16]);
}
let mut unit = plain;
assert!(ts_sync_destroyed(&unit));
assert!(decrypt_unit(&mut unit, &unit_key));
assert!(!ts_sync_destroyed(&unit));
let mut count = 0;
let mut off = 4;
while off < ALIGNED_UNIT_LEN {
if unit[off] == TS_SYNC {
count += 1;
}
off += BD_SOURCE_PACKET_BYTES;
}
assert_eq!(count, ts_packet_total(&unit));
}
fn aacs_encrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) {
unit[0] |= 0xC0;
let header: [u8; 16] = unit[..16].try_into().unwrap();
let derived = 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 = AACS_IV;
let num_blocks = (ALIGNED_UNIT_LEN - 16) / 16;
for i in 0..num_blocks {
let off = 16 + i * 16;
for j in 0..16 {
unit[off + j] ^= prev[j];
}
let mut block = GenericArray::clone_from_slice(&unit[off..off + 16]);
cipher.encrypt_block(&mut block);
unit[off..off + 16].copy_from_slice(&block);
prev.copy_from_slice(&unit[off..off + 16]);
}
}
fn clear_unit() -> Vec<u8> {
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
let mut off = 4;
while off < ALIGNED_UNIT_LEN {
unit[off] = TS_SYNC;
off += BD_SOURCE_PACKET_BYTES;
}
unit
}
#[test]
fn aes_ecb_matches_fips197_known_answer() {
let key = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F,
];
let pt = [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
0xEE, 0xFF,
];
let expected = [
0x69, 0xC4, 0xE0, 0xD8, 0x6A, 0x7B, 0x04, 0x30, 0xD8, 0xCD, 0xB7, 0x80, 0x70, 0xB4,
0xC5, 0x5A,
];
assert_eq!(aes_ecb_encrypt(&key, &pt), expected);
assert_eq!(aes_ecb_decrypt(&key, &expected), pt);
}
#[test]
fn cbc_decrypt_first_block_xors_aacs_iv() {
let key = [0x24u8; 16];
let plain = [0x5Au8; 16];
let mut x = plain;
for j in 0..16 {
x[j] ^= AACS_IV[j];
}
let ct = aes_ecb_encrypt(&key, &x);
let mut buf = ct;
aes_cbc_decrypt(&key, &mut buf);
assert_eq!(buf, plain, "block-0 CBC must XOR the fixed AACS IV");
}
#[test]
fn aes_cbc_decrypt_matches_nist_sp800_38a_f2_2() {
let key = [
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF,
0x4F, 0x3C,
];
let nist_iv = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F,
];
let ciphertext: [u8; 64] = [
0x76, 0x49, 0xAB, 0xAC, 0x81, 0x19, 0xB2, 0x46, 0xCE, 0xE9, 0x8E, 0x9B, 0x12, 0xE9,
0x19, 0x7D, 0x50, 0x86, 0xCB, 0x9B, 0x50, 0x72, 0x19, 0xEE, 0x95, 0xDB, 0x11, 0x3A,
0x91, 0x76, 0x78, 0xB2, 0x73, 0xBE, 0xD6, 0xB8, 0xE3, 0xC1, 0x74, 0x3B, 0x71, 0x16,
0xE6, 0x9E, 0x22, 0x22, 0x95, 0x16, 0x3F, 0xF1, 0xCA, 0xA1, 0x68, 0x1F, 0xAC, 0x09,
0x12, 0x0E, 0xCA, 0x30, 0x75, 0x86, 0xE1, 0xA7,
];
let nist_plaintext: [u8; 64] = [
0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93,
0x17, 0x2A, 0xAE, 0x2D, 0x8A, 0x57, 0x1E, 0x03, 0xAC, 0x9C, 0x9E, 0xB7, 0x6F, 0xAC,
0x45, 0xAF, 0x8E, 0x51, 0x30, 0xC8, 0x1C, 0x46, 0xA3, 0x5C, 0xE4, 0x11, 0xE5, 0xFB,
0xC1, 0x19, 0x1A, 0x0A, 0x52, 0xEF, 0xF6, 0x9F, 0x24, 0x45, 0xDF, 0x4F, 0x9B, 0x17,
0xAD, 0x2B, 0x41, 0x7B, 0xE6, 0x6C, 0x37, 0x10,
];
let mut buf = ciphertext;
aes_cbc_decrypt(&key, &mut buf);
assert_eq!(
&buf[16..64],
&nist_plaintext[16..64],
"CBC chaining (blocks 1..3) must match NIST SP 800-38A F.2.2 plaintext"
);
let mut expected_block0 = [0u8; 16];
for i in 0..16 {
expected_block0[i] = nist_plaintext[i] ^ nist_iv[i] ^ AACS_IV[i];
}
assert_eq!(
&buf[0..16],
&expected_block0,
"block-0 plaintext must equal NIST PT XOR NIST IV XOR AACS_IV (fixed-IV path)"
);
}
#[test]
fn decrypt_unit_roundtrip_restores_all_syncs() {
let unit_key = [0x37u8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &unit_key);
assert!(
ts_sync_destroyed(&unit),
"encrypted unit must look scrambled"
);
assert!(decrypt_unit(&mut unit, &unit_key));
assert_eq!(ts_sync_count(&unit), ts_packet_total(&unit));
assert!(!ts_sync_destroyed(&unit));
}
#[test]
fn decrypt_unit_wrong_key_fails_and_does_not_falsely_clear() {
let good = [0x11u8; 16];
let bad = [0x22u8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &good);
assert!(!decrypt_unit(&mut unit, &bad), "wrong key must not verify");
}
#[test]
fn decrypt_unit_rejects_short_unit() {
let mut short = vec![0u8; ALIGNED_UNIT_LEN - 1];
assert!(!decrypt_unit(&mut short, &[0u8; 16]));
}
#[test]
fn decrypt_unit_only_touches_bytes_16_onward() {
let unit_key = [0x9Au8; 16];
let mut clear = clear_unit();
clear[..16].copy_from_slice(&[
0xE0, 0xA1, 0xA2, 0xA3, 0x47, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
0xAE, 0xAF,
]);
let header_before: [u8; 16] = clear[..16].try_into().unwrap();
let mut unit = clear;
aacs_encrypt_unit(&mut unit, &unit_key);
assert_eq!(&unit[..16], &header_before);
decrypt_unit(&mut unit, &unit_key);
assert_eq!(
&unit[..16],
&header_before,
"header bytes must be preserved"
);
}
#[test]
fn try_keys_reports_already_clear_without_consuming_a_key() {
let mut unit = clear_unit();
assert_eq!(
decrypt_unit_try_keys(&mut unit, &[]),
Some(UnitKeyResult::AlreadyClear)
);
}
#[test]
fn try_keys_reports_correct_index_among_several() {
let real = [0x44u8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &real);
let keys = [[0x01u8; 16], [0x02u8; 16], real];
assert_eq!(
decrypt_unit_try_keys(&mut unit, &keys),
Some(UnitKeyResult::DecryptedWith(2))
);
assert!(
!ts_sync_destroyed(&unit),
"unit must be clear after the hit"
);
}
#[test]
fn try_keys_restores_original_bytes_on_total_failure() {
let real = [0x55u8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &real);
let snapshot = unit.clone();
let wrong = [[0xAAu8; 16], [0xBBu8; 16]];
assert_eq!(decrypt_unit_try_keys(&mut unit, &wrong), None);
assert_eq!(unit, snapshot, "failed try must restore the original bytes");
}
#[test]
fn cpi_gate_clear_flag_passes_through_even_when_body_looks_scrambled() {
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
for (i, b) in unit.iter_mut().enumerate().skip(16) {
*b = (i as u8).wrapping_mul(31) | 1; }
assert!(ts_sync_destroyed(&unit), "body has no syncs");
assert!(!aacs_unit_encrypted(&unit), "CPI clear");
assert!(
!aacs_unit_needs_decrypt(&unit),
"CPI-clear ⇒ no decrypt attempt"
);
let snapshot = unit.clone();
assert!(decrypt_unit(&mut unit, &[0xABu8; 16]));
assert_eq!(unit, snapshot, "CPI-clear unit must be left byte-identical");
assert_eq!(
decrypt_unit_try_keys(&mut unit, &[[0xABu8; 16]]),
Some(UnitKeyResult::AlreadyClear),
"CPI-clear unit consumes no key"
);
}
#[test]
fn cpi_gate_set_flag_decrypts_and_needs_decrypt_is_idempotent() {
let key = [0x5au8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &key); assert!(aacs_unit_encrypted(&unit), "CPI set");
assert!(aacs_unit_needs_decrypt(&unit), "flagged + still scrambled");
assert!(decrypt_unit(&mut unit, &key), "right key decrypts");
assert!(
aacs_unit_encrypted(&unit),
"CPI bits live in the preserved header ⇒ still set post-decrypt"
);
assert!(
!aacs_unit_needs_decrypt(&unit),
"syncs restored ⇒ no further decrypt attempt (idempotent re-decrypt)"
);
}
#[test]
fn unit_key_validates_agrees_with_decrypt_unit() {
let good = [0x6Au8; 16];
let bad = [0x6Bu8; 16];
let mut enc = clear_unit();
aacs_encrypt_unit(&mut enc, &good);
assert!(unit_key_validates(&enc, &good));
let mut probe = enc.clone();
assert!(decrypt_unit(&mut probe, &good));
assert!(!unit_key_validates(&enc, &bad));
let mut probe2 = enc.clone();
assert!(!decrypt_unit(&mut probe2, &bad));
}
#[test]
fn unit_is_clean_ts_is_strict_all_32_syncs() {
let clear = clear_unit();
assert!(unit_is_clean_ts(&clear), "all-32-sync unit is clean");
let mut one_missing = clear_unit();
one_missing[17 * BD_SOURCE_PACKET_BYTES + 4] = 0x00;
assert!(
ts_syncs_intact(&one_missing),
"majority heuristic still passes one missing sync (the hole)"
);
assert!(
!unit_is_clean_ts(&one_missing),
"strict gate rejects even one missing sync"
);
let key = [0x33u8; 16];
let mut enc = clear_unit();
aacs_encrypt_unit(&mut enc, &key);
let mut good = enc.clone();
decrypt_unit(&mut good, &key);
assert!(unit_is_clean_ts(&good), "right-key decrypt yields clean TS");
let mut wrong = enc.clone();
decrypt_unit(&mut wrong, &[0x34u8; 16]);
assert!(
!unit_is_clean_ts(&wrong),
"wrong-key decrypt must fail the strict gate"
);
assert!(!unit_is_clean_ts(&clear[..ALIGNED_UNIT_LEN - 1]));
}
#[test]
fn unit_key_validates_is_non_mutating() {
let good = [0x7Cu8; 16];
let mut enc = clear_unit();
aacs_encrypt_unit(&mut enc, &good);
let snapshot = enc.clone();
let _ = unit_key_validates(&enc, &good);
assert_eq!(enc, snapshot, "unit_key_validates must not mutate input");
}
#[test]
fn unit_key_validates_rejects_short_unit() {
let short = vec![0u8; ALIGNED_UNIT_LEN - 16];
assert!(!unit_key_validates(&short, &[0u8; 16]));
}
#[test]
fn decrypt_bus_roundtrips_per_sector_skipping_first_16_bytes() {
let rdk = [0x13u8; 16];
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
for (i, b) in unit.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
let plain = unit.clone();
let cipher = Aes128::new(GenericArray::from_slice(&rdk));
for s in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_BYTES) {
let mut prev = AACS_IV;
let body = s + 16;
let end = s + SECTOR_BYTES;
let nblocks = (end - body) / 16;
for i in 0..nblocks {
let off = body + i * 16;
for j in 0..16 {
unit[off + j] ^= prev[j];
}
let mut blk = GenericArray::clone_from_slice(&unit[off..off + 16]);
cipher.encrypt_block(&mut blk);
unit[off..off + 16].copy_from_slice(&blk);
prev.copy_from_slice(&unit[off..off + 16]);
}
}
assert_ne!(unit, plain, "forward bus-encrypt must change the body");
decrypt_bus(&mut unit, &rdk);
assert_eq!(
unit, plain,
"decrypt_bus must invert per-sector bus encrypt"
);
for s in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_BYTES) {
assert_eq!(&unit[s..s + 16], &plain[s..s + 16]);
}
}
#[test]
fn decrypt_unit_full_passthrough_when_already_clear() {
let mut unit = clear_unit();
let snapshot = unit.clone();
assert!(decrypt_unit_full(
&mut unit,
&[0u8; 16],
Some(&[0xFFu8; 16])
));
assert_eq!(unit, snapshot, "clear unit must pass through untouched");
}
#[test]
fn decrypt_unit_full_applies_bus_then_aacs() {
let unit_key = [0x21u8; 16];
let rdk = [0x84u8; 16];
let mut unit = clear_unit();
aacs_encrypt_unit(&mut unit, &unit_key);
let cipher = Aes128::new(GenericArray::from_slice(&rdk));
for s in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_BYTES) {
let mut prev = AACS_IV;
for i in 0..((SECTOR_BYTES - 16) / 16) {
let off = s + 16 + i * 16;
for j in 0..16 {
unit[off + j] ^= prev[j];
}
let mut blk = GenericArray::clone_from_slice(&unit[off..off + 16]);
cipher.encrypt_block(&mut blk);
unit[off..off + 16].copy_from_slice(&blk);
prev.copy_from_slice(&unit[off..off + 16]);
}
}
assert!(ts_sync_destroyed(&unit));
assert!(decrypt_unit_full(&mut unit, &unit_key, Some(&rdk)));
assert_eq!(ts_sync_count(&unit), ts_packet_total(&unit));
}
#[test]
fn ts_sync_destroyed_false_for_sub_unit_length() {
assert!(!ts_sync_destroyed(&[]));
assert!(!ts_sync_destroyed(&vec![0u8; ALIGNED_UNIT_LEN - 1]));
let mut almost = vec![0u8; ALIGNED_UNIT_LEN - 1];
almost[4] = 0x00; assert!(!ts_sync_destroyed(&almost));
}
#[test]
fn ts_sync_count_only_samples_the_192_byte_stride() {
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
unit[5] = TS_SYNC; unit[197] = TS_SYNC; assert_eq!(ts_sync_count(&unit), 0, "off-stride 0x47 must not count");
unit[4] = TS_SYNC; assert_eq!(ts_sync_count(&unit), 1);
}
#[test]
fn ts_packet_total_for_various_lengths() {
assert_eq!(ts_packet_total(&[0u8; 192]), 1);
assert_eq!(ts_packet_total(&[0u8; 384]), 2);
assert_eq!(ts_packet_total(&[0u8; 191]), 0);
assert_eq!(ts_packet_total(&[0u8; ALIGNED_UNIT_LEN]), 32);
}
}