Break the 2800-line keys.rs into four responsibility-scoped modules: - media_key.rs: DK/PK -> Media Key subset-difference walk (+ probe harness) - volume_key.rs: VUK derivation, unit-key unwrap - inf.rs: Unit_Key_RO.inf parsing, disc_hash, content cert, in-drive MKB read - resolve.rs: the resolve_keys_* orchestration (keys.rs renamed) Relocation only; the (white-box) test suite stays in resolve.rs and pulls the moved items via glob imports. Proven byte-identical to the pre-refactor state via the logic hash (95fb9924); 2210 tests green.
286 lines
8.7 KiB
Rust
286 lines
8.7 KiB
Rust
//! 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<u16>,
|
||
}
|
||
|
||
/// 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<UnitKeyFile> {
|
||
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<Vec<u8>> {
|
||
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<ContentCert> {
|
||
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,
|
||
})
|
||
}
|