//! AACS on-disc key-input files: `Unit_Key_RO.inf` parsing, the disc-hash //! keydb lookup key, the Content Certificate, and the in-drive MKB read. //! These turn raw disc files into the structures the key paths consume. use super::mkb::*; /// 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; } // The loop above `break`s if the buffer runs out mid-key. A short list // means the .inf is malformed/truncated — reject it rather than silently // accepting fewer keys than the header declared, which would later map // title CPS units to nonexistent keys. if encrypted_keys.len() != num_uk { return None; } // Title → CPS unit mapping. libaacs (unit_key.c) validates each on-disc CPS // value is in `1..=num_uk` (else zeroes it) and converts the 1-based on-disc // index to a 0-based key index. We mirror that so the stored value is a safe, // ready-to-use key index rather than a raw 1-based number. let to_key_idx = |cps: u16| -> u16 { if cps >= 1 && cps as usize <= num_uk { cps - 1 } else { 0 } }; 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(to_key_idx(first_play)); title_cps_unit.push(to_key_idx(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(to_key_idx(cps)); } } } Some(UnitKeyFile { disc_hash: hash, app_type, num_bdmv_dir, use_skb_mkb, version, encrypted_keys, title_cps_unit, }) } /// 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 dyn crate::scsi::ScsiTransport, ) -> 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.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 .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) } /// 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() < 20 { return None; } // Content Certificate layout (matches libaacs content_cert.c): // [0] certificate type (0x00 = AACS1, 0x10 = AACS2) // [1] bit7 bus_encryption_enabled_flag (libaacs: `p[1] >> 7`) // [14..20] cc_id (6 bytes) (libaacs: `p + 14`) let version = if data[0] == 0x00 { AacsVersion::V10 } else { AacsVersion::V20 }; // The flag is bit 7 of byte 1, NOT bit 0. Reading bit 0 (the prior bug) made // a bus-encrypted cert (byte1=0x80) read as `false`, defeating the // AacsBusKeyUnavailable fail-loud gate in disc/encrypt.rs. let bus_encryption = (data[1] >> 7) & 1 == 1; let mut cc_id = [0u8; 6]; cc_id.copy_from_slice(&data[14..20]); Some(ContentCert { bus_encryption, cc_id, version, }) }