libfreemkv: clip-anchored AACS unit gate + consolidate key mechanism

The AACS unit-alignment gate measured `lba % 3` against absolute disc LBA 0,
but aligned units are anchored at each clip's encrypted-region start. A clip
whose start_lba is not 3-aligned had its readable units wrongly rejected with
"Decryption failed" (the big-title-only failure on some Blu-rays). One
canonical clip-anchored helper (`aacs::is_unit_aligned`) is now the single
source of truth for the decrypt-on-read gate; both mux read paths set the
per-extent `unit_base = start_lba` via a new `SectorSource::set_unit_base`.

Also moves key *mechanism* into the library: the encrypted sample reader
(`read_encrypted_units`) and the candidate-key resolution loop
(`resolve_and_apply`) now live here, so a key source is purely a lookup.
Regression test covers a clip based at a non-3-aligned LBA.
This commit is contained in:
Matthew Jackson
2026-06-24 15:40:50 -07:00
parent 987e26e44d
commit 63ed05bd63
8 changed files with 224 additions and 16 deletions
+20
View File
@@ -13,6 +13,26 @@ pub(crate) const AACS_IV: [u8; 16] = [
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
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_LEN) 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).
///
/// AACS aligned units (6144 B / 3 sectors) are anchored at the start of each
/// clip's encrypted region, so a read must begin a whole number of units past
/// that base for `decrypt_sectors` (which anchors units at buffer offset 0) to
/// align the CBC correctly. This is the SINGLE source of truth for the test —
/// the decrypt-on-read gate, the inline and highway mux read paths, and the
/// key-validation sample reader all key off this, never absolute `lba % 3`. A
/// disc whose clip `start_lba` is not itself 3-aligned would otherwise mis-gate
/// (reject readable units, then report "Decryption failed") on exactly the
/// titles whose clips land off a 3-boundary.
pub fn is_unit_aligned(lba: u32, unit_base: u32) -> bool {
lba.wrapping_sub(unit_base) % ALIGNED_UNIT_SECTORS == 0
}
/// Size of one sector.
const SECTOR_LEN: usize = 2048;
+3 -2
View File
@@ -24,8 +24,9 @@ pub mod variants;
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
pub use decrypt::{
ALIGNED_UNIT_LEN, UnitKeyResult, decrypt_bus, decrypt_unit, decrypt_unit_full,
decrypt_unit_try_keys, is_aacs_scrambled, ts_packet_total, ts_sync_count, unit_key_validates,
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, decrypt_bus, decrypt_unit,
decrypt_unit_full, decrypt_unit_try_keys, is_aacs_scrambled, is_unit_aligned, ts_packet_total,
ts_sync_count, unit_key_validates,
};
pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
pub use keys::probe;