//! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing. use super::decrypt::aes_ecb_decrypt; use super::keydb::{DeviceKey, KeyDb}; // ── AACS version ──────────────────────────────────────────────────────────── /// AACS protection generation a disc carries. /// /// The content cert byte distinguishes V10 (`0x00`) from V20 (`0x01`). V21 /// cannot be detected from the cert alone — a V21 disc carries a V20 cert /// and is upgraded to `V21` only after the MKB walk turns up record types /// `0x82` / `0x83` (Media Key Variant Data and Variant Number). /// /// Key-storage stride in `Unit_Key_RO.inf` is 48 bytes for V10 and 64 /// bytes for V20 / V21. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AacsVersion { /// AACS 1.0 — original BD-ROM. V10, /// AACS 2.0 — UHD-BD, classical Media Key derivation. V20, /// AACS 2.1 — UHD-BD with Media Key Variant chain on top of V20. V21, } impl AacsVersion { /// Stride (in bytes) between successive encrypted unit keys in /// `Unit_Key_RO.inf`. fn unit_key_stride(self) -> usize { match self { AacsVersion::V10 => 48, AacsVersion::V20 | AacsVersion::V21 => 64, } } } // ── VUK derivation ────────────────────────────────────────────────────────── /// Derive VUK from Media Key and Volume ID. /// VUK = AES-128-ECB-DECRYPT(media_key, volume_id) XOR volume_id pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] { let mut vuk = aes_ecb_decrypt(media_key, volume_id); for i in 0..16 { vuk[i] ^= volume_id[i]; } vuk } /// Decrypt an encrypted unit key using the VUK (AES-128-ECB). pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] { aes_ecb_decrypt(vuk, encrypted_uk) } // ── Unit_Key_RO.inf parsing ───────────────────────────────────────────────── /// Parsed Unit_Key_RO.inf file. #[derive(Debug)] pub struct UnitKeyFile { /// Disc hash (SHA1 of the entire file) — used as KEYDB lookup key pub disc_hash: [u8; 20], /// Application type (1 = BD-ROM) pub app_type: u8, /// Number of BDMV directories pub num_bdmv_dir: u8, /// Whether SKB MKB is used pub use_skb_mkb: bool, /// AACS generation this file's stride matches pub version: AacsVersion, /// Encrypted unit keys (CPS unit number, encrypted key) pub encrypted_keys: Vec<(u32, [u8; 16])>, /// Title → CPS unit index mapping (title_idx → unit_key_idx) pub title_cps_unit: Vec, } /// Compute disc hash (SHA1 of Unit_Key_RO.inf content). pub fn disc_hash(data: &[u8]) -> [u8; 20] { use sha1::{Digest, Sha1}; let hash = Sha1::digest(data); let mut out = [0u8; 20]; out.copy_from_slice(&hash); out } /// Format disc hash as hex string with 0x prefix (for KEYDB lookup). pub fn disc_hash_hex(hash: &[u8; 20]) -> String { let mut s = String::with_capacity(42); s.push_str("0x"); for b in hash { s.push_str(&format!("{b:02X}")); } s } /// Parse Unit_Key_RO.inf from raw bytes. /// /// Format (from AACS spec): /// [0..4] BE32: offset to key storage area (uk_pos) /// [16] app_type (1 = BD-ROM) /// [17] num_bdmv_dir /// [18] bit 7: use_skb_mkb /// [20..22] BE16: first_play CPS unit /// [22..24] BE16: top_menu CPS unit /// [24..26] BE16: num_titles /// [26..] title entries: 2 bytes padding + 2 bytes CPS unit, × num_titles /// /// Key storage at uk_pos: /// [uk_pos..uk_pos+2] BE16: num_unit_keys /// [uk_pos+48..] encrypted keys, 16 bytes each /// AACS 1.0: 48-byte stride /// AACS 2.0 / 2.1: 64-byte stride (48 + 16 extra) pub fn parse_unit_key_ro(data: &[u8], version: AacsVersion) -> Option { if data.len() < 20 { return None; } let hash = disc_hash(data); // Header let app_type = data[16]; let num_bdmv_dir = data[17]; let use_skb_mkb = (data[18] >> 7) & 1 == 1; // Key storage offset let uk_pos = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; if uk_pos + 2 > data.len() { return None; } // Number of unit keys let num_uk = u16::from_be_bytes([data[uk_pos], data[uk_pos + 1]]) as usize; if num_uk == 0 { return Some(UnitKeyFile { disc_hash: hash, app_type, num_bdmv_dir, use_skb_mkb, version, encrypted_keys: Vec::new(), title_cps_unit: Vec::new(), }); } // Stride between keys let stride = version.unit_key_stride(); // Validate size let keys_start = uk_pos + 48; // first key at uk_pos + 48 if keys_start + 16 > data.len() { return None; } // Extract encrypted keys let mut encrypted_keys = Vec::with_capacity(num_uk); let mut pos = keys_start; for i in 0..num_uk { if pos + 16 > data.len() { break; } let mut key = [0u8; 16]; key.copy_from_slice(&data[pos..pos + 16]); encrypted_keys.push(((i + 1) as u32, key)); pos += stride; } // Title → CPS unit mapping let mut title_cps_unit = Vec::new(); if data.len() >= 26 { let first_play = u16::from_be_bytes([data[20], data[21]]); let top_menu = u16::from_be_bytes([data[22], data[23]]); let num_titles = u16::from_be_bytes([data[24], data[25]]) as usize; title_cps_unit.push(first_play); title_cps_unit.push(top_menu); for i in 0..num_titles { let off = 26 + i * 4 + 2; // 2 bytes padding + 2 bytes CPS unit if off + 2 <= data.len() { let cps = u16::from_be_bytes([data[off], data[off + 1]]); title_cps_unit.push(cps); } } } Some(UnitKeyFile { disc_hash: hash, app_type, num_bdmv_dir, use_skb_mkb, version, encrypted_keys, title_cps_unit, }) } // ── MKB processing ────────────────────────────────────────────────────────── /// Derive Media Key from MKB data using processing keys. /// /// Processing keys are pre-computed keys that work for specific MKB versions. /// This is the fast path — no subset-difference tree traversal needed. /// /// MKB format: /// Record type 0x10 = Type and Version Record (has MKB version) /// Record type 0x81 = Verify Media Key Record, AACS 1.0 (has mk_dv) /// Record type 0x86 = Verify Media Key Record, AACS 2.0/2.1 (has mk_dv) /// Record type 0x04 = Subset-Difference Index (has UVS entries) /// Record type 0x07 = Explicit Subset-Difference Record (has cvalues) pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> { // Parse MKB records let mk_dv = mkb_find_mk_dv(mkb)?; let uvs = mkb_find_subdiff_records(mkb)?; let cvalues = mkb_find_cvalues(mkb)?; // Count UV entries (each 5 bytes, stop when high bits set) let num_uvs = uvs .chunks(5) .take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0) .count(); // Try each processing key against each UV/cvalue pair for pk in processing_keys { for i in 0..num_uvs { if (i + 1) * 16 > cvalues.len() { continue; } let record_start = i * 5; if record_start + 5 > uvs.len() { continue; } let _u_mask_shift = uvs[record_start]; let uv = &uvs[record_start + 1..record_start + 5]; let cv = &cvalues[i * 16..(i + 1) * 16]; if let Some(mk) = validate_processing_key(pk, cv, uv, &mk_dv) { return Some(mk); } } } None } /// Validate a processing key against a cvalue/UV pair. /// Returns the Media Key if valid. /// /// Steps: /// 1. `mk = AES-128D(pk, cvalue)` /// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) /// 3. `dec_vd = AES-128D(mk, mk_dv)` /// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. fn validate_processing_key( pk: &[u8; 16], cvalue: &[u8], uv: &[u8], mk_dv: &[u8; 16], ) -> Option<[u8; 16]> { if cvalue.len() < 16 || uv.len() < 4 { return None; } // Step 1: mk = AES-128D(pk, cvalue) let mut cv = [0u8; 16]; cv.copy_from_slice(&cvalue[..16]); let mut mk = aes_ecb_decrypt(pk, &cv); // Step 2: XOR uv into the last 4 bytes of mk (mk[12..16]). for a in 0..4 { mk[12 + a] ^= uv[a]; } // Step 3 + 4: dec_vd = AES-128D(mk, mk_dv); verify magic. let dec_vd = aes_ecb_decrypt(&mk, mk_dv); const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]; if dec_vd[..8] == VERIFY_MAGIC { return Some(mk); } None } /// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB. fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { let mut pos = 0; let mut verify_rec_seen: Vec<(u8, usize, usize)> = Vec::new(); while pos + 4 <= mkb.len() { let rec_type = mkb[pos]; let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize; if rec_len < 4 || pos + rec_len > mkb.len() { break; } if rec_type == 0x81 || rec_type == 0x86 { verify_rec_seen.push((rec_type, pos, rec_len)); } if (rec_type == 0x81 || rec_type == 0x86) && rec_len >= 20 { // mk_dv is at offset 4 of the record (after the 4-byte header) let mut dv = [0u8; 16]; dv.copy_from_slice(&mkb[pos + 4..pos + 20]); tracing::warn!( target: "freemkv::disc", phase = "mkb_mk_dv_found", rec_type, pos, rec_len, "mk_dv extracted from MKB" ); return Some(dv); } pos += rec_len; } tracing::warn!( target: "freemkv::disc", phase = "mkb_mk_dv_not_found", verify_rec_seen = ?verify_rec_seen, scanned_bytes = pos, "no 0x81/0x86 record with rec_len>=20 found" ); None } /// Find Subset-Difference records (type 0x04) in MKB. fn mkb_find_subdiff_records(mkb: &[u8]) -> Option> { let mut pos = 0; while pos + 4 <= mkb.len() { let rec_type = mkb[pos]; let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize; if rec_len < 4 || pos + rec_len > mkb.len() { break; } if rec_type == 0x04 && rec_len > 4 { return Some(mkb[pos + 4..pos + rec_len].to_vec()); } pos += rec_len; } None } /// Find the Media Key Data Record (cvalues table) in an MKB. /// /// libaacs hard-codes record type `0x05` (matches AACS 1.0 and BD type-3/4 /// MKBs), but on AACS 2.x Category-C MKBs the cvalues table moved to /// record type `0x07` and `0x05` now carries the host-revocation /// signature. To stay correct on both lines we prefer `0x07` first (the /// AACS 2.x layout used by every modern UHD disc) and fall back to /// `0x05` for AACS 1.0 MKBs. fn mkb_find_cvalues(mkb: &[u8]) -> Option> { if let Some(body) = find_record_body(mkb, 0x07) { return Some(body); } find_record_body(mkb, 0x05) } /// Walk an MKB and return the payload (header stripped) of the first /// record matching `rec_type`. Returns `None` if no such record exists or /// the record is empty. fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option> { let mut pos = 0; while pos + 4 <= mkb.len() { let rec_type = mkb[pos]; let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize; if rec_len < 4 || pos + rec_len > mkb.len() { break; } if rec_type == rec_type_wanted && rec_len > 4 { return Some(mkb[pos + 4..pos + rec_len].to_vec()); } if rec_len == 0 { break; } pos += rec_len; } None } /// Get MKB version from Type and Version Record (type 0x10). /// Version is a BE u32 at offset 8 of the record body (offset 12 from `pos`). pub fn mkb_version(mkb: &[u8]) -> Option { let mut pos = 0; while pos + 4 <= mkb.len() { let rec_type = mkb[pos]; let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize; if rec_len < 4 || pos + rec_len > mkb.len() { break; } if rec_type == 0x10 && rec_len >= 12 { return Some(u32::from_be_bytes([ mkb[pos + 8], mkb[pos + 9], mkb[pos + 10], mkb[pos + 11], ])); } pos += rec_len; } None } // ── AACS-G3 key derivation (subset-difference tree) ───────────────────────── /// AACS-G3 seed constant. const AESG3_SEED: [u8; 16] = [ 0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9, ]; /// AACS-G3: derive a subkey from a parent key. /// seed[15] += inc, then AES-DEC(key, seed) XOR seed. fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] { let mut seed = AESG3_SEED; seed[15] = seed[15].wrapping_add(inc); let mut out = aes_ecb_decrypt(key, &seed); for i in 0..16 { out[i] ^= seed[i]; } out } /// Compute v_mask from a UV value. fn calc_v_mask(uv: u32) -> u32 { let mut v_mask: u32 = 0xFFFF_FFFF; while (uv & !v_mask) == 0 && v_mask != 0 { v_mask <<= 1; } v_mask } /// Derive processing key from device key using subset-difference tree traversal. fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] { // Initial derivation: left_child = aesg3(dk, 0), pk = aesg3(dk, 1), right_child = aesg3(dk, 2) let mut left_child = aesg3(dk, 0); let mut pk = aesg3(dk, 1); let mut right_child = aesg3(dk, 2); let mut current_v_mask = dev_key_v_mask; while current_v_mask != v_mask { // Find the highest unset bit in current_v_mask let mut bit_pos: i32 = -1; for i in (0..32).rev() { if (current_v_mask & (1u32 << i)) == 0 { bit_pos = i; break; } } let curr_key = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 { left_child } else { right_child }; left_child = aesg3(&curr_key, 0); pk = aesg3(&curr_key, 1); right_child = aesg3(&curr_key, 2); current_v_mask = ((current_v_mask as i32) >> 1) as u32; } pk } /// Derive Media Key from MKB using device keys (subset-difference tree). pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> { let mk_dv = mkb_find_mk_dv(mkb)?; let uvs = mkb_find_subdiff_records(mkb)?; let cvalues = mkb_find_cvalues(mkb)?; // Count UV entries let num_uvs = uvs .chunks(5) .take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0) .count(); for dk in device_keys { let device_number = dk.node as u32; // Find applying subset-difference for this device for uvs_idx in 0..num_uvs { let p_uv = &uvs[1 + 5 * uvs_idx..]; let u_mask_shift = uvs[5 * uvs_idx]; // byte before the UV value if u_mask_shift & 0xC0 != 0 { break; // device revoked } let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]); if uv == 0 { continue; } let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift; let v_mask = calc_v_mask(uv); if ((device_number & u_mask) == (uv & u_mask)) && ((device_number & v_mask) != (uv & v_mask)) { // Found matching subset-difference — find the right device key let dev_key_v_mask = calc_v_mask(dk.uv); let dev_key_u_mask: u32 = 0xFFFF_FFFF << dk.u_mask_shift; if u_mask == dev_key_u_mask && (uv & dev_key_v_mask) == (dk.uv & dev_key_v_mask) { // Derive processing key via tree traversal let pk = calc_pk_from_dk(&dk.key, uv, v_mask, dev_key_v_mask); // Validate and derive media key if uvs_idx < cvalues.len() / 16 { let cv = &cvalues[uvs_idx * 16..(uvs_idx + 1) * 16]; if let Some(mk) = validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv) { return Some(mk); } } } } } } None } /// MKB disc structure format code. const MKB_DISC_STRUCTURE_FORMAT: u8 = 0x83; /// MKB pack buffer size. const MKB_PACK_SIZE: usize = 32772; /// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83). /// Returns the concatenated MKB data from all packs. pub fn read_mkb_from_drive(session: &mut crate::drive::Drive) -> crate::error::Result> { use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE}; let cdb = [ SCSI_READ_DISC_STRUCTURE, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, MKB_DISC_STRUCTURE_FORMAT, (MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8, 0x00, 0x00, ]; let mut buf = vec![0u8; 32772]; session.scsi_execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000)?; let data_len = u16::from_be_bytes([buf[0], buf[1]]) as usize; if data_len < 2 { return Ok(Vec::new()); } let len = data_len - 2; let num_packs = buf[3] as usize; let mut mkb = Vec::with_capacity(32768 * num_packs.max(1)); if len > 0 && len <= 32768 { mkb.extend_from_slice(&buf[4..4 + len]); } // Read remaining packs for pack in 1..num_packs { let mut cdb = [ SCSI_READ_DISC_STRUCTURE, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, MKB_DISC_STRUCTURE_FORMAT, (MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8, 0x00, 0x00, ]; // Pack number goes in address field cdb[2] = ((pack >> 24) & 0xFF) as u8; cdb[3] = ((pack >> 16) & 0xFF) as u8; cdb[4] = ((pack >> 8) & 0xFF) as u8; cdb[5] = (pack & 0xFF) as u8; let mut buf = vec![0u8; 32772]; if session .scsi_execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000) .is_ok() { let len = u16::from_be_bytes([buf[0], buf[1]]) as usize; if len > 2 && len - 2 <= 32768 { mkb.extend_from_slice(&buf[4..4 + len - 2]); } } } Ok(mkb) } // ── Content Certificate parsing ───────────────────────────────────────────── /// AACS Content Certificate — identifies disc AACS version and features. #[derive(Debug)] pub struct ContentCert { /// Bus encryption enabled flag pub bus_encryption: bool, /// Content Certificate ID (6 bytes) pub cc_id: [u8; 6], /// AACS generation indicated by the certificate type byte. /// /// Cert type `0x00` → [`AacsVersion::V10`]; any other value → /// [`AacsVersion::V20`]. The certificate alone cannot distinguish /// V20 from V21 — Variant detection happens after the MKB walk. pub version: AacsVersion, } /// Parse a Content Certificate (ContentXXX.cer) file. pub fn parse_content_cert(data: &[u8]) -> Option { if data.len() < 8 { return None; } // Content Certificate format: // [0] certificate type (0x00 = AACS1, 0x01 = AACS2) // [1] bus_encryption_enabled (bit 0) // [2..8] cc_id (6 bytes) let version = if data[0] == 0x00 { AacsVersion::V10 } else { AacsVersion::V20 }; let bus_encryption = (data[1] & 0x01) != 0; let mut cc_id = [0u8; 6]; cc_id.copy_from_slice(&data[2..8]); Some(ContentCert { bus_encryption, cc_id, version, }) } // ── Full VUK resolution chain ─────────────────────────────────────────────── /// Result of resolving a disc's VUK. #[derive(Debug)] pub struct ResolvedKeys { /// Disc hash (SHA1 of Unit_Key_RO.inf) pub disc_hash: [u8; 20], /// Volume Unique Key pub vuk: [u8; 16], /// Decrypted unit keys (CPS unit number, key) pub unit_keys: Vec<(u32, [u8; 16])>, /// Title → CPS unit index mapping pub title_cps_unit: Vec, /// AACS generation that drove the resolution pub version: AacsVersion, /// Whether bus encryption is enabled (from Content Certificate) pub bus_encryption: bool, /// Which resolution path succeeded (1=KEYDB, 2=KEYDB derived, 3=PK, 4=DK) pub key_source: u8, } /// Inputs shared by every classical-path resolver. References only — /// callers retain ownership of all buffers. pub struct ResolveContext<'a> { /// `Unit_Key_RO.inf` raw bytes. pub unit_key_ro: &'a [u8], /// Content Certificate raw bytes (optional — used for bus-encryption flag). pub content_cert: Option<&'a [u8]>, /// 16-byte Volume ID from SCSI handshake. `[0u8; 16]` is the /// "no VID" sentinel and disables paths 2/3/4. pub volume_id: &'a [u8; 16], /// Key database. pub keydb: &'a KeyDb, /// MKB raw bytes (optional — paths 3/4 require it). pub mkb: Option<&'a [u8]>, } /// AACS 1.0 key resolution. Parses `Unit_Key_RO.inf` with 48-byte /// stride. Tries paths 1 → 4 in order. pub fn resolve_keys_v1(ctx: &ResolveContext<'_>) -> Option { resolve_keys_classical(ctx, AacsVersion::V10) } /// AACS 2.0 key resolution. Parses `Unit_Key_RO.inf` with 64-byte /// stride. Tries paths 1 → 4 in order. When paths 3/4 succeed against /// an MKB carrying Variant records (`0x82` / `0x83`), the result's /// `version` is upgraded to [`AacsVersion::V21`] — derivation still /// runs through the classical V2 path; the V21-specific Variant chain /// is wired separately via [`resolve_keys_v21`]. pub fn resolve_keys_v2(ctx: &ResolveContext<'_>) -> Option { let mut resolved = resolve_keys_classical(ctx, AacsVersion::V20)?; if let Some(mkb) = ctx.mkb { let recs = super::variants::walk_mkb(mkb); if super::variants::is_variant_mkb(&recs) { resolved.version = AacsVersion::V21; } } Some(resolved) } /// AACS 2.1 key resolution via the Media Key Variant chain. /// /// This is wired but not reachable from the production dispatcher — the /// Variant chain still requires an integrator-supplied Key Correction /// Data constant (see [`super::variants::KEY_CORRECTION_DATA_PLACEHOLDER`]) /// and an empirically-validated `VARIANTS[uv]` table. Until both are /// available, [`super::variants::derive_media_key_variant`] returns /// errors that this wrapper logs and converts to `None`. /// /// The chain still passes the disc hash → KEYDB path (1) and the /// KEYDB-derived MK+VID path (2) before attempting variant derivation; /// V21 discs already in the keydb behave identically to V20. pub fn resolve_keys_v21(ctx: &ResolveContext<'_>) -> Option { // Paths 1 and 2 are version-agnostic — try them first via the // classical V20-stride parser. let uk_file = parse_unit_key_ro(ctx.unit_key_ro, AacsVersion::V20)?; let hash_hex = disc_hash_hex(&uk_file.disc_hash); let bus_encryption = ctx .content_cert .and_then(parse_content_cert) .map(|cc| cc.bus_encryption) .unwrap_or(false); let build = |vuk: [u8; 16], key_source: u8| -> ResolvedKeys { let unit_keys: Vec<(u32, [u8; 16])> = uk_file .encrypted_keys .iter() .map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key))) .collect(); ResolvedKeys { disc_hash: uk_file.disc_hash, vuk, unit_keys, title_cps_unit: uk_file.title_cps_unit.clone(), version: AacsVersion::V21, bus_encryption, key_source, } }; tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_v21_start", bus_encryption, disc_hash = %hash_hex, mkb_present = ctx.mkb.is_some(), "resolve_keys_v21: starting" ); if let Some(entry) = ctx.keydb.find_disc(&hash_hex) { if let Some(vuk) = entry.vuk { return Some(build(vuk, 1)); } } if *ctx.volume_id == [0u8; 16] { tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_v21_no_vid", "VID unavailable; v21 derivation requires VID" ); return None; } for entry in ctx.keydb.disc_entries.values() { if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { if did == *ctx.volume_id { return Some(build(derive_vuk(&mk, ctx.volume_id), 2)); } } } // Variant chain — walk MKB, derive Km via the Media Key Variant // chain, then derive VUK off Km and the disc's VID. let mkb = ctx.mkb?; let recs = super::variants::walk_mkb(mkb); match super::variants::derive_media_key_variant( &recs, &ctx.keydb.device_keys, &super::variants::KEY_CORRECTION_DATA_PLACEHOLDER, ctx.volume_id, ) { Ok((_km, kvu)) => { tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_v21_variant_ok", "Media Key Variant chain produced Km + Kvu" ); Some(build(kvu, 4)) } Err(e) => { tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_v21_variant_err", error_code = %e, "Media Key Variant chain failed" ); None } } } /// Resolve all AACS keys for a disc using the classical (single-stage /// Media Key derivation) paths. Used by both V10 and V20. /// /// Tries in order: /// 1. Disc hash → KEYDB → VUK (fast path, no VID required) /// 2. KEYDB media key + volume ID → VUK /// 3. MKB + processing keys → media key → VUK /// 4. MKB + device keys → processing key → media key → VUK fn resolve_keys_classical(ctx: &ResolveContext<'_>, version: AacsVersion) -> Option { let bus_encryption = ctx .content_cert .and_then(parse_content_cert) .map(|cc| cc.bus_encryption) .unwrap_or(false); // Parse Unit_Key_RO.inf at the version-appropriate stride. let uk_file = parse_unit_key_ro(ctx.unit_key_ro, version)?; let hash_hex = disc_hash_hex(&uk_file.disc_hash); // Helper to build result let build = |vuk: [u8; 16], key_source: u8| -> ResolvedKeys { let unit_keys: Vec<(u32, [u8; 16])> = uk_file .encrypted_keys .iter() .map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key))) .collect(); ResolvedKeys { disc_hash: uk_file.disc_hash, vuk, unit_keys, title_cps_unit: uk_file.title_cps_unit.clone(), version, bus_encryption, key_source, } }; tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_start", version = ?version, bus_encryption, disc_hash = %hash_hex, mkb_present = ctx.mkb.is_some(), "resolve_keys: starting" ); // Path 1: Look up VUK by disc hash in KEYDB if let Some(entry) = ctx.keydb.find_disc(&hash_hex) { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_hit_entry", "disc hash found in keydb"); if let Some(vuk) = entry.vuk { return Some(build(vuk, 1)); } tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_no_vuk", "disc hash entry has no VUK"); } else { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_miss", "disc hash NOT in keydb"); } // Paths 2-4 all consume the Volume ID. Without it (handshake // skipped, raw-read bypass failed, etc.) every downstream // derivation produces garbage. Caller stamps `[0u8; 16]` as the // sentinel "no VID" — short-circuit here so we don't surface a // misleading "all paths failed" log when really the math is // structurally impossible. if *ctx.volume_id == [0u8; 16] { tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_no_vid", "VID unavailable; paths 2/3/4 require VID and are skipped" ); return None; } // Path 2: Find entry with matching VID → derive VUK from MK + VID let mut path2_mk_did_count = 0usize; for entry in ctx.keydb.disc_entries.values() { if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { path2_mk_did_count += 1; if did == *ctx.volume_id { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_hit", "MK+VID entry matched volume_id"); return Some(build(derive_vuk(&mk, ctx.volume_id), 2)); } } } tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_miss", mk_did_entries = path2_mk_did_count, "no MK+VID entry matched volume_id"); // Path 3: MKB + processing keys → media key → VUK if let Some(mkb) = ctx.mkb { let mk_dv = mkb_find_mk_dv(mkb); let subdiff = mkb_find_subdiff_records(mkb); let cvalues = mkb_find_cvalues(mkb); tracing::warn!( target: "freemkv::disc", phase = "resolve_keys_mkb_records", mk_dv_found = mk_dv.is_some(), subdiff_found = subdiff.is_some(), subdiff_len = subdiff.as_ref().map(|s| s.len()).unwrap_or(0), cvalues_found = cvalues.is_some(), cvalues_len = cvalues.as_ref().map(|c| c.len()).unwrap_or(0), "MKB record scan results" ); if let Some(mk) = derive_media_key_from_pk(mkb, &ctx.keydb.processing_keys) { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path3_hit", "media key derived from processing key"); return Some(build(derive_vuk(&mk, ctx.volume_id), 3)); } tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path3_miss", pk_count = ctx.keydb.processing_keys.len(), "PK derivation failed"); // Path 4: MKB + device keys → processing key → media key → VUK if let Some(mk) = derive_media_key_from_dk(mkb, &ctx.keydb.device_keys) { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path4_hit", "media key derived from device key"); return Some(build(derive_vuk(&mk, ctx.volume_id), 4)); } tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path4_miss", dk_count = ctx.keydb.device_keys.len(), "DK derivation failed"); } else { tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_no_mkb", "no MKB data available; paths 3/4 skipped"); } None } #[cfg(test)] mod tests { use super::super::decrypt::{ALIGNED_UNIT_LEN, aes_ecb_encrypt}; use super::super::keydb::{DiscEntry, KeyDb}; use super::*; /// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found. fn keydb_path() -> Option { let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?); if path.exists() { Some(path) } else { None } } #[test] fn test_vuk_derivation() { // Civil War UHD: known MK, VID, VUK from KEYDB // MK = 15665F98..., VID (disc_id) = from entry, VUK = F96D7908... // VUK = AES-DEC(MK, VID) XOR VID let path = match keydb_path() { Some(p) => p, None => return, }; let db = KeyDb::load(&path).unwrap(); // Find a disc with both MK, disc_id, and VUK so we can verify derivation let entry = db .disc_entries .values() .find(|e| e.media_key.is_some() && e.disc_id.is_some() && e.vuk.is_some()) .expect("No disc with MK + VID + VUK"); let mk = entry.media_key.unwrap(); let vid = entry.disc_id.unwrap(); let expected_vuk = entry.vuk.unwrap(); let derived = derive_vuk(&mk, &vid); assert_eq!( derived, expected_vuk, "VUK derivation failed for disc: {} (hash {})", entry.title, entry.disc_hash ); eprintln!("VUK derivation verified for: {}", entry.title); } #[test] fn test_decrypt_unit_key_from_vuk() { // Test the full chain: VUK → decrypt encrypted unit key → unit key // Use a known disc from KEYDB that has both VUK and unit keys let path = match keydb_path() { Some(p) => p, None => return, }; let db = KeyDb::load(&path).unwrap(); // Find a disc with VUK and unit keys let entry = db .disc_entries .values() .find(|e| e.vuk.is_some() && !e.unit_keys.is_empty()) .expect("No disc with VUK + unit keys"); eprintln!( "Testing unit key decrypt for: {} ({})", entry.title, entry.disc_hash ); eprintln!(" VUK: {:02X?}", entry.vuk.unwrap()); for (num, key) in &entry.unit_keys { eprintln!(" Unit key {}: {:02X?}", num, key); } // The unit keys in KEYDB are already decrypted — we can verify the chain // by encrypting with VUK and then decrypting let vuk = entry.vuk.unwrap(); for (num, expected_uk) in &entry.unit_keys { let encrypted = aes_ecb_encrypt(&vuk, expected_uk); let decrypted = decrypt_unit_key(&vuk, &encrypted); assert_eq!( &decrypted, expected_uk, "Unit key {} roundtrip failed for {}", num, entry.title ); } eprintln!(" All {} unit key roundtrips passed", entry.unit_keys.len()); } #[test] fn test_decrypt_real_unit() { // Try decrypting a real encrypted aligned unit from Civil War UHD // This disc is AACS 2.0 (BEE) so unit key alone won't work — // we need bus decryption first. But this verifies the pipeline. let unit_path = std::path::Path::new("/tmp/encrypted_unit.bin"); if !unit_path.exists() { return; } let original = std::fs::read(unit_path).unwrap(); assert_eq!(original.len(), ALIGNED_UNIT_LEN); assert!( super::super::decrypt::is_unit_encrypted(&original), "Unit should be encrypted" ); let kp = match keydb_path() { Some(p) => p, None => return, }; let db = KeyDb::load(&kp).unwrap(); // Civil War UHD entries let civil_war_entries: Vec<&DiscEntry> = db .disc_entries .values() .filter(|e| e.title.contains("CIVIL WAR") && !e.unit_keys.is_empty()) .collect(); eprintln!( "Found {} Civil War entries with unit keys", civil_war_entries.len() ); // Try each entry's unit keys for entry in &civil_war_entries { let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect(); let mut unit = original.clone(); if let Some(idx) = super::super::decrypt::decrypt_unit_try_keys(&mut unit, &keys) { eprintln!( "SUCCESS: Decrypted with entry {} key {}", entry.disc_hash, idx ); // Count TS sync bytes let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count(); eprintln!(" TS sync bytes: {}/32", ts); return; } } // Expected: none work because this is AACS 2.0 and needs bus decryption first eprintln!("No unit key worked (expected for AACS 2.0 BEE disc — needs read_data_key)"); } #[test] fn test_disc_hash() { // SHA1 of a known byte sequence let data = b"test unit key ro inf data"; let hash = disc_hash(data); assert_ne!(hash, [0u8; 20]); // Same input → same hash assert_eq!(hash, disc_hash(data)); } #[test] fn test_disc_hash_hex() { let hash = [ ***REMOVED***, ]; let hex = disc_hash_hex(&hash); assert_eq!(hex, "***REMOVED***"); } #[test] fn test_parse_unit_key_ro_synthetic() { // Build a synthetic Unit_Key_RO.inf // Header: uk_pos at offset 0 (BE32), points to key storage // Keys at uk_pos + 48 (16 bytes each, 48-byte stride for AACS 1.0) let mut data = vec![0u8; 256]; // uk_pos = 0x60 (96) data[0] = 0x00; data[1] = 0x00; data[2] = 0x00; data[3] = 0x60; // Header fields at 16-18 data[16] = 1; // app_type = BD-ROM data[17] = 1; // num_bdmv_dir data[18] = 0; // no SKB // Title mapping at 20-25 data[20] = 0; data[21] = 1; // first_play = CPS unit 1 data[22] = 0; data[23] = 1; // top_menu = CPS unit 1 data[24] = 0; data[25] = 1; // num_titles = 1 // Title 0 entry: 2 bytes pad + CPS unit data[28] = 0; data[29] = 1; // CPS unit 1 // Key storage at offset 0x60 let uk_pos = 0x60usize; data[uk_pos] = 0; data[uk_pos + 1] = 2; // 2 unit keys // Key 1 at uk_pos + 48 let key1_pos = uk_pos + 48; for i in 0..16 { data[key1_pos + i] = 0xAA; } // Key 2 at uk_pos + 48 + 48 let key2_pos = key1_pos + 48; for i in 0..16 { data[key2_pos + i] = 0xBB; } let parsed = parse_unit_key_ro(&data, AacsVersion::V10).unwrap(); assert_eq!(parsed.app_type, 1); assert_eq!(parsed.num_bdmv_dir, 1); assert_eq!(parsed.version, AacsVersion::V10); assert_eq!(parsed.encrypted_keys.len(), 2); assert_eq!(parsed.encrypted_keys[0].0, 1); // CPS unit 1 assert_eq!(parsed.encrypted_keys[0].1, [0xAA; 16]); assert_eq!(parsed.encrypted_keys[1].0, 2); // CPS unit 2 assert_eq!(parsed.encrypted_keys[1].1, [0xBB; 16]); } #[test] fn mkb_version_recognizes_type_0x10() { // Type-and-Version record: type=0x10, rec_len=12 (BE24). // Body is 8 bytes; the version u32 sits at offset 8 of the record. let mkb = [ 0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D, ]; assert_eq!(mkb_version(&mkb), Some(77)); } #[test] fn mkb_version_returns_none_on_empty() { assert_eq!(mkb_version(&[]), None); assert_eq!(mkb_version(&[0x10, 0x00]), None); // Type 0x10 record but rec_len < 12 → no version available. let short = [0x10, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01]; assert_eq!(mkb_version(&short), None); } #[test] fn mkb_find_mk_dv_recognizes_type_0x81() { // First: type-0x10 type/version record (12 bytes), then type-0x81 verify record. // Verify record carries a known 16-byte mk_dv at offset 4 of the record body. let expected: [u8; 16] = [ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, ]; let mut mkb = vec![ 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ]; // type=0x81, rec_len=24 (4-byte header + 16-byte mk_dv + 4-byte trailing zeros) mkb.extend_from_slice(&[0x81, 0x00, 0x00, 0x18]); mkb.extend_from_slice(&expected); mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert_eq!(mkb_find_mk_dv(&mkb), Some(expected)); } #[test] fn validate_processing_key_round_trip_with_nonzero_uv() { // Synthesise a (pk, uv, mk, cvalue, mk_dv) tuple that satisfies the // libaacs _validate_pk relation, then confirm validate_processing_key // recovers mk. Catches the bugs that landed pre-fix: // * uv XOR step was missing → mk wrong whenever uv != 0 // * AES-128E + 12-zero check instead of AES-128D + magic use super::super::decrypt::{aes_ecb_decrypt as dec, aes_ecb_encrypt as enc}; let pk: [u8; 16] = [ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, ]; let mk: [u8; 16] = [ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, ]; let uv: [u8; 4] = [0x00, 0x00, 0x04, 0x00]; // cvalue is what AES-128E(pk, mk') gives, where mk' = mk with the // last-4-bytes-uv XOR pre-undone: // mk_raw[12..16] = mk[12..16] XOR uv (so the validate step XORs // uv back in and recovers mk). let mut mk_raw = mk; for a in 0..4 { mk_raw[12 + a] ^= uv[a]; } let cvalue = enc(&pk, &mk_raw); // mk_dv is the encryption (under the correct mk) of the verify // magic, padded with arbitrary bytes — when decrypted with mk we // recover the magic. let mut plaintext_vd = [0u8; 16]; plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); // Trailing 8 bytes are don't-cares in the magic check. plaintext_vd[8..].copy_from_slice(&[0x11; 8]); let mk_dv = enc(&mk, &plaintext_vd); // Sanity: decrypting mk_dv with mk yields the magic. let _check = dec(&mk, &mk_dv); let recovered = validate_processing_key(&pk, &cvalue, &uv, &mk_dv) .expect("validate_processing_key must accept a correct pk + uv pair"); assert_eq!(recovered, mk, "recovered mk must match the planted mk"); // And a wrong pk must be rejected. let mut wrong_pk = pk; wrong_pk[0] ^= 0xFF; assert!(validate_processing_key(&wrong_pk, &cvalue, &uv, &mk_dv).is_none()); // And a uv mismatch must be rejected. let wrong_uv = [0x00u8, 0x00, 0x00, 0x00]; assert!(validate_processing_key(&pk, &cvalue, &wrong_uv, &mk_dv).is_none()); } #[test] fn mkb_find_cvalues_prefers_0x07_then_falls_back_to_0x05() { // AACS 2.x: type 0x07 carries cvalues; 0x05 is the host-revocation // signature. Mixed-record MKB → 0x07 wins. let mut mkb = vec![ 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, ]; // type=0x05, body = [0xAA; 4] mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 0xAA, 0xAA, 0xAA, 0xAA]); // type=0x07, body = [0xBB; 4] mkb.extend_from_slice(&[0x07, 0x00, 0x00, 0x08, 0xBB, 0xBB, 0xBB, 0xBB]); let body = mkb_find_cvalues(&mkb).expect("cvalues record must be found"); assert_eq!(body, vec![0xBB, 0xBB, 0xBB, 0xBB], "0x07 must be preferred"); // AACS 1.0: only 0x05 present → fall back to it. let mut mkb1 = vec![ 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ]; mkb1.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 0xCC, 0xCC, 0xCC, 0xCC]); let body = mkb_find_cvalues(&mkb1).expect("0x05 fallback must work for AACS 1.0"); assert_eq!(body, vec![0xCC, 0xCC, 0xCC, 0xCC]); } #[test] fn mkb_find_mk_dv_recognizes_type_0x86() { // AACS 2.0 form uses type 0x86 for the verify record. let expected: [u8; 16] = [ 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, ]; let mut mkb = vec![ 0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, ]; mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]); mkb.extend_from_slice(&expected); mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert_eq!(mkb_find_mk_dv(&mkb), Some(expected)); } #[test] fn test_resolve_keys_vuk_path() { // Test the full resolve chain using VUK path let path = match keydb_path() { Some(p) => p, None => return, }; let db = KeyDb::load(&path).unwrap(); // Find V for Vendetta BD — has VUK and unit keys // hash: ***REMOVED*** let entry = db.find_disc("***REMOVED***"); if entry.is_none() { return; } let entry = entry.unwrap(); let vuk = entry.vuk.unwrap(); let vid = entry.disc_id.unwrap(); // We need the actual Unit_Key_RO.inf from the disc to compute disc hash. // Since we don't have it, we can at least test that the KEYDB lookup // works with a known hash. let hash_hex = "***REMOVED***"; let found = db.find_disc(hash_hex); assert!(found.is_some()); assert_eq!(found.unwrap().vuk, Some(vuk)); // Verify VUK derivation if we have MK + VID if let Some(mk) = entry.media_key { let derived = derive_vuk(&mk, &vid); assert_eq!(derived, vuk, "VUK derivation mismatch for V for Vendetta"); eprintln!("V for Vendetta VUK derivation verified"); } } /// Build a minimal Unit_Key_RO.inf with `num_unit_keys = 1`. The /// disc hash won't be in any synthetic keydb so path 1 misses, /// which lets us isolate the path-2/3/4 short-circuit behavior. fn minimal_unit_key_ro() -> Vec { let mut data = vec![0u8; 256]; // uk_pos = 0x60 data[3] = 0x60; data[16] = 1; // app_type = BD-ROM data[17] = 1; // num_bdmv_dir let uk_pos = 0x60usize; data[uk_pos + 1] = 1; // 1 unit key // Key at uk_pos + 48 — value doesn't matter, just needs to fit. for i in 0..16 { data[uk_pos + 48 + i] = 0xCC; } data } #[test] fn resolve_keys_skips_paths_2_through_4_when_vid_is_zero() { // No VID -> paths 2/3/4 cannot succeed. The function must // return None WITHOUT touching the MKB / device keys, so we // can pass an MKB that would otherwise cause expensive // derivation work — it must not be consumed. let uk_ro = minimal_unit_key_ro(); let zero_vid = [0u8; 16]; // Populate keydb with a non-matching VID entry (path 2 would // miss anyway) plus dummy processing/device keys (paths 3/4 // would also miss, but the short-circuit means they're never // attempted). let mut keydb = KeyDb::empty(); keydb.disc_entries.insert( "0xDEADBEEF".to_string(), DiscEntry { disc_hash: "0xDEADBEEF".to_string(), title: "fixture".to_string(), media_key: Some([0x11u8; 16]), disc_id: Some([0x22u8; 16]), vuk: None, unit_keys: Vec::new(), }, ); keydb.processing_keys.push([0u8; 16]); let ctx = ResolveContext { unit_key_ro: &uk_ro, content_cert: None, volume_id: &zero_vid, keydb: &keydb, mkb: None, }; let result = resolve_keys_v1(&ctx); assert!( result.is_none(), "resolve_keys with VID=0 and no matching disc-hash entry must return None" ); } #[test] fn resolve_keys_path1_still_runs_when_vid_is_zero() { // Path 1 (disc-hash → VUK) doesn't need VID. Confirm the // short-circuit doesn't block it: install a keydb entry whose // disc_hash matches the fixture's hash, with a known VUK, and // verify resolve_keys returns it with key_source = 1. let uk_ro = minimal_unit_key_ro(); let hash = disc_hash(&uk_ro); // `find_disc` lowercases the incoming hash; the entry map is // keyed lowercase too, so we have to lowercase here. let hash_hex = disc_hash_hex(&hash).to_lowercase(); let mut keydb = KeyDb::empty(); let known_vuk = [0xABu8; 16]; keydb.disc_entries.insert( hash_hex.clone(), DiscEntry { disc_hash: hash_hex, title: "fixture".to_string(), media_key: None, disc_id: None, vuk: Some(known_vuk), unit_keys: Vec::new(), }, ); let vid = [0u8; 16]; let ctx = ResolveContext { unit_key_ro: &uk_ro, content_cert: None, volume_id: &vid, keydb: &keydb, mkb: None, }; let resolved = resolve_keys_v1(&ctx).expect("path 1 must run regardless of VID availability"); assert_eq!(resolved.vuk, known_vuk); assert_eq!(resolved.key_source, 1); } #[test] fn test_content_cert_parse() { // AACS 1.0 cert let mut data = vec![0u8; 16]; data[0] = 0x00; // AACS 1.0 data[1] = 0x00; // no bus encryption let cc = parse_content_cert(&data).unwrap(); assert_eq!(cc.version, AacsVersion::V10); assert!(!cc.bus_encryption); // AACS 2.0 with bus encryption data[0] = 0x01; // AACS 2.0 data[1] = 0x01; // bus encryption enabled let cc = parse_content_cert(&data).unwrap(); assert_eq!(cc.version, AacsVersion::V20); assert!(cc.bus_encryption); } }