//! Top-level DRM scheme dispatch. //! //! Four content-protection schemes ride through a single //! detect-then-load pipeline: //! //! | Scheme | Discriminator | //! |---------------------|------------------------------------------------| //! | [`DrmScheme::Css`] | DVD probe sector flagged scrambled | //! | [`DrmScheme::Aacs10`] | Content cert type byte `0x00` | //! | [`DrmScheme::Aacs20`] | Content cert type byte `!= 0x00`, no Variant | //! | [`DrmScheme::Aacs21`] | Content cert + MKB records `0x82` / `0x83` | //! //! Detection happens from a [`DrmProbe`] (raw inputs the caller has //! already extracted from the disc); resolution runs through a //! [`DrmContext`] (the full set of inputs the loaders need). //! //! The AACS 2.1 arm is wired but disabled. The dispatcher leaves //! [`crate::aacs::resolve_keys_v21`] reachable as a library entry point //! for fixture-driven validation, but production consumers go through //! [`DrmScheme::load`], which short-circuits V21 to `None` until the //! Variant chain has a real Variant-scheme disc to validate against. use crate::aacs; use crate::css; /// Which content-protection scheme governs a disc. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DrmScheme { /// DVD Content Scramble System. Css, /// AACS 1.0 — original BD-ROM. Aacs10, /// AACS 2.0 — UHD-BD, classical Media Key chain. Aacs20, /// AACS 2.1 — UHD-BD with Media Key Variant chain. Aacs21, } /// Inputs to [`DrmScheme::detect`]. All borrows — caller retains /// ownership. pub struct DrmProbe<'a> { /// 2048-byte sample sector from inside a DVD title's extents. Used /// only for CSS scramble-flag detection. `None` for non-DVD discs. pub dvd_sample_sector: Option<&'a [u8]>, /// Content Certificate file bytes (typically `/AACS/Content000.cer`). /// `None` when the disc has no AACS directory. pub content_cert: Option<&'a [u8]>, /// MKB file bytes (typically `/AACS/MKB_RW.inf`). Required to /// distinguish AACS 2.0 from AACS 2.1. pub mkb: Option<&'a [u8]>, } /// Inputs to [`DrmScheme::load`]. Carries everything needed by either /// the AACS or CSS loader. pub struct DrmContext<'a> { /// AACS resolver inputs — required when the scheme is any AACS /// variant. pub aacs: Option>, /// CSS resolver inputs — required when the scheme is [`DrmScheme::Css`]. pub css: Option>, } /// Resolved key material, tagged by scheme. #[derive(Debug)] pub enum ResolvedScheme { Css(css::CssState), Aacs(aacs::ResolvedKeys), } impl DrmScheme { /// Detect which DRM scheme protects the disc described by `probe`. /// /// Returns `None` for unencrypted media. The order is intentional: /// CSS is checked first (DVD-format probe), then AACS (Blu-ray /// format). pub fn detect(probe: &DrmProbe<'_>) -> Option { // CSS — DVD probe sector carries the scramble flag. if let Some(sector) = probe.dvd_sample_sector { if css::is_scrambled(sector) { return Some(DrmScheme::Css); } } // AACS — content cert type byte distinguishes V10 from V20+. // V21 promotion requires MKB Variant records. let cc = probe.content_cert.and_then(aacs::parse_content_cert)?; match cc.version { aacs::AacsVersion::V10 => Some(DrmScheme::Aacs10), aacs::AacsVersion::V20 | aacs::AacsVersion::V21 => { if let Some(mkb) = probe.mkb { let recs = aacs::variants::walk_mkb(mkb); if aacs::variants::is_variant_mkb(&recs) { return Some(DrmScheme::Aacs21); } } Some(DrmScheme::Aacs20) } } } /// Run key resolution for this scheme against `ctx`. /// /// Returns `None` when the scheme's resolver could not produce keys /// (missing context, KEYDB miss, failed crypto walk, etc.) or when /// the scheme itself is gated off (see the inline comment on the /// `Aacs21` arm). pub fn load(self, ctx: &mut DrmContext<'_>) -> Option { match self { DrmScheme::Css => ctx .css .as_mut() .and_then(css::resolve) .map(ResolvedScheme::Css), DrmScheme::Aacs10 => ctx .aacs .as_ref() .and_then(aacs::resolve_keys_v1) .map(ResolvedScheme::Aacs), DrmScheme::Aacs20 => ctx .aacs .as_ref() .and_then(aacs::resolve_keys_v2) .map(ResolvedScheme::Aacs), // AACS 2.1 derivation is wired but disabled. KCD validation // against a Variant-scheme disc is pending. To enable, // uncomment the line below. // DrmScheme::Aacs21 => ctx // .aacs // .as_ref() // .and_then(aacs::resolve_keys_v21) // .map(ResolvedScheme::Aacs), DrmScheme::Aacs21 => None, } } } #[cfg(test)] mod tests { use super::*; // Build a minimal cert: type byte + bus-encryption byte + 6 zero // cc_id bytes. fn cert(type_byte: u8) -> Vec { let mut v = vec![0u8; 8]; v[0] = type_byte; v } // Synthetic AACS 2.x MKB with no Variant records. fn mkb_classical() -> Vec { vec![ 0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D, ] } // Synthetic AACS 2.x MKB with a 0x82 + 0x83 record pair. fn mkb_with_variant() -> Vec { let mut m = mkb_classical(); m.extend_from_slice(&[0x82, 0x00, 0x00, 0x14]); m.extend_from_slice(&[0xEE; 16]); m.extend_from_slice(&[0x83, 0x00, 0x00, 0x14]); m.extend_from_slice(&[0x55; 16]); m } // Synthetic scrambled DVD sector — byte 0x14 carries the CSS // scramble flag in bits 4-5. fn scrambled_dvd_sector() -> Vec { let mut s = vec![0u8; 2048]; s[0x14] = 0x30; s } #[test] fn detect_returns_none_for_unencrypted() { let probe = DrmProbe { dvd_sample_sector: None, content_cert: None, mkb: None, }; assert_eq!(DrmScheme::detect(&probe), None); } #[test] fn detect_returns_css_for_scrambled_dvd() { let sector = scrambled_dvd_sector(); let probe = DrmProbe { dvd_sample_sector: Some(§or), content_cert: None, mkb: None, }; assert_eq!(DrmScheme::detect(&probe), Some(DrmScheme::Css)); } #[test] fn detect_returns_aacs10_for_type0_cert() { let c = cert(0x00); let probe = DrmProbe { dvd_sample_sector: None, content_cert: Some(&c), mkb: None, }; assert_eq!(DrmScheme::detect(&probe), Some(DrmScheme::Aacs10)); } #[test] fn detect_returns_aacs20_for_type1_cert_no_variant() { let c = cert(0x01); let mkb = mkb_classical(); let probe = DrmProbe { dvd_sample_sector: None, content_cert: Some(&c), mkb: Some(&mkb), }; assert_eq!(DrmScheme::detect(&probe), Some(DrmScheme::Aacs20)); } #[test] fn detect_returns_aacs21_for_type1_cert_with_variant() { let c = cert(0x01); let mkb = mkb_with_variant(); let probe = DrmProbe { dvd_sample_sector: None, content_cert: Some(&c), mkb: Some(&mkb), }; assert_eq!(DrmScheme::detect(&probe), Some(DrmScheme::Aacs21)); } #[test] fn detect_returns_aacs20_when_mkb_absent() { // Type-1 cert but no MKB to upgrade with -> Aacs20. let c = cert(0x01); let probe = DrmProbe { dvd_sample_sector: None, content_cert: Some(&c), mkb: None, }; assert_eq!(DrmScheme::detect(&probe), Some(DrmScheme::Aacs20)); } #[test] fn load_aacs21_returns_none() { // The Aacs21 dispatch arm is commented out; load() must // return None until KCD validation lands. let uk_ro = vec![0u8; 256]; let vid = [0u8; 16]; let keydb = aacs::KeyDb::empty(); let ctx_aacs = aacs::ResolveContext { unit_key_ro: &uk_ro, content_cert: None, volume_id: &vid, keydb: &keydb, mkb: None, }; let mut ctx = DrmContext { aacs: Some(ctx_aacs), css: None, }; assert!(DrmScheme::Aacs21.load(&mut ctx).is_none()); } /// Exercises the V21 helper directly. Gated `#[ignore]` because /// the chain reaches `MediaKeyVariantError::VariantsTableUnavailable` /// without a real Variant-scheme disc to fix the per-uv table /// layout against — running it here would assert only the /// not-yet-wired error code. Kept as a wiring smoke-test for /// future enablement. #[test] #[ignore] fn resolve_keys_v21_helper_exists() { let uk_ro = vec![0u8; 256]; let vid = [0xAAu8; 16]; let keydb = aacs::KeyDb::empty(); let mkb = mkb_with_variant(); let ctx = aacs::ResolveContext { unit_key_ro: &uk_ro, content_cert: None, volume_id: &vid, keydb: &keydb, mkb: Some(&mkb), }; // Just confirm the symbol is callable; we don't assert on the // result. let _ = aacs::resolve_keys_v21(&ctx); } }