use libfreemkv::aacs;
use libfreemkv::css;
#[test]
fn css_descramble_sector_roundtrip_via_public_api() {
let state = css::CssState {
title_key: [0x42, 0x13, 0x37, 0xBE, 0xEF],
};
let mut sector = vec![0x00u8; 2048];
sector[0x14] = 0x30; sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]); sector[0x80] = 0x00;
sector[0x81] = 0x00;
sector[0x82] = 0x01;
sector[0x83] = 0xE0;
for (i, byte) in sector.iter_mut().enumerate().take(2048).skip(0x84) {
*byte = (i & 0xFF) as u8;
}
let original = sector.clone();
css::descramble_sector(&state, &mut sector);
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
assert_ne!(
§or[0x80..0x84],
&original[0x80..0x84],
"content unchanged"
);
sector[0x14] = 0x30;
css::descramble_sector(&state, &mut sector);
assert_eq!(
§or[0x80..2048],
&original[0x80..2048],
"double descramble did not roundtrip"
);
}
#[test]
fn css_is_scrambled_detection() {
let mut sector = vec![0u8; 2048];
assert!(
!css::is_scrambled(§or),
"empty sector should not be scrambled"
);
sector[0x14] = 0x10; assert!(css::is_scrambled(§or), "bit 4 set should be detected");
sector[0x14] = 0x20; assert!(css::is_scrambled(§or), "bit 5 set should be detected");
sector[0x14] = 0x30; assert!(
css::is_scrambled(§or),
"both bits set should be detected"
);
sector[0x14] = 0xCF; assert!(
!css::is_scrambled(§or),
"bits 4-5 clear should not be scrambled"
);
}
#[test]
fn aacs_decrypt_unit_roundtrip() {
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
let unit_key = [0xAAu8; 16];
let aacs_iv: [u8; 16] = [
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F,
0x78,
];
let mut plain = vec![0u8; aacs::ALIGNED_UNIT_LEN];
let mut offset = 4;
while offset < aacs::ALIGNED_UNIT_LEN {
plain[offset] = 0x47; offset += 192;
}
plain[0] |= 0xC0;
let expected = plain.clone();
let header: [u8; 16] = plain[..16].try_into().unwrap();
let cipher_header = Aes128::new(GenericArray::from_slice(&unit_key));
let mut block = GenericArray::clone_from_slice(&header);
cipher_header.encrypt_block(&mut block);
let mut derived = [0u8; 16];
derived.copy_from_slice(&block);
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 = (aacs::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 blk = GenericArray::clone_from_slice(&plain[off..off + 16]);
cipher.encrypt_block(&mut blk);
plain[off..off + 16].copy_from_slice(&blk);
prev.copy_from_slice(&plain[off..off + 16]);
}
assert!(aacs::is_unit_encrypted(&plain));
let result = aacs::decrypt_unit(&mut plain, &unit_key);
assert!(
result,
"decrypt_unit should return true on valid encrypted unit"
);
assert!(
!aacs::is_unit_encrypted(&plain),
"encryption flag should be cleared"
);
let mut sync_count = 0;
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
if plain[off] == 0x47 {
sync_count += 1;
}
off += 192;
}
let expected_syncs = (aacs::ALIGNED_UNIT_LEN - 4) / 192 + 1;
assert_eq!(
sync_count, expected_syncs,
"TS sync bytes not recovered: got {}, expected {}",
sync_count, expected_syncs
);
assert_eq!(
&plain[1..aacs::ALIGNED_UNIT_LEN],
&expected[1..aacs::ALIGNED_UNIT_LEN],
"decrypted unit body does not match original"
);
assert_eq!(
plain[0] & !0xC0,
expected[0] & !0xC0,
"byte 0 mismatch ignoring flag"
);
}
#[test]
fn aacs_disc_hash_deterministic() {
let data1 = b"Unit_Key_RO.inf test data for deterministic hashing";
let data2 = b"Different data should produce different hash";
let hash1a = aacs::disc_hash(data1);
let hash1b = aacs::disc_hash(data1);
assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");
let hash2 = aacs::disc_hash(data2);
assert_ne!(
hash1a, hash2,
"different inputs should produce different hashes"
);
assert_eq!(hash1a.len(), 20);
let hex = aacs::disc_hash_hex(&hash1a);
assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
assert_eq!(
hex.len(),
42,
"hex string should be 42 chars (0x + 40 hex digits)"
);
}
#[test]
fn aacs_decrypt_unit_key_roundtrip() {
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
let vuk = [
0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52, 0xEA,
0xEB,
];
let original_unit_key = [
0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0, 0x47,
0x0F,
];
let cipher = Aes128::new(GenericArray::from_slice(&vuk));
let mut block = GenericArray::clone_from_slice(&original_unit_key);
cipher.encrypt_block(&mut block);
let mut encrypted_uk = [0u8; 16];
encrypted_uk.copy_from_slice(&block);
let decrypted = aacs::decrypt_unit_key(&vuk, &encrypted_uk);
assert_eq!(
decrypted, original_unit_key,
"decrypt_unit_key did not recover original unit key"
);
}
#[test]
fn aacs_vuk_derivation_roundtrip() {
let media_key = [
0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E, 0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16,
0x40,
];
let volume_id = [
0xA1u8, 0x3C, 0xBE, 0x2C, 0xE4, 0x05, 0x65, 0xD1, 0x04, 0xB5, 0x3E, 0x76, 0x8C, 0x70, 0x0E,
0x30,
];
let vuk = aacs::derive_vuk(&media_key, &volume_id);
assert_ne!(vuk, [0u8; 16], "VUK should not be all zeros");
assert_ne!(vuk, media_key, "VUK should differ from media_key");
assert_ne!(vuk, volume_id, "VUK should differ from volume_id");
let vuk2 = aacs::derive_vuk(&media_key, &volume_id);
assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
}
#[test]
fn aacs_is_unit_encrypted_detection() {
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
assert!(
!aacs::is_unit_encrypted(&unit),
"zero unit should not be encrypted"
);
unit[0] = 0x40; assert!(aacs::is_unit_encrypted(&unit));
unit[0] = 0x80; assert!(aacs::is_unit_encrypted(&unit));
unit[0] = 0xC0; assert!(aacs::is_unit_encrypted(&unit));
unit[0] = 0x3F; assert!(!aacs::is_unit_encrypted(&unit));
let short = vec![0xC0u8; 100];
assert!(
!aacs::is_unit_encrypted(&short),
"short buffer should not be detected"
);
}
#[test]
fn aacs_decrypt_unit_unencrypted_passthrough() {
let mut unit = vec![0x42u8; aacs::ALIGNED_UNIT_LEN];
unit[0] = 0x00; let original = unit.clone();
let key = [0xAA; 16];
let result = aacs::decrypt_unit(&mut unit, &key);
assert!(result, "unencrypted unit should return true");
assert_eq!(unit, original, "unencrypted unit should be unchanged");
}
fn ref_aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
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
}
fn ref_aes_cbc_encrypt(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
let cipher = Aes128::new(GenericArray::from_slice(key));
let mut prev = *iv;
let num_blocks = data.len() / 16;
for i in 0..num_blocks {
let off = i * 16;
for j in 0..16 {
data[off + j] ^= prev[j];
}
let mut block = GenericArray::clone_from_slice(&data[off..off + 16]);
cipher.encrypt_block(&mut block);
data[off..off + 16].copy_from_slice(&block);
prev.copy_from_slice(&data[off..off + 16]);
}
}
const CROSS_AACS_IV: [u8; 16] = [
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
];
#[test]
fn aacs_cross_validation_encrypt_then_decrypt() {
let unit_key: [u8; 16] = [
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32,
0x10,
];
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
plaintext[off] = 0x47;
off += 192;
}
for i in 16..aacs::ALIGNED_UNIT_LEN {
if plaintext[i] == 0 {
plaintext[i] = (i % 251) as u8;
}
}
plaintext[0] = 0xC0;
let expected = plaintext.clone();
let mut header = [0u8; 16];
header.copy_from_slice(&plaintext[..16]);
let derived = ref_aes_ecb_encrypt(&unit_key, &header);
let mut dk = [0u8; 16];
for i in 0..16 {
dk[i] = derived[i] ^ header[i];
}
ref_aes_cbc_encrypt(
&dk,
&CROSS_AACS_IV,
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
);
assert_ne!(
&plaintext[16..32],
&expected[16..32],
"encryption did not change ciphertext region"
);
let ok = aacs::decrypt_unit(&mut plaintext, &unit_key);
assert!(
ok,
"decrypt_unit returned false (TS sync verification failed)"
);
assert_eq!(plaintext[0] & 0xC0, 0x00, "encryption flag not cleared");
let mut expected_cleared = expected.clone();
expected_cleared[0] &= !0xC0;
assert_eq!(
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
"decrypted unit does not match original plaintext"
);
}
#[test]
fn aacs_cross_validation_alternate_key() {
let unit_key: [u8; 16] = [
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08,
];
let mut plaintext = vec![0xFFu8; aacs::ALIGNED_UNIT_LEN];
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
plaintext[off] = 0x47;
off += 192;
}
plaintext[0] = 0xC0;
let expected = plaintext.clone();
let mut header = [0u8; 16];
header.copy_from_slice(&plaintext[..16]);
let derived = ref_aes_ecb_encrypt(&unit_key, &header);
let mut dk = [0u8; 16];
for i in 0..16 {
dk[i] = derived[i] ^ header[i];
}
ref_aes_cbc_encrypt(
&dk,
&CROSS_AACS_IV,
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
);
assert!(aacs::decrypt_unit(&mut plaintext, &unit_key));
let mut expected_cleared = expected;
expected_cleared[0] &= !0xC0;
assert_eq!(
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
);
}
#[test]
fn aacs_bus_decrypt_cross_validation() {
let read_data_key: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x00,
];
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
for i in 0..aacs::ALIGNED_UNIT_LEN {
plaintext[i] = ((i * 3 + 17) & 0xFF) as u8;
}
let expected = plaintext.clone();
for sector_start in (0..aacs::ALIGNED_UNIT_LEN).step_by(2048) {
ref_aes_cbc_encrypt(
&read_data_key,
&CROSS_AACS_IV,
&mut plaintext[sector_start + 16..sector_start + 2048],
);
}
assert_ne!(&plaintext[16..32], &expected[16..32]);
aacs::decrypt_bus(&mut plaintext, &read_data_key);
assert_eq!(
plaintext, expected,
"bus decrypt did not recover original plaintext"
);
}
#[test]
fn css_roundtrip_with_snapshot() {
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let seed: [u8; 5] = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
let mut sector = vec![0x00u8; 2048];
sector[0] = 0x00;
sector[1] = 0x00;
sector[2] = 0x01;
sector[3] = 0xBA;
sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(&seed);
sector[0x80] = 0x00;
sector[0x81] = 0x00;
sector[0x82] = 0x01;
sector[0x83] = 0xE0;
sector[0x84] = 0x07;
sector[0x85] = 0xEC;
sector[0x86] = 0x80;
sector[0x87] = 0x80;
sector[0x88] = 0x05;
sector[0x89] = 0x21;
for i in 0x8A..2048 {
sector[i] = ((i * 7 + 3) & 0xFF) as u8;
}
let original = sector.clone();
css::lfsr::descramble_sector(&title_key, &mut sector);
let snapshot: Vec<u8> = sector[0x80..0xA0].to_vec();
assert_eq!(snapshot.len(), 32);
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
assert_ne!(§or[0x80..0xA0], &original[0x80..0xA0]);
sector[0x14] = 0x30;
css::lfsr::descramble_sector(&title_key, &mut sector);
assert_eq!(
§or[0x80..2048],
&original[0x80..2048],
"CSS roundtrip failed"
);
}
#[test]
fn css_roundtrip_multiple_keys() {
let cases: &[([u8; 5], [u8; 5])] = &[
(
[0x00, 0x00, 0x00, 0x00, 0x00],
[0x00, 0x00, 0x00, 0x00, 0x00],
),
(
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
),
(
[0x01, 0x02, 0x03, 0x04, 0x05],
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
),
(
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
[0x12, 0x34, 0x56, 0x78, 0x9A],
),
];
for (idx, (key, seed)) in cases.iter().enumerate() {
let mut sector = vec![0x00u8; 2048];
sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(seed);
for i in 0x80..2048 {
sector[i] = ((i + idx) & 0xFF) as u8;
}
let original = sector.clone();
css::lfsr::descramble_sector(key, &mut sector);
assert_eq!(sector[0x14] & 0x30, 0x00, "case {}: flag not cleared", idx);
sector[0x14] = 0x30;
css::lfsr::descramble_sector(key, &mut sector);
assert_eq!(
§or[0x80..2048],
&original[0x80..2048],
"case {}: roundtrip failed",
idx
);
}
}
#[test]
fn css_stevenson_attack_validates_cracked_key() {
let candidates: &[([u8; 5], [u8; 5])] = &[
(
[0x42, 0x13, 0x37, 0xBE, 0xEF],
[0x11, 0x22, 0x33, 0x44, 0x55],
),
(
[0x01, 0x02, 0x03, 0x04, 0x05],
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
),
(
[0x10, 0x20, 0x30, 0x40, 0x50],
[0x05, 0x06, 0x07, 0x08, 0x09],
),
(
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
[0x12, 0x34, 0x56, 0x78, 0x9A],
),
(
[0x55, 0xAA, 0x55, 0xAA, 0x55],
[0x00, 0x00, 0x00, 0x00, 0x00],
),
];
let mut any_cracked = false;
for (key, seed) in candidates {
let mut sector = vec![0x00u8; 2048];
sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(seed);
sector[0x80] = 0x00;
sector[0x81] = 0x00;
sector[0x82] = 0x01;
sector[0x83] = 0xE0;
sector[0x84] = 0x00;
sector[0x85] = 0x00;
sector[0x86] = 0x80;
sector[0x87] = 0x80;
sector[0x88] = 0x05;
sector[0x89] = 0x21;
let original = sector.clone();
css::lfsr::descramble_sector(key, &mut sector);
sector[0x14] = 0x30;
let cracked = css::crack::crack_title_key(§or);
if let Some(cracked_key) = cracked {
let mut test = sector.clone();
css::lfsr::descramble_sector(&cracked_key, &mut test);
assert_eq!(test[0x80], 0x00, "PES byte 0 mismatch");
assert_eq!(test[0x81], 0x00, "PES byte 1 mismatch");
assert_eq!(test[0x82], 0x01, "PES byte 2 mismatch");
assert_eq!(test[0x83], 0xE0, "PES byte 3 mismatch");
assert_eq!(
&test[0x80..2048],
&original[0x80..2048],
"cracked key did not recover original plaintext"
);
any_cracked = true;
eprintln!(
"Stevenson attack succeeded: key={:02X?} seed={:02X?} cracked={:02X?}",
key, seed, cracked_key
);
}
}
if !any_cracked {
eprintln!(
"Stevenson attack did not converge on any synthetic key/seed pair. \
This is expected: synthetic sectors lack the TAB1 output encoding \
present in real CSS-encrypted DVD sectors."
);
}
}
#[test]
fn css_recover_title_key_with_exact_plaintext() {
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let seed: [u8; 5] = [0x11, 0x22, 0x33, 0x44, 0x55];
let mut sector = vec![0x00u8; 2048];
sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(&seed);
let pes_header: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
sector[0x80..0x8A].copy_from_slice(&pes_header);
for i in 0x8A..2048 {
sector[i] = ((i * 13 + 7) & 0xFF) as u8;
}
let original = sector.clone();
css::lfsr::descramble_sector(&title_key, &mut sector);
sector[0x14] = 0x30;
let recovered = css::crack::recover_title_key(§or, &pes_header);
if let Some(rkey) = recovered {
let mut test = sector.clone();
css::lfsr::descramble_sector(&rkey, &mut test);
assert_eq!(
&test[0x80..2048],
&original[0x80..2048],
"recovered key did not produce correct plaintext"
);
eprintln!("recover_title_key succeeded: {:02X?}", rkey);
} else {
eprintln!(
"recover_title_key returned None for key={:02X?} seed={:02X?}. \
The LFSR0 recovery phase may not converge for this combination.",
title_key, seed
);
}
}
#[test]
fn aacs_parse_unit_key_ro_minimal() {
let uk_pos: u32 = 100;
let mut data = vec![0u8; 200];
data[0..4].copy_from_slice(&uk_pos.to_be_bytes());
data[16] = 1; data[17] = 1;
data[18] = 0;
let pos = uk_pos as usize;
data[pos] = 0;
data[pos + 1] = 1;
let key_pos = pos + 48;
for i in 0..16 {
data[key_pos + i] = (0xA0 + i) as u8;
}
let result = aacs::parse_unit_key_ro(&data, false);
assert!(
result.is_some(),
"parse_unit_key_ro should succeed on valid data"
);
let ukf = result.unwrap();
assert_eq!(ukf.app_type, 1);
assert_eq!(ukf.num_bdmv_dir, 1);
assert_eq!(ukf.encrypted_keys.len(), 1);
assert_eq!(ukf.disc_hash.len(), 20);
let hash = aacs::disc_hash(&data);
assert_eq!(ukf.disc_hash, hash);
}