Add UnitKey.variant_number (0 = ordinary, 1..32 = forensic variant) with new/variant constructors, and aacs::variant_select — resolve a disc's single variant and classify each aligned unit (default / variant / drop foreign / conceal keyless). Correct IndividualSegment.tbl: the per-record field is the variant (cycles 1..32 on a retail disc), not a segment number — Segment.number -> Segment.variant.
114 lines
5.0 KiB
Rust
114 lines
5.0 KiB
Rust
//! AACS decryption — Volume Unique Key lookup and title key derivation.
|
||
//!
|
||
//! Two paths:
|
||
//! 1. VUK lookup: disc_hash → KEYDB.cfg → VUK (fast, 99% of discs)
|
||
//! 2. Full handshake: device_keys + MKB → Media Key → + Volume ID → VUK (fallback)
|
||
//!
|
||
//! KEYDB.cfg format:
|
||
//! | DK | DEVICE_KEY 0x... | DEVICE_NODE 0x... | KEY_UV 0x... | KEY_U_MASK_SHIFT 0x...
|
||
//! | PK | 0x...
|
||
//! | HC | HOST_PRIV_KEY 0x... | HOST_CERT 0x...
|
||
//! | HC2 | HOST_PRIV_KEY 0x... | HOST_CERT 0x...
|
||
//! 0x<disc_hash> = <title> | D | <date> | M | 0x<media_key> | I | 0x<disc_id> | V | 0x<vuk> | U | <unit_keys>
|
||
//!
|
||
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
||
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
||
//!
|
||
//! ## Spec provenance
|
||
//!
|
||
//! The crypto below carries `[TAG] §x.y` citations back to the published AACS
|
||
//! specification (Final Rev 0.953), so each primitive links to the section it
|
||
//! implements:
|
||
//! - `[C]` — AACS Introduction and Common Cryptographic Elements Book (primitives, MKB/key-management).
|
||
//! - `[PR]` — AACS Pre-recorded Video Book (Volume/Title Key layer).
|
||
//! - `[BD]` — AACS Blu-ray Disc Pre-recorded Book (CPS Unit Key, Aligned Unit, Block Key).
|
||
//! - `[RE]` — reverse-engineered from real discs, cited only where the public
|
||
//! spec is silent (the `0x86` verify record and the Category-C MKB type values).
|
||
|
||
pub mod content;
|
||
pub mod crypto;
|
||
pub mod derive;
|
||
pub mod host_certs;
|
||
pub mod inf;
|
||
pub mod mkb;
|
||
pub mod provider;
|
||
pub mod resolve;
|
||
pub mod segment;
|
||
pub mod segment_key;
|
||
pub mod trace;
|
||
pub mod types;
|
||
pub mod variant;
|
||
pub mod variant_select;
|
||
|
||
/// On-disc UDF paths to the AACS key-input files (with their fallbacks).
|
||
/// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`,
|
||
/// `read_mkb_content`, `read_aacs_version`) walks the exact same files — adding
|
||
/// or changing a fallback in one place can then never silently diverge the
|
||
/// disc_hash / MKB / VID that another reader feeds a key service.
|
||
pub const PATH_UNIT_KEY_RO: &str = "/AACS/Unit_Key_RO.inf";
|
||
pub const PATH_UNIT_KEY_RO_DUPLICATE: &str = "/AACS/DUPLICATE/Unit_Key_RO.inf";
|
||
pub const PATH_MKB_RO: &str = "/AACS/MKB_RO.inf";
|
||
pub const PATH_MKB_RW: &str = "/AACS/MKB_RW.inf";
|
||
pub const PATH_CONTENT_CERT: &str = "/AACS/Content000.cer";
|
||
pub const PATH_CONTENT_CERT_ALT: &str = "/AACS/Content001.cer";
|
||
|
||
// The module structure IS the public API — consumers import from the owning
|
||
// module directly (e.g. `aacs::content::decrypt_unit`, `aacs::mkb::MkbType`,
|
||
// `aacs::derive::{derive_vuk, resolve_candidate}`, `aacs::resolve::resolve_keys_v2`).
|
||
// The `derive::probe` reproduction harness stays reachable via its module path.
|
||
//
|
||
// A small set of flat re-exports is kept for the typed key primitives and the
|
||
// content-decrypt entry points that downstream key-source crates import through
|
||
// the `aacs::` path. These are the stable, load-bearing names; keeping them here
|
||
// lets those crates track the module refactor without a lockstep re-pin.
|
||
pub use content::{ALIGNED_UNIT_LEN, decrypt_unit_try_keys};
|
||
pub use derive::derive_vuk;
|
||
pub use types::{DeviceKey, HostCert, MediaKey, ProcessingKey, UnitKey, Vid, Vuk};
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
//! Surface guards. The public API is the module tree itself (no facade).
|
||
//! Touching one representative item per module keeps these as a
|
||
//! compile-time contract that the module paths stay stable.
|
||
|
||
use super::content::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
||
use super::inf::{disc_hash, disc_hash_hex};
|
||
use super::mkb::{AacsVersion, mkb_content_len, walk_mkb};
|
||
use super::variant::is_variant_mkb;
|
||
|
||
#[test]
|
||
fn aligned_unit_len_is_three_2048_byte_sectors() {
|
||
// ALIGNED_UNIT_LEN is the AACS aligned-unit size: 3 × 2048 = 6144.
|
||
// Re-exported from decrypt; pin the value here so the public constant
|
||
// and the spec stay in lockstep.
|
||
assert_eq!(ALIGNED_UNIT_LEN, 6144);
|
||
assert_eq!(ALIGNED_UNIT_LEN, 3 * 2048);
|
||
}
|
||
|
||
#[test]
|
||
fn version_strides_are_reexported_and_distinct() {
|
||
// The three AACS generations are part of the public surface, and the
|
||
// V10 (48) vs V20/V21 (64) stride distinction is the load-bearing
|
||
// difference. Confirm the enum re-export is usable and the variants
|
||
// are distinct values.
|
||
assert_ne!(AacsVersion::V10, AacsVersion::V20);
|
||
assert_ne!(AacsVersion::V20, AacsVersion::V21);
|
||
}
|
||
|
||
#[test]
|
||
fn public_helpers_are_callable_by_module_path() {
|
||
// Touch a representative function from each module so a dropped/renamed
|
||
// item fails to compile. Smoke calls, not behavioural assertions.
|
||
let _ = ts_sync_destroyed(&[0u8; ALIGNED_UNIT_LEN]);
|
||
let _ = mkb_content_len(&[]);
|
||
let _ = is_variant_mkb(&walk_mkb(&[]));
|
||
let _ = disc_hash_hex(&disc_hash(b"x"));
|
||
let _ = super::derive::resolve_candidate(
|
||
&super::derive::KeyCandidate::Uk(super::types::UnitKey::new(0, [0u8; 16])),
|
||
&[],
|
||
&[],
|
||
None,
|
||
);
|
||
}
|
||
}
|