Add crypto roundtrip tests: CSS + AACS validation
- CSS: decrypt_key determinism, descramble XOR roundtrip, TAB1 permutation, TAB4 bit-reversal involution, Stevenson attack on scrambled sector - AACS: decrypt_unit roundtrip, disc hash deterministic, VUK derivation, unit key parsing, EC point-on-curve and ECDSA already covered - 239 tests total
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
//! Comprehensive roundtrip tests for CSS and AACS cryptographic implementations.
|
||||
//!
|
||||
//! These tests prove the cryptographic algorithms work end-to-end.
|
||||
//! Tests that require access to private internals are placed as unit tests
|
||||
//! inside the respective source files (css/lfsr.rs, css/crack.rs, aacs/handshake.rs).
|
||||
//!
|
||||
//! This file tests public API items accessible from integration tests.
|
||||
|
||||
use libfreemkv::aacs;
|
||||
use libfreemkv::css;
|
||||
|
||||
// ── CSS Public API Tests ────────────────────────────────────────────────────
|
||||
|
||||
/// Test: css_descramble_sector_roundtrip_via_public_api
|
||||
///
|
||||
/// The public css::descramble_sector() wraps the LFSR descrambler.
|
||||
/// Since the cipher is XOR-based, calling descramble twice (with restored
|
||||
/// flags) should roundtrip the data.
|
||||
#[test]
|
||||
fn css_descramble_sector_roundtrip_via_public_api() {
|
||||
let state = css::CssState {
|
||||
title_key: [0x42, 0x13, 0x37, 0xBE, 0xEF],
|
||||
};
|
||||
|
||||
// Build a sector with scramble flag set
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
sector[0x14] = 0x30; // scramble flag
|
||||
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]); // seed
|
||||
// PES header at byte 128
|
||||
sector[0x80] = 0x00;
|
||||
sector[0x81] = 0x00;
|
||||
sector[0x82] = 0x01;
|
||||
sector[0x83] = 0xE0;
|
||||
// Fill content
|
||||
for i in 0x84..2048 {
|
||||
sector[i] = (i & 0xFF) as u8;
|
||||
}
|
||||
let original = sector.clone();
|
||||
|
||||
// First descramble
|
||||
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");
|
||||
|
||||
// Restore flag for second pass
|
||||
sector[0x14] = 0x30;
|
||||
|
||||
// Second descramble = roundtrip
|
||||
css::descramble_sector(&state, &mut sector);
|
||||
assert_eq!(
|
||||
§or[0x80..2048],
|
||||
&original[0x80..2048],
|
||||
"double descramble did not roundtrip"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test: css_is_scrambled detects scramble flags correctly.
|
||||
#[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; // bit 4 set
|
||||
assert!(css::is_scrambled(§or), "bit 4 set should be detected");
|
||||
|
||||
sector[0x14] = 0x20; // bit 5 set
|
||||
assert!(css::is_scrambled(§or), "bit 5 set should be detected");
|
||||
|
||||
sector[0x14] = 0x30; // both bits set
|
||||
assert!(css::is_scrambled(§or), "both bits set should be detected");
|
||||
|
||||
sector[0x14] = 0xCF; // bits 4-5 clear, other bits set
|
||||
assert!(!css::is_scrambled(§or), "bits 4-5 clear should not be scrambled");
|
||||
}
|
||||
|
||||
// ── AACS Public API Tests ───────────────────────────────────────────────────
|
||||
|
||||
/// Test 6: aacs_decrypt_unit_roundtrip
|
||||
///
|
||||
/// Build a synthetic 6144-byte aligned unit with TS sync bytes, encrypt it
|
||||
/// using the AACS algorithm (AES-ECB header derivation + AES-CBC body),
|
||||
/// then decrypt with decrypt_unit() and verify the plaintext matches.
|
||||
#[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,
|
||||
];
|
||||
|
||||
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
||||
let mut plain = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||
let mut offset = 4;
|
||||
while offset < aacs::ALIGNED_UNIT_LEN {
|
||||
plain[offset] = 0x47; // TS sync byte
|
||||
offset += 192;
|
||||
}
|
||||
// Set encryption flag (bits 6-7 of byte 0)
|
||||
plain[0] |= 0xC0;
|
||||
|
||||
// Save original plaintext for comparison
|
||||
let expected = plain.clone();
|
||||
|
||||
// Encrypt: replicate the AACS encryption algorithm (reverse of decrypt_unit)
|
||||
let header: [u8; 16] = plain[..16].try_into().unwrap();
|
||||
|
||||
// Step 1: AES-ECB encrypt header with unit key
|
||||
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);
|
||||
|
||||
// Step 2: XOR to get per-unit decryption key
|
||||
let mut encrypt_key = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
encrypt_key[i] = derived[i] ^ header[i];
|
||||
}
|
||||
|
||||
// Step 3: AES-CBC encrypt bytes 16..6144
|
||||
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]);
|
||||
}
|
||||
|
||||
// Verify it looks encrypted
|
||||
assert!(aacs::is_unit_encrypted(&plain));
|
||||
|
||||
// Now decrypt
|
||||
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");
|
||||
|
||||
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
|
||||
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
|
||||
);
|
||||
|
||||
// Compare all bytes except byte 0 (encryption flag cleared)
|
||||
assert_eq!(
|
||||
&plain[1..aacs::ALIGNED_UNIT_LEN],
|
||||
&expected[1..aacs::ALIGNED_UNIT_LEN],
|
||||
"decrypted unit body does not match original"
|
||||
);
|
||||
// Byte 0: original had 0xC0 set, decrypted has it cleared
|
||||
assert_eq!(plain[0] & !0xC0, expected[0] & !0xC0, "byte 0 mismatch ignoring flag");
|
||||
}
|
||||
|
||||
/// Test 7: aacs_disc_hash_deterministic
|
||||
///
|
||||
/// compute disc_hash on the same data twice, verify identical results.
|
||||
#[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");
|
||||
|
||||
// Verify it is a 20-byte SHA-1 hash
|
||||
assert_eq!(hash1a.len(), 20);
|
||||
|
||||
// Verify disc_hash_hex formatting
|
||||
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: aacs_decrypt_unit_key_roundtrip
|
||||
///
|
||||
/// Verify that encrypting a unit key with AES-ECB and decrypting it with
|
||||
/// decrypt_unit_key recovers the original.
|
||||
#[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,
|
||||
];
|
||||
|
||||
// Encrypt: AES-ECB encrypt the unit key with VUK
|
||||
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);
|
||||
|
||||
// Decrypt with the public API
|
||||
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: aacs_vuk_derivation
|
||||
///
|
||||
/// Verify derive_vuk: VUK = AES-ECB-DECRYPT(media_key, volume_id) XOR volume_id
|
||||
#[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);
|
||||
|
||||
// VUK should be non-zero and different from both inputs
|
||||
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");
|
||||
|
||||
// Verify determinism
|
||||
let vuk2 = aacs::derive_vuk(&media_key, &volume_id);
|
||||
assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
|
||||
}
|
||||
|
||||
/// Test: aacs_is_unit_encrypted detects encryption flags correctly.
|
||||
#[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; // bit 6 set
|
||||
assert!(aacs::is_unit_encrypted(&unit));
|
||||
|
||||
unit[0] = 0x80; // bit 7 set
|
||||
assert!(aacs::is_unit_encrypted(&unit));
|
||||
|
||||
unit[0] = 0xC0; // both bits set
|
||||
assert!(aacs::is_unit_encrypted(&unit));
|
||||
|
||||
unit[0] = 0x3F; // bits 6-7 clear
|
||||
assert!(!aacs::is_unit_encrypted(&unit));
|
||||
|
||||
// Too short
|
||||
let short = vec![0xC0u8; 100];
|
||||
assert!(!aacs::is_unit_encrypted(&short), "short buffer should not be detected");
|
||||
}
|
||||
|
||||
/// Test: aacs_decrypt_unit_unencrypted_passthrough
|
||||
///
|
||||
/// A unit without encryption flags should pass through decrypt_unit unchanged.
|
||||
#[test]
|
||||
fn aacs_decrypt_unit_unencrypted_passthrough() {
|
||||
let mut unit = vec![0x42u8; aacs::ALIGNED_UNIT_LEN];
|
||||
unit[0] = 0x00; // no encryption flag
|
||||
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");
|
||||
}
|
||||
|
||||
/// Test: aacs_parse_unit_key_ro with minimal valid data
|
||||
#[test]
|
||||
fn aacs_parse_unit_key_ro_minimal() {
|
||||
// Build a minimal Unit_Key_RO.inf structure
|
||||
// Header: first 4 bytes = BE32 offset to key storage area
|
||||
let uk_pos: u32 = 100;
|
||||
let mut data = vec![0u8; 200];
|
||||
|
||||
// Key storage offset
|
||||
data[0..4].copy_from_slice(&uk_pos.to_be_bytes());
|
||||
// app_type
|
||||
data[16] = 1; // BD-ROM
|
||||
// num_bdmv_dir
|
||||
data[17] = 1;
|
||||
// flags
|
||||
data[18] = 0;
|
||||
|
||||
// At uk_pos: num_unit_keys = 1
|
||||
let pos = uk_pos as usize;
|
||||
data[pos] = 0;
|
||||
data[pos + 1] = 1; // 1 key
|
||||
|
||||
// At uk_pos + 48: first encrypted key (16 bytes)
|
||||
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);
|
||||
|
||||
// disc_hash should be deterministic
|
||||
let hash = aacs::disc_hash(&data);
|
||||
assert_eq!(ukf.disc_hash, hash);
|
||||
}
|
||||
Reference in New Issue
Block a user