//! AACS content decryption — AES primitives, unit decryption, bus encryption. use aes::Aes128; use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray}; // ── AACS constants ────────────────────────────────────────────────────────── /// Fixed IV used by AACS for all AES-CBC operations. pub(crate) const AACS_IV: [u8; 16] = [ 0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78, ]; /// Size of an AACS aligned unit (3 × 2048-byte sectors). pub const ALIGNED_UNIT_LEN: usize = 6144; /// Size of one sector. const SECTOR_LEN: usize = 2048; /// Transport stream packet spacing in Blu-ray m2ts (192 bytes = 4 TP_extra + 188 TS). const TS_PACKET_LEN: usize = 192; /// TS sync byte. const TS_SYNC: u8 = 0x47; // ── AES primitives ────────────────────────────────────────────────────────── /// AES-128-ECB encrypt a single 16-byte block. 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 } /// AES-128-ECB decrypt a single 16-byte block. 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 } /// AES-128-CBC decrypt in-place with the fixed AACS IV. /// /// Precondition: `data.len()` is a multiple of 16. Any trailing partial /// block is silently ignored; all callers pass aligned regions (6128 and /// 2032 bytes), and the assert documents/enforces that contract. 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; // Process blocks in reverse to avoid clobbering ciphertext needed for XOR 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]; } } } // ── Content decryption ────────────────────────────────────────────────────── /// True if a 6144-byte aligned unit is AACS-scrambled on disc. /// /// AACS encrypts the unit body, which destroys the MPEG-TS sync bytes (`0x47`) /// a clear unit carries at offsets 4, 196, 388, … (one per 192-byte source /// packet). So "scrambled" = "the TS syncs are NOT intact". This is /// flag-independent: it does NOT read the TP_extra copy-control bits (byte 0) /// or the TS scrambling-control bits (byte 7) — AACS sets neither reliably /// across discs/players. /// /// This is the single shared definition of "encrypted" for the whole ecosystem /// — libfreemkv's decrypt gate, autorip's sample selection, and the online key /// service's validation gate all call THIS, so they always agree on what is /// encrypted. A correctly-decrypted (or natively-clear) unit reports `false`, /// so the decrypt path never double-decrypts and there is no flag to clear. pub fn is_aacs_scrambled(unit: &[u8]) -> bool { unit.len() >= ALIGNED_UNIT_LEN && !ts_syncs_intact(unit) } /// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride /// (offset 4 and every 192 bytes after — 4-byte TP_extra_header + 188-byte /// TS packet). A clear or correctly-decrypted m2ts unit shows ~one per /// packet; an encrypted unit, or a non-content unit decrypted under a key /// that doesn't apply, shows ~none. 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 += TS_PACKET_LEN; } count } /// Number of BD-TS packets in the unit — the maximum possible sync count. pub fn ts_packet_total(unit: &[u8]) -> usize { // One sync byte per 192-byte BD-TS packet (at offset 4 of each). The old // `(len - 4) / TS_PACKET_LEN + 1` over-counted by one for lengths of the // form `4 + k·192`. unit.len() / TS_PACKET_LEN } fn ts_syncs_intact(unit: &[u8]) -> bool { ts_sync_count(unit) > ts_packet_total(unit) / 2 } /// Verify a decrypted unit looks like clear MPEG-TS (sync bytes intact). fn verify_ts(unit: &[u8]) -> bool { ts_syncs_intact(unit) } /// Decrypt one AACS aligned unit (6144 bytes) in-place. /// Returns true if decryption succeeded (verified by TS sync bytes). /// /// Algorithm: /// 1. AES-128-ECB encrypt first 16 bytes with unit_key → derived /// 2. XOR derived with original 16 bytes → unit_decrypt_key /// 3. AES-128-CBC decrypt bytes 16..6143 with unit_decrypt_key and AACS IV /// /// Decryption restores the TS sync bytes, so the unit reads as clear afterward; /// there is no flag to clear. pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool { if unit.len() < ALIGNED_UNIT_LEN { return false; } if !is_aacs_scrambled(unit) { return true; // not encrypted } // Save original first 16 bytes (they're plaintext TP_extra_header) let mut header = [0u8; 16]; header.copy_from_slice(&unit[..16]); // Step 1: Encrypt header with unit key to derive per-unit key let derived = aes_ecb_encrypt(unit_key, &header); // Step 2: XOR to get the actual decryption key let mut decrypt_key = [0u8; 16]; for i in 0..16 { decrypt_key[i] = derived[i] ^ header[i]; } // Step 3: Decrypt bytes 16..6143 with AES-CBC aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]); // Decryption restored the TS syncs; verify the unit now looks like clear TS. verify_ts(unit) } /// Fast, NON-MUTATING unit-key validation for the brute-force key search. /// /// `decrypt_unit` pays a full 6128-byte (383-block) CBC decrypt before /// `verify_ts` can reject a wrong key — but in a brute scan ~every candidate is /// wrong. In CBC the plaintext of block *i* is `AES_dec(C_i) XOR C_{i-1}`, so /// the FIRST restored TS sync byte (payload offset 196, which lands in CBC /// block 11 of the `unit[16..]` region) can be recovered with a SINGLE block /// decrypt instead of 383. A wrong key fails this 1-byte gate ~255/256 of the /// time for the cost of one AES block; the rare survivor is then confirmed with /// the full [`decrypt_unit`], so the set of accepted keys is bit-for-bit /// identical to the slow path. /// /// The caller MUST pass an aligned, already-[`is_aacs_scrambled`] unit /// (`unit.len() >= ALIGNED_UNIT_LEN`). The brute pre-filters its units, so the /// per-candidate scramble re-scan is intentionally skipped here. /// /// NOTE: this is a search accelerator — it never writes the input and never /// participates in the content decrypt path. Aggregate correctness (does a key /// validate against *any* of the disc's units) is preserved because a true key /// restores offset-196 on every standard BD-TS unit. pub fn unit_key_validates(unit: &[u8], unit_key: &[u8; 16]) -> bool { if unit.len() < ALIGNED_UNIT_LEN { return false; } // Per-unit decrypt key: AES-ECB-encrypt the 16-byte plaintext header with // the unit key, XOR with the header (same derivation as `decrypt_unit`). 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]; } // Cheap gate: recover ONLY payload byte 196 (the 2nd BD-TS packet's sync). // The CBC region is `unit[16..]`; payload offset 196 → region offset 180 = // block 11, byte 4. P[11] = AES_dec(C[11]) XOR C[10]; C[10] is raw // ciphertext (no decrypt needed). Constant offsets for the fixed 6144 unit. const SYNC_PAYLOAD_OFF: usize = 196; let region_off = SYNC_PAYLOAD_OFF - 16; // 180 let blk = region_off / 16; // 11 let byte = region_off % 16; // 4 let c11 = 16 + blk * 16; // absolute offset of C[11] in `unit` (=192) 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]; // C[10] byte (region block 10) if b[byte] ^ prev != TS_SYNC { return false; } // Survivor (~1/256 of candidates): confirm with the authoritative full // decrypt + verify, so the verdict matches `decrypt_unit` exactly. let mut full = [0u8; ALIGNED_UNIT_LEN]; full.copy_from_slice(&unit[..ALIGNED_UNIT_LEN]); decrypt_unit(&mut full, unit_key) } /// Outcome of [`decrypt_unit_try_keys`]. /// /// Distinguishes "the unit was already clear, no key was consumed" from "key /// at index `i` decrypted it" — the bare `Option` form conflated the two /// (a clear unit reported `Some(0)`, indistinguishable from key index 0, and /// possibly out of range when `unit_keys` is empty). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnitKeyResult { /// The unit was not scrambled; it was left untouched and no key was used. AlreadyClear, /// The unit was decrypted in place by `unit_keys[index]`. DecryptedWith(usize), } /// Decrypt one aligned unit trying multiple unit keys. /// /// Returns [`UnitKeyResult::AlreadyClear`] if the unit was not scrambled (no key /// consumed), [`UnitKeyResult::DecryptedWith(i)`] if key `i` decrypted it, or /// `None` if no key worked (the unit is restored to its original bytes). pub fn decrypt_unit_try_keys(unit: &mut [u8], unit_keys: &[[u8; 16]]) -> Option { if !is_aacs_scrambled(unit) { return Some(UnitKeyResult::AlreadyClear); } // Save original for retry. Stack-backed buffer — no heap allocation, and the // restore-on-failure contract holds uniformly regardless of key count. 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)); } } // Restore original on failure unit[..ALIGNED_UNIT_LEN].copy_from_slice(&original); None } /// Remove bus encryption from an aligned unit (AACS 2.0 / UHD). /// Bus encryption uses read_data_key, decrypting bytes 16..2047 of each 2048-byte sector. pub fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) { for sector_start in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_LEN) { if sector_start + SECTOR_LEN > unit.len() { break; } // First 16 bytes of each sector are plaintext aes_cbc_decrypt( read_data_key, &mut unit[sector_start + 16..sector_start + SECTOR_LEN], ); } } /// Full decrypt of an aligned unit: bus decrypt (if needed) then AACS decrypt. pub fn decrypt_unit_full( unit: &mut [u8], unit_key: &[u8; 16], read_data_key: Option<&[u8; 16]>, ) -> bool { if !is_aacs_scrambled(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 test_decrypt_unit_unencrypted() { // A clear unit (TS syncs intact) is not scrambled → passes through. let mut unit = vec![0u8; ALIGNED_UNIT_LEN]; let mut off = 4; while off < ALIGNED_UNIT_LEN { unit[off] = TS_SYNC; off += TS_PACKET_LEN; } let key = [0u8; 16]; assert!(!is_aacs_scrambled(&unit)); assert!(decrypt_unit(&mut unit, &key)); } #[test] fn ts_packet_total_no_off_by_one() { // The maximum sync count is exactly the number of stride // positions the counting loop visits (offset 4, 196, ...), i.e. // len / 192, NOT (len - 4) / 192 + 1. For the 6144-byte aligned unit // the loop checks offsets 4..=5956 → 32 positions. let unit = vec![0u8; ALIGNED_UNIT_LEN]; assert_eq!(ts_packet_total(&unit), 32); // Confirm the loop visits exactly that many stride positions. let visited = (4..ALIGNED_UNIT_LEN).step_by(TS_PACKET_LEN).count(); assert_eq!(visited, ts_packet_total(&unit)); } #[test] fn scramble_detection_at_16_32_boundary() { // With 32 stride positions the majority threshold is // total/2 = 16. A unit with EXACTLY half its syncs intact (16) must // NOT be over-counted into the "scrambled" bucket by an inflated // total: 16 > 16 is false → not-intact → scrambled. 17 intact → clear. // The fix is that `total` is 32 (not 33), so the boundary sits cleanly // at the real midpoint. 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 += TS_PACKET_LEN; placed += 1; } unit }; assert_eq!(ts_sync_count(&set_syncs(16)), 16); assert_eq!(ts_sync_count(&set_syncs(17)), 17); // Exactly half intact → classified scrambled (16 > 16 is false). assert!(is_aacs_scrambled(&set_syncs(16))); // One past half → classified clear. assert!(!is_aacs_scrambled(&set_syncs(17))); } #[test] fn scramble_detection_extremes() { // Detection semantics for the clear-cut cases must be preserved: // a fully-clear unit (all 32 syncs) is NOT scrambled; a unit with no // syncs (fully scrambled body) IS scrambled. let mut clear = vec![0u8; ALIGNED_UNIT_LEN]; let mut off = 4; while off < ALIGNED_UNIT_LEN { clear[off] = TS_SYNC; off += TS_PACKET_LEN; } assert_eq!(ts_sync_count(&clear), 32); assert!( !is_aacs_scrambled(&clear), "fully-clear unit → not scrambled" ); let scrambled = vec![0u8; ALIGNED_UNIT_LEN]; assert_eq!(ts_sync_count(&scrambled), 0); assert!(is_aacs_scrambled(&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]; // 8 blocks let mut data = original.clone(); // Encrypt with CBC manually (forward direction) 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); // should be different after encrypt super::aes_cbc_decrypt(&key, &mut data); assert_eq!(data, original); // should match after roundtrip } #[test] fn test_decrypt_unit_synthetic() { // Build a fake 6144-byte aligned unit with known TS sync pattern, // encrypt it with the AACS algorithm, then decrypt and verify. let unit_key = [0xAAu8; 16]; // Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4 let mut plain = vec![0u8; ALIGNED_UNIT_LEN]; let mut offset = 4; while offset < ALIGNED_UNIT_LEN { plain[offset] = TS_SYNC; offset += TS_PACKET_LEN; } // No flag set: CBC-encrypting the body below scrambles packets 1..31's // TS syncs, which is exactly what `is_aacs_scrambled` (raw-sync) detects. // Now encrypt bytes 16..6143 using the AACS algorithm (reverse of decrypt) 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]; } // CBC encrypt bytes 16..6143 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]); } // Now plain contains encrypted data. Decrypt it. let mut unit = plain; assert!(is_aacs_scrambled(&unit)); assert!(decrypt_unit(&mut unit, &unit_key)); assert!(!is_aacs_scrambled(&unit)); // decrypted: TS syncs restored // Verify TS sync bytes let mut count = 0; let mut off = 4; while off < ALIGNED_UNIT_LEN { if unit[off] == TS_SYNC { count += 1; } off += TS_PACKET_LEN; } // Assert against the single canonical packet count, not the old // `(len - 4) / 192 + 1` form that `ts_packet_total` corrected away from. assert_eq!(count, ts_packet_total(&unit)); } }