libfreemkv 0.31.1

Open source raw disc access library for optical drives
Documentation
//! 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`] | as Aacs20 + MKB Variant records `0x82`/`0x83` |
//!
//! The content cert type byte only ever decodes to V10 (`0x00`) or V20
//! (`!= 0x00`); the V21 promotion is decided solely by the MKB Variant
//! walk in [`DrmScheme::detect`], never by the cert byte.
//!
//! 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).
//!
//! AACS 2.1 discs that are fully keyed in `keydb.cfg` decrypt through
//! the same classical Media Key chain as AACS 2.0, so [`DrmScheme::load`]
//! routes the `Aacs21` arm to [`crate::aacs::resolve_keys_v2`] — the
//! KEYDB lookup paths (MK+VID, disc-hash VUK, pre-decrypted unit keys)
//! succeed for any disc present in the keydb. The dedicated Variant/KCD
//! chain ([`crate::aacs::resolve_keys_v21`]) stays reachable as a
//! library entry point for fixture-driven validation but is not yet on
//! the dispatch path; it is enabled once KCD validation against a real
//! Variant-scheme disc lands.

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<aacs::ResolveContext<'a>>,
    /// CSS resolver inputs — required when the scheme is [`DrmScheme::Css`].
    pub css: Option<css::CssContext<'a>>,
}

/// 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<DrmScheme> {
        // 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.).
    ///
    /// The `Aacs21` arm resolves through the classical [`aacs::resolve_keys_v2`]
    /// chain: AACS 2.1 discs already keyed in `keydb.cfg` decrypt identically
    /// to AACS 2.0 (the KEYDB MK+VID / disc-hash VUK / pre-decrypted unit-key
    /// paths succeed for any disc in the keydb), and `resolve_keys_v2` promotes
    /// the resolved version to V21 when Variant MKB records are present. The
    /// dedicated Variant/KCD chain ([`aacs::resolve_keys_v21`]) is kept opt-in
    /// until KCD validation against a real Variant-scheme disc lands.
    pub fn load(self, ctx: &mut DrmContext<'_>) -> Option<ResolvedScheme> {
        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),
            // Both Aacs20 and Aacs21 route through the classical V2 chain.
            // The Variant/KCD chain (resolve_keys_v21) is wired but gated;
            // a keyed-in V21 disc resolves via the KEYDB paths here.
            DrmScheme::Aacs20 | DrmScheme::Aacs21 => ctx
                .aacs
                .as_ref()
                .and_then(aacs::resolve_keys_v2)
                .map(ResolvedScheme::Aacs),
        }
    }
}

#[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<u8> {
        let mut v = vec![0u8; 8];
        v[0] = type_byte;
        v
    }

    // Synthetic AACS 2.x MKB with no Variant records.
    fn mkb_classical() -> Vec<u8> {
        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<u8> {
        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<u8> {
        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(&sector),
            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_routes_through_v2_resolver() {
        // The Aacs21 arm shares the classical V2 resolver with Aacs20, so a
        // V21 disc keyed in keydb.cfg resolves instead of being short-circuited
        // to None at the dispatcher. With an EMPTY keydb both schemes fail key
        // resolution identically — proving Aacs21 takes the resolver path
        // rather than an unconditional `None` gate.
        let uk_ro = vec![0u8; 256];
        let vid = [0u8; 16];
        let keydb = aacs::KeyDb::empty();
        let providers: &[&dyn aacs::KeyProvider] = &[&keydb];

        let make_ctx = || DrmContext {
            aacs: Some(aacs::ResolveContext {
                unit_key_ro: &uk_ro,
                content_cert: None,
                volume_id: &vid,
                providers,
                mkb: None,
            }),
            css: None,
        };

        let v20 = DrmScheme::Aacs20.load(&mut make_ctx());
        let v21 = DrmScheme::Aacs21.load(&mut make_ctx());
        // Same resolver, same (empty-keydb) outcome.
        assert_eq!(v20.is_none(), v21.is_none());
        // Empty keydb -> no keys for either.
        assert!(v21.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 providers: &[&dyn aacs::KeyProvider] = &[&keydb];
        let mkb = mkb_with_variant();
        let ctx = aacs::ResolveContext {
            unit_key_ro: &uk_ro,
            content_cert: None,
            volume_id: &vid,
            providers,
            mkb: Some(&mkb),
        };
        // Just confirm the symbol is callable; we don't assert on the
        // result.
        let _ = aacs::resolve_keys_v21(&ctx);
    }
}