Files
libfreemkv/src/aacs/content.rs
T
Matthew Jackson 1810161256 keyproof: answer only for a provable encrypted BD-TS unit; differential tests
unit_key_opens is false for other formats, units that are short, clear, or carry
fewer than KEY_PROOF_PACKETS content packets (before, an empty slice or an
all-zero unit opened under any key). The fast path returns packet counts, and a
seeded differential pins them to decrypt_unit's over padding, zero-sync and
corrupted packets; it fails if either skip branch is removed. The compile_fail
door names E0425.
2026-10-07 11:09:36 -07:00

443 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! AACS content decryption — aligned-unit / bus decryption and TS verification.
//! The low-level AES primitives it uses live in [`super::crypto`].
#[cfg(test)]
use aes::Aes128;
#[cfg(test)]
use aes::cipher::{Array, KeyInit};
use super::crypto::{aes_cbc_decrypt, aes_cbc_encrypt, aes_ecb_encrypt};
// Only this module's test fixtures build CBC ciphertext by hand now — the
// production paths get the IV from `aes_cbc_encrypt` / `aes_cbc_decrypt`.
#[cfg(test)]
use super::crypto::AACS_IV;
// ── AACS constants ──────────────────────────────────────────────────────────
/// Size of an AACS aligned unit (3 × 2048-byte sectors). `[BD]` §3.10.1.
pub const ALIGNED_UNIT_LEN: usize = 6144;
/// An AACS aligned unit spans this many 2048-byte sectors (3).
pub const ALIGNED_UNIT_SECTORS: u32 = (ALIGNED_UNIT_LEN / SECTOR_BYTES) as u32;
/// Whether `lba` sits on an AACS aligned-unit boundary, measured **relative to
/// the encrypted region's base LBA** (`unit_base` = the clip/extent `start_lba`,
/// NOT absolute disc LBA 0). This is the SINGLE source of truth used by the
/// decrypt-on-read gate, the mux read paths, and key validation — never
/// absolute `lba % 3`.
///
/// `saturating_sub` keeps `lba < unit_base` well-defined (clamped to 0, a unit boundary)
/// instead of a `wrapping_sub` mod-3 underflow trap.
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
lba.saturating_sub(unit_base)
.is_multiple_of(ALIGNED_UNIT_SECTORS)
}
use crate::consts::SECTOR_BYTES;
use crate::consts::BD_SOURCE_PACKET_BYTES;
/// TS sync byte.
const TS_SYNC: u8 = 0x47;
// ── Content decryption ──────────────────────────────────────────────────────
/// The AUTHORITATIVE AACS "is this aligned unit encrypted?" signal, per
/// container: for `BdTs` the Copy Permission Indicator in the top 2 bits of
/// byte 0 (`[BD]` §3.10.2, always clear in the unencrypted 16-byte seed —
/// `(buf[0] & 0xC0) == 0` → clear); for `MpegPs` (HD-DVD `.evo`) whether any
/// pack of the slice carries `PES_scrambling_control` `01` (`[HD]` §4.3.2: HD DVD
/// encrypts per pack, so every pack is judged, not the first).
///
/// Readable WITHOUT a key. Only meaningful when `unit` is read at the correct
/// clip-FILE-anchored boundary.
pub fn aacs_unit_encrypted(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
use crate::disc::ContentFormat;
match format {
ContentFormat::BdTs => unit.len() >= ALIGNED_UNIT_LEN && (unit[0] & 0xC0) != 0,
ContentFormat::MpegPs | ContentFormat::DvdPs => any_pack_scrambled(unit),
}
}
// Whether any pack of `buf` (a trailing partial pack included) is flagged scrambled.
fn any_pack_scrambled(buf: &[u8]) -> bool {
use super::hddvd::{PackKind, classify};
buf.chunks(SECTOR_BYTES)
.any(|p| classify(p) == PackKind::Scrambled)
}
/// The AACS encrypted flag from an aligned unit's CLEAR seed, readable even on a
/// trailing PARTIAL unit (unlike [`aacs_unit_encrypted`], which requires a whole
/// 6144-byte unit for `BdTs`). The flag lives at a fixed low offset in the clear header, so a
/// fragment that still contains that byte can be classified. Used to catch an
/// encrypted unit truncated across a buffer/extent boundary — a fragment we cannot
/// CBC-decrypt and must not emit as clear. `false` for a slice too short to hold
/// the flag byte. Same clip-anchored-read caveat as [`aacs_unit_encrypted`].
pub fn aacs_unit_seed_encrypted(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
use crate::disc::ContentFormat;
match format {
ContentFormat::BdTs => unit.first().is_some_and(|b| b & 0xC0 != 0),
ContentFormat::MpegPs | ContentFormat::DvdPs => any_pack_scrambled(unit),
}
}
/// Clear the Copy_permission_indicator of every source packet of a unit freemkv has
/// DECRYPTED, so the flag is true of the bytes written (KU design §5.4, K-13). `BdTs`
/// only; HD DVD (`MpegPs`) is excluded (§2.6). Never called on a unit left as ciphertext.
pub(crate) fn clear_copy_permission_indicator(unit: &mut [u8], format: crate::disc::ContentFormat) {
if format != crate::disc::ContentFormat::BdTs || unit.len() < ALIGNED_UNIT_LEN {
return;
}
// KS-5 [BD] §3.10.2: "shall be set to 11₂ if the data is encrypted, or … 00₂ if the data
// is not encrypted"; KS-6: CPI is the top 2 of TP_extra_header's 32 bits (ATS kept).
// KS-22 libaacs `_verify_ts` (corroboration): "buf[i] &= ~0xc0;" per 192-byte packet.
for packet in unit[..ALIGNED_UNIT_LEN].chunks_mut(BD_SOURCE_PACKET_BYTES) {
packet[0] &= 0x3F;
}
}
/// Test helper: `plain` as a decrypt writes it, CPI cleared in every source packet (KS-5;
/// KU design §5.4). For a buffer of whole decrypted units only.
#[cfg(test)]
pub(crate) fn cpi_cleared(mut plain: Vec<u8>) -> Vec<u8> {
for packet in plain.chunks_mut(BD_SOURCE_PACKET_BYTES) {
packet[0] &= 0x3F;
}
plain
}
/// Was `unit` cut on its file's unit grid? An encrypted BD-TS unit's clear seed
/// always starts a source packet, so its TS sync (0x47) sits at byte 4; a chunk
/// read off the grid starts mid-unit on ciphertext. Clear units, and `MpegPs`
/// (seed layout unverified), always pass. A wrong-grid chunk with CPI=0 also
/// passes (~1 in 4 per unit), so single-unit reads can slip; multi-unit reads are
/// caught almost surely. A damaged seed fails it too: either way the unit opens under
/// no key and readers blank it (`decrypt::blank_damaged_units`); it is never E7013.
pub(crate) fn aacs_unit_on_grid(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
format != crate::disc::ContentFormat::BdTs
|| !aacs_unit_seed_encrypted(unit, format)
|| unit.get(4) == Some(&TS_SYNC)
}
/// True when an aligned unit is flagged encrypted AND still looks scrambled
/// (structure not yet restored) — genuine encrypted content NOT yet decrypted.
///
/// Composes [`aacs_unit_encrypted`] (the authoritative flag) with an IDEMPOTENT "structure
/// restored?" check, so callers that may run twice over the same buffer (re-decrypt, sampling,
/// diagnosis) get a stable answer. A unit freemkv decrypted has its flag CLEARED (KS-5 `[BD]`
/// §3.10.2: "00₂ if the data is not encrypted"; KU design §5.4), so it reads clear here; a
/// unit left as ciphertext keeps it. Only meaningful at the clip-FILE-anchored boundary.
pub fn aacs_unit_needs_decrypt(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
// "Still needs key" = flagged encrypted AND not structurally clean per the
// ONE definition, [`is_clean`]'s min(E,4) proof floor. Never a second threshold:
// the old >50% majority false-flagged bad-but-opened units, causing re-sample storms.
aacs_unit_encrypted(unit, format) && !is_clean(unit, format)
}
// Minimum synced 0x47 packets (of the ~31/unit) that PROVE a key opened a unit: an ABSOLUTE
// proof floor, NOT a proportion (false-pass ≈ C(31,4)*256^-4 ≈ 1e-5/unit).
const KEY_PROOF_PACKETS: usize = 4;
/// Structural "did a key open this content unit?" — the pure, NO-CRYPTO signal
/// for key SELECTION (pick the right key among multiple on a multi-CPS disc) and
/// read VERIFY. AACS has no cryptographic "did the key work" answer (no MAC), so
/// a key is proven STRUCTURALLY: its plaintext must look like valid content for
/// the disc's container. This dispatches to the right container check by
/// `format` — BD/UHD/FMTS are Transport Stream ([`is_clean_ts`]); HD-DVD `.evo`
/// is Program Stream ([`is_clean_ps`]). NOT a decryption verdict: a correct key
/// can decrypt structurally-broken content, which is the muxer's concern.
pub fn is_clean(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
match format {
crate::disc::ContentFormat::BdTs => is_clean_ts(unit),
crate::disc::ContentFormat::MpegPs | crate::disc::ContentFormat::DvdPs => is_clean_ps(unit),
}
}
// Structural "does this unit carry enough valid MPEG-TS to prove a key opened it?" (the mux's
// key-selection/verify signal): synced >= min(E, KEY_PROOF_PACKETS) over non-padding packets.
fn is_clean_ts(unit: &[u8]) -> bool {
let (content, synced) = ts_packet_counts(unit);
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
}
// `(content, synced)`: non-padding packets after packet 0, and those whose sync byte is 0x47.
fn ts_packet_counts(unit: &[u8]) -> (usize, usize) {
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
let limit = ALIGNED_UNIT_LEN.min(unit.len());
let mut content = 0usize;
let mut synced = 0usize;
// Skip packet 0: its sync byte lives in the clear 16-byte seed (unencrypted),
// so it is `0x47` regardless of the key and proves nothing about decryption.
let mut off = PKT;
while off + PKT <= limit {
let payload = &unit[off + 4..off + PKT];
if !payload.iter().all(|&b| b == 0) {
content += 1;
if unit[off + 4] == TS_SYNC {
synced += 1;
}
}
off += PKT;
}
(content, synced)
}
/// Does `unit_key` open this encrypted BD-TS aligned `unit`? One bit, never plaintext.
/// `true` only for a whole 6144-byte unit flagged encrypted whose plaintext would carry
/// at least `KEY_PROOF_PACKETS` content packets, all synced: anything else proves nothing
/// and is `false` (other formats, short or clear units, too little content). A wrong key
/// passes about once in 10^5 units, so confirm a match on several units.
///
/// Behind the `keyproof` feature so the default public API still cannot decrypt AACS: a
/// key oracle, not a decryptor (KU §2.2; see `keys::public_api_cannot_decrypt_aacs`).
#[cfg(feature = "keyproof")]
pub fn unit_key_opens(
unit: &[u8],
unit_key: &[u8; 16],
format: crate::disc::ContentFormat,
) -> bool {
use crate::disc::ContentFormat;
if !matches!(format, ContentFormat::BdTs)
|| unit.len() != ALIGNED_UNIT_LEN
|| !aacs_unit_encrypted(unit, format)
{
return false;
}
let (content, synced) = ts_unit_key_proof(unit, unit_key);
content >= KEY_PROOF_PACKETS && synced >= KEY_PROOF_PACKETS
}
/// [`is_clean_ts`]'s verdict for `decrypt_unit(unit, unit_key)`, from the packet counts of
/// `ts_unit_key_proof`.
#[cfg(test)]
fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool {
let (content, synced) = ts_unit_key_proof(unit, unit_key);
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
}
/// The `(content, synced)` counts `ts_packet_counts` would see after `decrypt_unit`, from
/// one AES block per source packet. CBC decrypts any block alone
/// (`P[i] = AES_dec(C[i]) ⊕ C[i-1]`), and packet heads sit at block-aligned offsets ≥ 192.
/// Packet 0 is skipped (its sync byte is in the clear seed), as are all-zero padding
/// packets, which `decrypt_unit` zeroes.
#[cfg(any(test, feature = "keyproof"))]
fn ts_unit_key_proof(unit: &[u8], unit_key: &[u8; 16]) -> (usize, usize) {
use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
use aes::{Aes128, Block};
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
const NPKT: usize = ALIGNED_UNIT_LEN / PKT;
debug_assert_eq!(unit.len(), ALIGNED_UNIT_LEN);
let block_at =
|o: usize| -> Block { Array::try_from(&unit[o..o + 16]).expect("16-byte block") };
// Block key = AES-128E(unit_key, seed) ⊕ seed — `decrypt_unit`'s derivation.
let mut bk = block_at(0);
Aes128::new(&(*unit_key).into()).encrypt_block(&mut bk);
for (b, s) in bk.iter_mut().zip(&unit[..16]) {
*b ^= s;
}
let cipher = Aes128::new(&bk);
// Plaintext of the 16-byte block at `o` (o ≥ 192, so C[i-1] is ciphertext).
let plain = |o: usize| {
let mut b = block_at(o);
cipher.decrypt_block(&mut b);
for (x, c) in b.iter_mut().zip(&unit[o - 16..o]) {
*x ^= c;
}
b
};
// Batch the head block of every non-padding packet (the backend pipelines it).
let mut offs = [0usize; NPKT];
let mut heads = [Block::default(); NPKT];
let mut n = 0;
for p in 1..NPKT {
let off = p * PKT;
if unit[off..off + PKT].iter().all(|&b| b == 0) {
continue; // padding — decrypt_unit restores it to zeros: not content
}
offs[n] = off;
heads[n] = block_at(off);
n += 1;
}
cipher.decrypt_blocks(&mut heads[..n]);
let (mut content, mut synced) = (0usize, 0usize);
for (head, &off) in heads[..n].iter().zip(&offs[..n]) {
match head[4] ^ unit[off - 16 + 4] {
TS_SYNC => {
content += 1;
synced += 1;
}
0 => {
// Sync byte decrypted to 0x00: content only if the rest of the
// payload isn't all zero too.
let first = plain(off);
let nonzero = first[4..].iter().any(|&b| b != 0)
|| (1..PKT / 16).any(|j| plain(off + 16 * j).iter().any(|&b| b != 0));
if nonzero {
content += 1;
}
}
_ => content += 1,
}
}
(content, synced)
}
/// 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 += BD_SOURCE_PACKET_BYTES;
}
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) / BD_SOURCE_PACKET_BYTES + 1` over-counted by one for lengths of the
// form `4 + k·192`.
unit.len() / BD_SOURCE_PACKET_BYTES
}
// The Program-Stream arm of [`is_clean`] (HD-DVD `.evo`): no pack still flagged scrambled and
// none failing its payload check past the clear head (`hddvd::payload_check`). The pack start
// code proves nothing: an encrypted pack keeps it in its clear 128-byte head (`[HD]` §4.3.2).
fn is_clean_ps(unit: &[u8]) -> bool {
!any_pack_scrambled(unit)
&& unit.as_chunks::<SECTOR_BYTES>().0.iter().all(|p| {
p[..4] == [0x00, 0x00, 0x01, 0xBA] && super::hddvd::payload_check(p) != Some(false)
})
}
/// Decrypt one AACS aligned unit (6144 bytes) IN PLACE — PURE crypto that
/// applies `unit_key`; makes no verdict about whether the plaintext is clean
/// TS (that's the separate [`is_clean_ts`] question, for key SELECTION or a
/// read VERIFY).
///
/// Applies the key UNCONDITIONALLY: does NOT check the encrypted-flag, so the CALLER must gate
/// on [`aacs_unit_encrypted`] first. Block Key = AES-128E(Kcu, seed) ⊕ seed, then AES-128-CBC
/// decrypt bytes 16..6144 under the AACS IV.
pub(crate) fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) {
if unit.len() < ALIGNED_UNIT_LEN {
return;
}
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
let npkt = ALIGNED_UNIT_LEN / PKT;
let mut pad = [false; ALIGNED_UNIT_LEN / PKT];
for (p, slot) in pad.iter_mut().enumerate().take(npkt) {
let off = p * PKT;
*slot = unit[off..off + PKT].iter().all(|&b| b == 0);
}
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]);
for (p, &is_pad) in pad.iter().enumerate().take(npkt) {
if is_pad {
let off = p * PKT;
for b in unit[off..off + PKT].iter_mut() {
*b = 0;
}
}
}
}
/// Encrypt one AACS aligned unit (6144 bytes) IN PLACE — the exact inverse of `decrypt_unit`.
/// Caller must set the encrypted flag BEFORE calling and check the returned bool.
#[must_use = "returns false when the slice is too short to encrypt, leaving \
plaintext behind a flag that already says 'encrypted'"]
pub fn encrypt_unit(unit: &mut [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 k = [0u8; 16];
for i in 0..16 {
k[i] = derived[i] ^ header[i];
}
// CBC-encrypt bytes 16.. under the fixed AACS IV — the exact forward of the
// `aes_cbc_decrypt` call in `decrypt_unit`, and one key expansion for the whole
// unit rather than one per 16-byte block.
aes_cbc_encrypt(&k, &mut unit[16..ALIGNED_UNIT_LEN]);
true
}
/// Remove bus encryption from an aligned unit (AACS 2.0 / UHD).
/// Bus encryption uses read_data_key, decrypting bytes 16..2048 of each 2048-byte sector.
/// Test-only unit-granular form; production de-busses via [`decrypt_bus_in_content`].
#[cfg(test)]
pub(crate) fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) {
// Expand the key schedule ONCE for the whole unit: `read_data_key` is
// loop-invariant, but per-sector `aes_cbc_decrypt` rebuilt it 3x per unit
// (~29M redundant expansions over a 90GB read) on the decrypt hot path.
let cipher = crate::aacs::crypto::new_cipher_for(read_data_key);
for sector_start in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_BYTES) {
if sector_start + SECTOR_BYTES > unit.len() {
break;
}
// First 16 bytes of each sector are plaintext
crate::aacs::crypto::cbc_decrypt_blocks(
&cipher,
&mut unit[sector_start + 16..sector_start + SECTOR_BYTES],
);
}
}
/// Remove bus encryption from every whole 2048-byte sector of `buf` (bytes
/// 16..2048 of each; the first 16 are plaintext on the wire). For content-only
/// callers; whole-disc readers gate through `sector::bus_removal::BusMap`.
pub(crate) fn decrypt_bus_sectors(buf: &mut [u8], read_data_key: &[u8; 16]) {
let cipher = crate::aacs::crypto::new_cipher_for(read_data_key);
for sector in buf.as_chunks_mut::<SECTOR_BYTES>().0 {
crate::aacs::crypto::cbc_decrypt_blocks(&cipher, &mut sector[16..]);
}
}
/// Test-only exact inverse of [`decrypt_bus`]: apply AACS 2.0 bus ENCRYPTION to
/// an aligned unit in place — per-sector AES-CBC-encrypt bytes 16..2048 of each
/// 2048-byte sector under `read_data_key`, leaving the first 16 bytes of every
/// sector clear (they are plaintext on the wire, as `decrypt_bus` relies on).
/// Used to build a bus-encrypted fixture that `decrypt_bus` must recover, and by
/// the `decrypt.rs` end-to-end tests to model the drive's forward transform.
#[cfg(test)]
pub(crate) fn encrypt_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;
}
crate::aacs::crypto::aes_cbc_encrypt(
read_data_key,
&mut unit[sector_start + 16..sector_start + SECTOR_BYTES],
);
}
}
#[cfg(test)]
#[path = "content_tests.rs"]
mod tests;
// Spec guards (keys-upfront-design §7.8) on the unit decrypt; here because
// `decrypt_unit` is crate-private (KU §2.2). Vectors come from the `aes` crate.
#[cfg(test)]
#[path = "content_spec_guards_tests.rs"]
mod spec_guards;