aacs: split keys.rs god-module into media_key/volume_key/inf/resolve
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.
This commit is contained in:
+6
-6
@@ -26,10 +26,9 @@
|
|||||||
//! the processing-key path starts from a precomputed PK. Neither needs a VID
|
//! the processing-key path starts from a precomputed PK. Neither needs a VID
|
||||||
//! (the VID enters at `vuk_from_mk`).
|
//! (the VID enters at `vuk_from_mk`).
|
||||||
|
|
||||||
use super::keys::{
|
use super::media_key::{derive_media_key_and_pk_from_dk, derive_media_key_from_pk};
|
||||||
decrypt_unit_key, derive_media_key_and_pk_from_dk, derive_media_key_from_pk, derive_vuk,
|
|
||||||
};
|
|
||||||
use super::types::DeviceKey;
|
use super::types::DeviceKey;
|
||||||
|
use super::volume_key::{decrypt_unit_key, derive_vuk};
|
||||||
|
|
||||||
/// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path.
|
/// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -204,7 +203,8 @@ pub fn resolve_candidate(
|
|||||||
unit_key_ro: &[u8],
|
unit_key_ro: &[u8],
|
||||||
vid: Option<Vid>,
|
vid: Option<Vid>,
|
||||||
) -> Option<ResolvedChain> {
|
) -> Option<ResolvedChain> {
|
||||||
use super::keys::{derive_media_key_and_pk_from_dk, parse_unit_key_ro};
|
use super::inf::parse_unit_key_ro;
|
||||||
|
use super::media_key::derive_media_key_and_pk_from_dk;
|
||||||
use super::mkb::{AacsVersion, mkb_type};
|
use super::mkb::{AacsVersion, mkb_type};
|
||||||
|
|
||||||
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number.
|
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number.
|
||||||
@@ -219,7 +219,7 @@ pub fn resolve_candidate(
|
|||||||
if ukf.encrypted_keys.is_empty() {
|
if ukf.encrypted_keys.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(super::keys::derive_unit_keys(&ukf, &vuk.0))
|
Some(super::volume_key::derive_unit_keys(&ukf, &vuk.0))
|
||||||
};
|
};
|
||||||
|
|
||||||
match candidate {
|
match candidate {
|
||||||
@@ -277,7 +277,7 @@ pub fn resolve_candidate(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::aacs::crypto::aes_ecb_encrypt;
|
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||||
use crate::aacs::keys::{decrypt_unit_key, derive_vuk};
|
use crate::aacs::volume_key::{decrypt_unit_key, derive_vuk};
|
||||||
|
|
||||||
/// `vuk_from_mk` must equal the inline `derive_vuk` path bit-for-bit, for
|
/// `vuk_from_mk` must equal the inline `derive_vuk` path bit-for-bit, for
|
||||||
/// several known (MK, VID) vectors.
|
/// several known (MK, VID) vectors.
|
||||||
|
|||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
//! 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
//! Media-key derivation: DK/PK → Media Key via the subset-difference tree.
|
||||||
|
//! [C] §3.2.2–§3.2.5.
|
||||||
|
|
||||||
|
use super::crypto::*;
|
||||||
|
use super::mkb::*;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Derive Media Key from MKB data using processing keys.
|
||||||
|
///
|
||||||
|
/// A Processing Key is **terminal**: it is the key at its Subset-Difference
|
||||||
|
/// node, one `AES-G` from the Media Key. So this is the fast path — each PK is
|
||||||
|
/// tried *directly* against the MKB cvalue tables (no tree descent), matching
|
||||||
|
/// libaacs `_calc_mk_pks` (iterate PKs × cvalues). On a large AACS 2.x UHD MKB
|
||||||
|
/// (~181k cvalues) this is ~15x faster than treating a PK as a device-node
|
||||||
|
/// label and walking the tree.
|
||||||
|
///
|
||||||
|
/// If you hold a **device-node label** at unknown tree depth (not a terminal
|
||||||
|
/// PK), derive its Media Key through the device-key path
|
||||||
|
/// ([`derive_media_key_from_dk`]) — that path owns the Subset-Difference tree
|
||||||
|
/// walk; the PK path never descends.
|
||||||
|
///
|
||||||
|
/// 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 0x05 = Media Key Data Record (cvalues, 1:1 with 0x04)
|
||||||
|
/// Record type 0x07 = Explicit Subset-Difference Record (NOT cvalues)
|
||||||
|
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
|
||||||
|
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||||
|
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||||
|
let cvalues = mkb_find_cvalues(mkb)?;
|
||||||
|
try_pk_against_tables(processing_keys, &uvs, &cvalues, &mk_dv)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core terminal-PK table scan over explicit record bodies. Each processing
|
||||||
|
/// key is tried **directly** against every `(uv, cvalue)` pair — no tree
|
||||||
|
/// descent. Reached in production via [`derive_media_key_from_pk`]; factored
|
||||||
|
/// out so reproduction harnesses can drive it with explicit tables.
|
||||||
|
pub(crate) fn try_pk_against_tables(
|
||||||
|
processing_keys: &[[u8; 16]],
|
||||||
|
uvs: &[u8],
|
||||||
|
cvalues: &[u8],
|
||||||
|
mk_dv: &[u8; 16],
|
||||||
|
) -> Option<[u8; 16]> {
|
||||||
|
let num_uvs = uvs
|
||||||
|
.chunks(5)
|
||||||
|
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
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 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 (media key: [C] §3.2.4; verify relation: [C] §3.2.5.1.4):
|
||||||
|
/// 1. `mk = AES-128D(pk, cvalue)` [C] §3.2.4
|
||||||
|
/// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) [C] §3.2.4
|
||||||
|
/// 3. `dec_vd = AES-128D(mk, mk_dv)` [C] §3.2.5.1.4
|
||||||
|
/// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. [C] §3.2.5.1.4
|
||||||
|
pub(crate) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variants`].
|
||||||
|
pub(super) 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.
|
||||||
|
/// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). Shared with [`super::variants`].
|
||||||
|
pub(super) fn calc_pk_from_dk(
|
||||||
|
dk: &[u8; 16],
|
||||||
|
uv: u32,
|
||||||
|
v_mask: u32,
|
||||||
|
dev_key_v_mask: u32,
|
||||||
|
) -> [u8; 16] {
|
||||||
|
// Descend from the device node to the record node, following the record's
|
||||||
|
// `uv` bits. At each level only the child we descend INTO is needed (the
|
||||||
|
// sibling is computed but never used), and the Processing Key is the
|
||||||
|
// `aesg3(.,1)` of the FINAL node — so we derive ONE child per level and the
|
||||||
|
// PK once at the end, instead of left/pk/right at every level. Identical
|
||||||
|
// result, ~3x fewer block ops. (left child = `aesg3(node,0)`, right = `,2`.)
|
||||||
|
let mut node = *dk;
|
||||||
|
let mut current_v_mask = dev_key_v_mask;
|
||||||
|
|
||||||
|
// The subset-difference tree is at most 32 levels deep (u32 mask), so the
|
||||||
|
// walk must converge in <= 32 steps. The arithmetic `>> 1` sign-extends
|
||||||
|
// current_v_mask, so a v_mask coarser than dev_key_v_mask (reachable from
|
||||||
|
// a crafted/corrupt MKB) would otherwise saturate at 0xFFFF_FFFF and spin
|
||||||
|
// forever — bound the loop to keep a bad disc from hanging the rip thread.
|
||||||
|
let mut steps = 0u32;
|
||||||
|
while current_v_mask != v_mask {
|
||||||
|
if steps >= 32 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
steps += 1;
|
||||||
|
// 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 inc = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 {
|
||||||
|
0 // left child
|
||||||
|
} else {
|
||||||
|
2 // right child
|
||||||
|
};
|
||||||
|
node = aesg3(&node, inc);
|
||||||
|
|
||||||
|
current_v_mask = ((current_v_mask as i32) >> 1) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
aesg3(&node, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive Media Key from MKB using device keys (subset-difference tree).
|
||||||
|
///
|
||||||
|
/// Thin wrapper over [`derive_media_key_and_pk_from_dk`] that drops the
|
||||||
|
/// intermediate Processing Key. Callers that need the PK lineage (e.g.
|
||||||
|
/// the key service banking DK·PK·MK) should call the `_and_pk_` form.
|
||||||
|
pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> {
|
||||||
|
derive_media_key_and_pk_from_dk(mkb, device_keys).map(|(mk, _pk)| mk)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive both the Media Key and the intermediate Processing Key from an
|
||||||
|
/// MKB using device keys (subset-difference tree).
|
||||||
|
///
|
||||||
|
/// Identical walk to [`derive_media_key_from_dk`]; this form additionally
|
||||||
|
/// returns the Processing Key `Kp` derived at the matching subset-difference
|
||||||
|
/// node — the value `calc_pk_from_dk` produces immediately before it
|
||||||
|
/// validates into the Media Key. Returns `Some((mk, pk))` for the first DK
|
||||||
|
/// that walks a uv slot whose Processing Key validates against the MKB.
|
||||||
|
pub fn derive_media_key_and_pk_from_dk(
|
||||||
|
mkb: &[u8],
|
||||||
|
device_keys: &[DeviceKey],
|
||||||
|
) -> Option<([u8; 16], [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
|
||||||
|
}
|
||||||
|
// Shifts of 32..=63 (0x20..=0x3F pass the 0xC0 mask above) would
|
||||||
|
// panic in debug / wrap to a wrong mask in release. The MKB byte
|
||||||
|
// is disc-controlled, so a crafted/corrupt MKB must not crash the
|
||||||
|
// ripper: skip an out-of-range slot rather than `<<` it.
|
||||||
|
if u_mask_shift >= 32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
||||||
|
if uv == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// u-mask = shift count of low-order 0 bits ([C] §3.2.5.1.5); v-mask [C] §3.2.3.
|
||||||
|
let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift;
|
||||||
|
let v_mask = calc_v_mask(uv);
|
||||||
|
|
||||||
|
// Subset-difference applies iff (d&mu)==(uv&mu) && (d&mv)!=(uv&mv). [C] §3.2.4.
|
||||||
|
if ((device_number & u_mask) == (uv & u_mask))
|
||||||
|
&& ((device_number & v_mask) != (uv & v_mask))
|
||||||
|
{
|
||||||
|
// Found matching subset-difference — find the right device key.
|
||||||
|
// dk.u_mask_shift is a u8 from keydb with no range check;
|
||||||
|
// guard the shift the same way as the MKB byte above.
|
||||||
|
if dk.u_mask_shift >= 32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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, pk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover the subset-difference position (`node`, `uv`, `u_mask_shift`) of an
|
||||||
|
/// UNPOSITIONED device key by scanning a disc MKB. A device key alone (just the
|
||||||
|
/// 16 bytes) cannot be walked — the walk needs its tree node. This finds that
|
||||||
|
/// node empirically: for each MKB subset-difference record, it tries the device
|
||||||
|
/// at the record's node AND at every ancestor v-position (the device may sit one
|
||||||
|
/// or more levels ABOVE the record, descending via AES-G to reach it), deriving
|
||||||
|
/// the candidate Processing Key DIRECTLY (one [`calc_pk_from_dk`] per candidate,
|
||||||
|
/// no full re-walk) and checking it validates against that record's cvalue.
|
||||||
|
///
|
||||||
|
/// On the first verifying candidate it pins `(uv, u_mask_shift)` — invariant for
|
||||||
|
/// the key across all discs — and resolves a gate-passing `node` (a one-time
|
||||||
|
/// ≤32-try search at the single hit). Returns a [`DeviceKey`] ready to bank and
|
||||||
|
/// reuse on every future disc via [`derive_media_key_from_dk`]. `None` if the
|
||||||
|
/// key does not apply to this MKB.
|
||||||
|
///
|
||||||
|
/// Cost is `O(slots × tree_depth)` — linear in the MKB's subset-difference
|
||||||
|
/// index, not the quartic cost of re-deriving per candidate.
|
||||||
|
pub fn recover_dk_position(mkb: &[u8], key: &[u8; 16]) -> Option<DeviceKey> {
|
||||||
|
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||||
|
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||||
|
let cvalues = mkb_find_cvalues(mkb)?;
|
||||||
|
let num_uvs = uvs
|
||||||
|
.chunks(5)
|
||||||
|
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
||||||
|
.count();
|
||||||
|
let n_cv = cvalues.len() / 16;
|
||||||
|
|
||||||
|
// Hoisted ONCE for the whole scan: the Processing Key the device produces if
|
||||||
|
// it sits EXACTLY at a record (zero descent) is `AES-G3(key, 1)` — it does
|
||||||
|
// not depend on the record, so the zero-descent probe of every slot reuses
|
||||||
|
// this single value instead of re-deriving it per slot.
|
||||||
|
let pk_zero_descent = aesg3(key, 1);
|
||||||
|
|
||||||
|
// The slots are independent, so the scan parallelises — a UHD MKB has ~181k
|
||||||
|
// slots (~26s single-threaded). `find_map_any` returns the first matching
|
||||||
|
// node found by any thread and cancels the rest; a valid MKB has exactly one
|
||||||
|
// matching subset-difference, so which thread finds it is immaterial.
|
||||||
|
use rayon::prelude::*;
|
||||||
|
let found = (0..num_uvs.min(n_cv)).into_par_iter().find_map_any(|i| {
|
||||||
|
let u_mask_shift = uvs[5 * i];
|
||||||
|
if u_mask_shift >= 32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let p_uv = &uvs[1 + 5 * i..];
|
||||||
|
let uv_r = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
||||||
|
if uv_r == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let v_mask = calc_v_mask(uv_r);
|
||||||
|
let cv = &cvalues[i * 16..(i + 1) * 16];
|
||||||
|
let uv_bytes = &uvs[1 + i * 5..];
|
||||||
|
|
||||||
|
// Zero descent (device sits at this slot's node): cheapest, most common.
|
||||||
|
if validate_processing_key(&pk_zero_descent, cv, uv_bytes, &mk_dv).is_some() {
|
||||||
|
return Some((uv_r, u_mask_shift));
|
||||||
|
}
|
||||||
|
// Descent: device is an ANCESTOR of the slot. Walk the depth bit up from
|
||||||
|
// the slot's lowest set bit; each level descends to the slot's node.
|
||||||
|
let p = uv_r.trailing_zeros();
|
||||||
|
for k in (p + 1)..32 {
|
||||||
|
let uv_d = if k + 1 >= 32 {
|
||||||
|
1u32 << k
|
||||||
|
} else {
|
||||||
|
(uv_r & (0xFFFF_FFFFu32 << (k + 1))) | (1u32 << k)
|
||||||
|
};
|
||||||
|
let pk = calc_pk_from_dk(key, uv_r, v_mask, calc_v_mask(uv_d));
|
||||||
|
if validate_processing_key(&pk, cv, uv_bytes, &mk_dv).is_some() {
|
||||||
|
return Some((uv_d, u_mask_shift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
found.and_then(|(uv, mask)| resolve_dk_node(mkb, key, uv, mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a positioned [`DeviceKey`] for an orphan `key` known to sit at
|
||||||
|
/// `(uv, u_mask_shift)`: find a `device_number` (node) that passes the walk's
|
||||||
|
/// subset-difference gate on `mkb`. The derived key is independent of the exact
|
||||||
|
/// node (it only gates), so any gating node yields the same Media Key — a
|
||||||
|
/// one-time ≤32-try search, run only once at the recovered position.
|
||||||
|
pub(crate) fn resolve_dk_node(
|
||||||
|
mkb: &[u8],
|
||||||
|
key: &[u8; 16],
|
||||||
|
uv: u32,
|
||||||
|
u_mask_shift: u8,
|
||||||
|
) -> Option<DeviceKey> {
|
||||||
|
for b in 0..u_mask_shift {
|
||||||
|
let dk = DeviceKey {
|
||||||
|
key: *key,
|
||||||
|
node: ((uv ^ (1u32 << b)) & 0xFFFF) as u16,
|
||||||
|
uv,
|
||||||
|
u_mask_shift,
|
||||||
|
};
|
||||||
|
if derive_media_key_from_dk(mkb, std::slice::from_ref(&dk)).is_some() {
|
||||||
|
return Some(dk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Degenerate MKB (no gating bit): fall back to the node itself.
|
||||||
|
Some(DeviceKey {
|
||||||
|
key: *key,
|
||||||
|
node: (uv & 0xFFFF) as u16,
|
||||||
|
uv,
|
||||||
|
u_mask_shift,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public, side-effect-free accessors over the MKB record helpers, exposed so
|
||||||
|
/// independent reproduction harnesses (e.g. `examples/prove_hkd_aacs.rs`) can
|
||||||
|
/// exercise the exact same parser + verify primitives the production walk uses.
|
||||||
|
/// These are thin wrappers — no new logic.
|
||||||
|
pub mod probe {
|
||||||
|
use super::super::crypto::aes_ecb_decrypt;
|
||||||
|
|
||||||
|
/// `mk_dv` from the MKB's Verify-Media-Key record (type 0x81 / 0x86).
|
||||||
|
pub fn mkb_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
super::mkb_find_mk_dv(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body of the MKB's Subset-Difference Index record (type 0x04).
|
||||||
|
pub fn mkb_subdiff(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
super::mkb_find_subdiff_records(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body of the MKB's Media-Key-Data (cvalues) record. Selects record
|
||||||
|
/// `0x05` (the large cvalue table, 1:1 with the `0x04` Subset-Difference
|
||||||
|
/// index on AACS 2.x UHD MKBs), falling back to `0x07` only when `0x05`
|
||||||
|
/// is absent.
|
||||||
|
pub fn mkb_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
super::mkb_find_cvalues(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body (header stripped) of the first MKB record of `rec_type`. Lets a
|
||||||
|
/// harness pin an exact record type for cross-checking the production
|
||||||
|
/// cvalue selection (e.g. compare record `0x05` vs `0x07` sizes).
|
||||||
|
pub fn mkb_record_body(mkb: &[u8], rec_type: u8) -> Option<Vec<u8>> {
|
||||||
|
super::find_record_body(mkb, rec_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AES-128-ECB single-block decrypt (the AACS verify primitive).
|
||||||
|
pub fn aes_dec(key: &[u8; 16], block: &[u8; 16]) -> [u8; 16] {
|
||||||
|
aes_ecb_decrypt(key, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `km` satisfy the MKB's Verify-Media-Key relation?
|
||||||
|
/// `AES-D(km, mk_dv)[0..8] == 01 23 45 67 89 AB CD EF`.
|
||||||
|
pub fn km_verifies(mkb: &[u8], km: &[u8; 16]) -> bool {
|
||||||
|
match super::mkb_find_mk_dv(mkb) {
|
||||||
|
Some(mk_dv) => {
|
||||||
|
aes_ecb_decrypt(km, &mk_dv)[..8] == [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-7
@@ -29,12 +29,15 @@ pub mod boil;
|
|||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod host_certs;
|
pub mod host_certs;
|
||||||
pub mod keys;
|
pub mod inf;
|
||||||
|
pub mod media_key;
|
||||||
pub mod mkb;
|
pub mod mkb;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
pub mod resolve;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod variant;
|
pub mod variant;
|
||||||
|
pub mod volume_key;
|
||||||
|
|
||||||
/// On-disc UDF paths to the AACS key-input files (with their fallbacks).
|
/// On-disc UDF paths to the AACS key-input files (with their fallbacks).
|
||||||
/// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`,
|
/// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`,
|
||||||
@@ -68,14 +71,15 @@ pub use content::{
|
|||||||
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
||||||
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
||||||
// don't commit semver stability to test primitives.
|
// don't commit semver stability to test primitives.
|
||||||
|
pub use inf::{
|
||||||
|
ContentCert, UnitKeyFile, disc_hash, disc_hash_hex, parse_content_cert, parse_unit_key_ro,
|
||||||
|
read_mkb_from_drive,
|
||||||
|
};
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use keys::probe;
|
pub use media_key::probe;
|
||||||
pub use keys::{
|
pub use media_key::{
|
||||||
ContentCert, ResolveContext, ResolveFailure, ResolvedKeys, UnitKeyFile, decrypt_unit_key,
|
|
||||||
derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk,
|
derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk,
|
||||||
derive_vuk, disc_hash, disc_hash_hex, parse_content_cert, parse_unit_key_ro,
|
recover_dk_position,
|
||||||
read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21,
|
|
||||||
resolve_keys_with_reason,
|
|
||||||
};
|
};
|
||||||
pub use mkb::{
|
pub use mkb::{
|
||||||
AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C,
|
AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C,
|
||||||
@@ -83,11 +87,16 @@ pub use mkb::{
|
|||||||
mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, trim_mkb, walk_mkb,
|
mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, trim_mkb, walk_mkb,
|
||||||
};
|
};
|
||||||
pub use provider::KeyProvider;
|
pub use provider::KeyProvider;
|
||||||
|
pub use resolve::{
|
||||||
|
ResolveContext, ResolveFailure, ResolvedKeys, resolve_keys_v1, resolve_keys_v2,
|
||||||
|
resolve_keys_v21, resolve_keys_with_reason,
|
||||||
|
};
|
||||||
pub use types::{DeviceKey, DiscEntry, HostCert};
|
pub use types::{DeviceKey, DiscEntry, HostCert};
|
||||||
pub use variant::{
|
pub use variant::{
|
||||||
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, ProcessingKeyMatch,
|
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, ProcessingKeyMatch,
|
||||||
derive_media_key_variant, is_variant_mkb, variant_nonce, walk_processing_key,
|
derive_media_key_variant, is_variant_mkb, variant_nonce, walk_processing_key,
|
||||||
};
|
};
|
||||||
|
pub use volume_key::{decrypt_unit_key, derive_vuk};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -1,735 +1,16 @@
|
|||||||
//! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing.
|
//! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing.
|
||||||
|
|
||||||
use super::crypto::{aes_ecb_decrypt, aesg3};
|
use super::crypto::aes_ecb_decrypt;
|
||||||
|
use super::inf::*;
|
||||||
|
use super::media_key::*;
|
||||||
use super::mkb::*;
|
use super::mkb::*;
|
||||||
use super::types::DeviceKey;
|
use super::volume_key::*;
|
||||||
|
|
||||||
// ── AACS version ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// ── VUK derivation ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Derive VUK from Media Key and Volume ID. [PR] §3.3 / [BD] §3.3
|
|
||||||
/// (`Kvu = AES-G(Km, IDv)`; AES-G uses AES-128D):
|
|
||||||
/// 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). [PR] §3.5
|
|
||||||
/// (Title Key unwrap `Kt = AES-128D(Ku, Kte)`); the BD "CPS Unit Key" synonym is [BD] §3.9.3.
|
|
||||||
pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] {
|
|
||||||
aes_ecb_decrypt(vuk, encrypted_uk)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt every encrypted unit key in a parsed `Unit_Key_RO.inf` with a VUK,
|
|
||||||
/// paired with its declared CPS-unit number. THE single VUK→unit-keys step:
|
|
||||||
/// both classical/v21 resolvers and `boil::resolve_candidate` call this, so the
|
|
||||||
/// map cannot drift between the player and harvest paths.
|
|
||||||
pub(crate) fn derive_unit_keys(uk_file: &UnitKeyFile, vuk: &[u8; 16]) -> Vec<(u32, [u8; 16])> {
|
|
||||||
uk_file
|
|
||||||
.encrypted_keys
|
|
||||||
.iter()
|
|
||||||
.map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key)))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 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<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 processing ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Derive Media Key from MKB data using processing keys.
|
|
||||||
///
|
|
||||||
/// A Processing Key is **terminal**: it is the key at its Subset-Difference
|
|
||||||
/// node, one `AES-G` from the Media Key. So this is the fast path — each PK is
|
|
||||||
/// tried *directly* against the MKB cvalue tables (no tree descent), matching
|
|
||||||
/// libaacs `_calc_mk_pks` (iterate PKs × cvalues). On a large AACS 2.x UHD MKB
|
|
||||||
/// (~181k cvalues) this is ~15x faster than treating a PK as a device-node
|
|
||||||
/// label and walking the tree.
|
|
||||||
///
|
|
||||||
/// If you hold a **device-node label** at unknown tree depth (not a terminal
|
|
||||||
/// PK), derive its Media Key through the device-key path
|
|
||||||
/// ([`derive_media_key_from_dk`]) — that path owns the Subset-Difference tree
|
|
||||||
/// walk; the PK path never descends.
|
|
||||||
///
|
|
||||||
/// 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 0x05 = Media Key Data Record (cvalues, 1:1 with 0x04)
|
|
||||||
/// Record type 0x07 = Explicit Subset-Difference Record (NOT cvalues)
|
|
||||||
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
|
|
||||||
let mk_dv = mkb_find_mk_dv(mkb)?;
|
|
||||||
let uvs = mkb_find_subdiff_records(mkb)?;
|
|
||||||
let cvalues = mkb_find_cvalues(mkb)?;
|
|
||||||
try_pk_against_tables(processing_keys, &uvs, &cvalues, &mk_dv)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Core terminal-PK table scan over explicit record bodies. Each processing
|
|
||||||
/// key is tried **directly** against every `(uv, cvalue)` pair — no tree
|
|
||||||
/// descent. Reached in production via [`derive_media_key_from_pk`]; factored
|
|
||||||
/// out so reproduction harnesses can drive it with explicit tables.
|
|
||||||
fn try_pk_against_tables(
|
|
||||||
processing_keys: &[[u8; 16]],
|
|
||||||
uvs: &[u8],
|
|
||||||
cvalues: &[u8],
|
|
||||||
mk_dv: &[u8; 16],
|
|
||||||
) -> Option<[u8; 16]> {
|
|
||||||
let num_uvs = uvs
|
|
||||||
.chunks(5)
|
|
||||||
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
|
||||||
.count();
|
|
||||||
|
|
||||||
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 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 (media key: [C] §3.2.4; verify relation: [C] §3.2.5.1.4):
|
|
||||||
/// 1. `mk = AES-128D(pk, cvalue)` [C] §3.2.4
|
|
||||||
/// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) [C] §3.2.4
|
|
||||||
/// 3. `dec_vd = AES-128D(mk, mk_dv)` [C] §3.2.5.1.4
|
|
||||||
/// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. [C] §3.2.5.1.4
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Public, side-effect-free accessors over the MKB record helpers, exposed so
|
|
||||||
/// independent reproduction harnesses (e.g. `examples/prove_hkd_aacs.rs`) can
|
|
||||||
/// exercise the exact same parser + verify primitives the production walk uses.
|
|
||||||
/// These are thin wrappers — no new logic.
|
|
||||||
pub mod probe {
|
|
||||||
use super::aes_ecb_decrypt;
|
|
||||||
|
|
||||||
/// `mk_dv` from the MKB's Verify-Media-Key record (type 0x81 / 0x86).
|
|
||||||
pub fn mkb_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
|
||||||
super::mkb_find_mk_dv(mkb)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Body of the MKB's Subset-Difference Index record (type 0x04).
|
|
||||||
pub fn mkb_subdiff(mkb: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
super::mkb_find_subdiff_records(mkb)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Body of the MKB's Media-Key-Data (cvalues) record. Selects record
|
|
||||||
/// `0x05` (the large cvalue table, 1:1 with the `0x04` Subset-Difference
|
|
||||||
/// index on AACS 2.x UHD MKBs), falling back to `0x07` only when `0x05`
|
|
||||||
/// is absent.
|
|
||||||
pub fn mkb_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
super::mkb_find_cvalues(mkb)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Body (header stripped) of the first MKB record of `rec_type`. Lets a
|
|
||||||
/// harness pin an exact record type for cross-checking the production
|
|
||||||
/// cvalue selection (e.g. compare record `0x05` vs `0x07` sizes).
|
|
||||||
pub fn mkb_record_body(mkb: &[u8], rec_type: u8) -> Option<Vec<u8>> {
|
|
||||||
super::find_record_body(mkb, rec_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AES-128-ECB single-block decrypt (the AACS verify primitive).
|
|
||||||
pub fn aes_dec(key: &[u8; 16], block: &[u8; 16]) -> [u8; 16] {
|
|
||||||
aes_ecb_decrypt(key, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Does `km` satisfy the MKB's Verify-Media-Key relation?
|
|
||||||
/// `AES-D(km, mk_dv)[0..8] == 01 23 45 67 89 AB CD EF`.
|
|
||||||
pub fn km_verifies(mkb: &[u8], km: &[u8; 16]) -> bool {
|
|
||||||
match super::mkb_find_mk_dv(mkb) {
|
|
||||||
Some(mk_dv) => {
|
|
||||||
aes_ecb_decrypt(km, &mk_dv)[..8] == [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]
|
|
||||||
}
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── MKB Type field (Type-and-Version record 0x10, bytes 4-7) ────────────────
|
|
||||||
//
|
//
|
||||||
// Canonical form is `<category>1003` (low 16 bits `0x1003` is a fixed marker).
|
// Canonical form is `<category>1003` (low 16 bits `0x1003` is a fixed marker).
|
||||||
// Types 3/4/10 are from the AACS Common Cryptographic Elements spec (0.953,
|
// Types 3/4/10 are from the AACS Common Cryptographic Elements spec (0.953,
|
||||||
// §3.2.5.1.1); the Category-C 2.0/2.1 values match libaacs `mkb.h` constants.
|
// §3.2.5.1.1); the Category-C 2.0/2.1 values match libaacs `mkb.h` constants.
|
||||||
|
|
||||||
// ── AACS-G3 key derivation (subset-difference tree) ─────────────────────────
|
|
||||||
|
|
||||||
/// Compute v_mask from a UV value. [C] §3.2.3. Shared with [`super::variants`].
|
|
||||||
pub(super) 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.
|
|
||||||
/// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). Shared with [`super::variants`].
|
|
||||||
pub(super) fn calc_pk_from_dk(
|
|
||||||
dk: &[u8; 16],
|
|
||||||
uv: u32,
|
|
||||||
v_mask: u32,
|
|
||||||
dev_key_v_mask: u32,
|
|
||||||
) -> [u8; 16] {
|
|
||||||
// Descend from the device node to the record node, following the record's
|
|
||||||
// `uv` bits. At each level only the child we descend INTO is needed (the
|
|
||||||
// sibling is computed but never used), and the Processing Key is the
|
|
||||||
// `aesg3(.,1)` of the FINAL node — so we derive ONE child per level and the
|
|
||||||
// PK once at the end, instead of left/pk/right at every level. Identical
|
|
||||||
// result, ~3x fewer block ops. (left child = `aesg3(node,0)`, right = `,2`.)
|
|
||||||
let mut node = *dk;
|
|
||||||
let mut current_v_mask = dev_key_v_mask;
|
|
||||||
|
|
||||||
// The subset-difference tree is at most 32 levels deep (u32 mask), so the
|
|
||||||
// walk must converge in <= 32 steps. The arithmetic `>> 1` sign-extends
|
|
||||||
// current_v_mask, so a v_mask coarser than dev_key_v_mask (reachable from
|
|
||||||
// a crafted/corrupt MKB) would otherwise saturate at 0xFFFF_FFFF and spin
|
|
||||||
// forever — bound the loop to keep a bad disc from hanging the rip thread.
|
|
||||||
let mut steps = 0u32;
|
|
||||||
while current_v_mask != v_mask {
|
|
||||||
if steps >= 32 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
steps += 1;
|
|
||||||
// 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 inc = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 {
|
|
||||||
0 // left child
|
|
||||||
} else {
|
|
||||||
2 // right child
|
|
||||||
};
|
|
||||||
node = aesg3(&node, inc);
|
|
||||||
|
|
||||||
current_v_mask = ((current_v_mask as i32) >> 1) as u32;
|
|
||||||
}
|
|
||||||
|
|
||||||
aesg3(&node, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive Media Key from MKB using device keys (subset-difference tree).
|
|
||||||
///
|
|
||||||
/// Thin wrapper over [`derive_media_key_and_pk_from_dk`] that drops the
|
|
||||||
/// intermediate Processing Key. Callers that need the PK lineage (e.g.
|
|
||||||
/// the key service banking DK·PK·MK) should call the `_and_pk_` form.
|
|
||||||
pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> {
|
|
||||||
derive_media_key_and_pk_from_dk(mkb, device_keys).map(|(mk, _pk)| mk)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive both the Media Key and the intermediate Processing Key from an
|
|
||||||
/// MKB using device keys (subset-difference tree).
|
|
||||||
///
|
|
||||||
/// Identical walk to [`derive_media_key_from_dk`]; this form additionally
|
|
||||||
/// returns the Processing Key `Kp` derived at the matching subset-difference
|
|
||||||
/// node — the value `calc_pk_from_dk` produces immediately before it
|
|
||||||
/// validates into the Media Key. Returns `Some((mk, pk))` for the first DK
|
|
||||||
/// that walks a uv slot whose Processing Key validates against the MKB.
|
|
||||||
pub fn derive_media_key_and_pk_from_dk(
|
|
||||||
mkb: &[u8],
|
|
||||||
device_keys: &[DeviceKey],
|
|
||||||
) -> Option<([u8; 16], [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
|
|
||||||
}
|
|
||||||
// Shifts of 32..=63 (0x20..=0x3F pass the 0xC0 mask above) would
|
|
||||||
// panic in debug / wrap to a wrong mask in release. The MKB byte
|
|
||||||
// is disc-controlled, so a crafted/corrupt MKB must not crash the
|
|
||||||
// ripper: skip an out-of-range slot rather than `<<` it.
|
|
||||||
if u_mask_shift >= 32 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
|
||||||
if uv == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// u-mask = shift count of low-order 0 bits ([C] §3.2.5.1.5); v-mask [C] §3.2.3.
|
|
||||||
let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift;
|
|
||||||
let v_mask = calc_v_mask(uv);
|
|
||||||
|
|
||||||
// Subset-difference applies iff (d&mu)==(uv&mu) && (d&mv)!=(uv&mv). [C] §3.2.4.
|
|
||||||
if ((device_number & u_mask) == (uv & u_mask))
|
|
||||||
&& ((device_number & v_mask) != (uv & v_mask))
|
|
||||||
{
|
|
||||||
// Found matching subset-difference — find the right device key.
|
|
||||||
// dk.u_mask_shift is a u8 from keydb with no range check;
|
|
||||||
// guard the shift the same way as the MKB byte above.
|
|
||||||
if dk.u_mask_shift >= 32 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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, pk));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recover the subset-difference position (`node`, `uv`, `u_mask_shift`) of an
|
|
||||||
/// UNPOSITIONED device key by scanning a disc MKB. A device key alone (just the
|
|
||||||
/// 16 bytes) cannot be walked — the walk needs its tree node. This finds that
|
|
||||||
/// node empirically: for each MKB subset-difference record, it tries the device
|
|
||||||
/// at the record's node AND at every ancestor v-position (the device may sit one
|
|
||||||
/// or more levels ABOVE the record, descending via AES-G to reach it), deriving
|
|
||||||
/// the candidate Processing Key DIRECTLY (one [`calc_pk_from_dk`] per candidate,
|
|
||||||
/// no full re-walk) and checking it validates against that record's cvalue.
|
|
||||||
///
|
|
||||||
/// On the first verifying candidate it pins `(uv, u_mask_shift)` — invariant for
|
|
||||||
/// the key across all discs — and resolves a gate-passing `node` (a one-time
|
|
||||||
/// ≤32-try search at the single hit). Returns a [`DeviceKey`] ready to bank and
|
|
||||||
/// reuse on every future disc via [`derive_media_key_from_dk`]. `None` if the
|
|
||||||
/// key does not apply to this MKB.
|
|
||||||
///
|
|
||||||
/// Cost is `O(slots × tree_depth)` — linear in the MKB's subset-difference
|
|
||||||
/// index, not the quartic cost of re-deriving per candidate.
|
|
||||||
pub fn recover_dk_position(mkb: &[u8], key: &[u8; 16]) -> Option<DeviceKey> {
|
|
||||||
let mk_dv = mkb_find_mk_dv(mkb)?;
|
|
||||||
let uvs = mkb_find_subdiff_records(mkb)?;
|
|
||||||
let cvalues = mkb_find_cvalues(mkb)?;
|
|
||||||
let num_uvs = uvs
|
|
||||||
.chunks(5)
|
|
||||||
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
|
||||||
.count();
|
|
||||||
let n_cv = cvalues.len() / 16;
|
|
||||||
|
|
||||||
// Hoisted ONCE for the whole scan: the Processing Key the device produces if
|
|
||||||
// it sits EXACTLY at a record (zero descent) is `AES-G3(key, 1)` — it does
|
|
||||||
// not depend on the record, so the zero-descent probe of every slot reuses
|
|
||||||
// this single value instead of re-deriving it per slot.
|
|
||||||
let pk_zero_descent = aesg3(key, 1);
|
|
||||||
|
|
||||||
// The slots are independent, so the scan parallelises — a UHD MKB has ~181k
|
|
||||||
// slots (~26s single-threaded). `find_map_any` returns the first matching
|
|
||||||
// node found by any thread and cancels the rest; a valid MKB has exactly one
|
|
||||||
// matching subset-difference, so which thread finds it is immaterial.
|
|
||||||
use rayon::prelude::*;
|
|
||||||
let found = (0..num_uvs.min(n_cv)).into_par_iter().find_map_any(|i| {
|
|
||||||
let u_mask_shift = uvs[5 * i];
|
|
||||||
if u_mask_shift >= 32 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let p_uv = &uvs[1 + 5 * i..];
|
|
||||||
let uv_r = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
|
||||||
if uv_r == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let v_mask = calc_v_mask(uv_r);
|
|
||||||
let cv = &cvalues[i * 16..(i + 1) * 16];
|
|
||||||
let uv_bytes = &uvs[1 + i * 5..];
|
|
||||||
|
|
||||||
// Zero descent (device sits at this slot's node): cheapest, most common.
|
|
||||||
if validate_processing_key(&pk_zero_descent, cv, uv_bytes, &mk_dv).is_some() {
|
|
||||||
return Some((uv_r, u_mask_shift));
|
|
||||||
}
|
|
||||||
// Descent: device is an ANCESTOR of the slot. Walk the depth bit up from
|
|
||||||
// the slot's lowest set bit; each level descends to the slot's node.
|
|
||||||
let p = uv_r.trailing_zeros();
|
|
||||||
for k in (p + 1)..32 {
|
|
||||||
let uv_d = if k + 1 >= 32 {
|
|
||||||
1u32 << k
|
|
||||||
} else {
|
|
||||||
(uv_r & (0xFFFF_FFFFu32 << (k + 1))) | (1u32 << k)
|
|
||||||
};
|
|
||||||
let pk = calc_pk_from_dk(key, uv_r, v_mask, calc_v_mask(uv_d));
|
|
||||||
if validate_processing_key(&pk, cv, uv_bytes, &mk_dv).is_some() {
|
|
||||||
return Some((uv_d, u_mask_shift));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
});
|
|
||||||
found.and_then(|(uv, mask)| resolve_dk_node(mkb, key, uv, mask))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a positioned [`DeviceKey`] for an orphan `key` known to sit at
|
|
||||||
/// `(uv, u_mask_shift)`: find a `device_number` (node) that passes the walk's
|
|
||||||
/// subset-difference gate on `mkb`. The derived key is independent of the exact
|
|
||||||
/// node (it only gates), so any gating node yields the same Media Key — a
|
|
||||||
/// one-time ≤32-try search, run only once at the recovered position.
|
|
||||||
fn resolve_dk_node(mkb: &[u8], key: &[u8; 16], uv: u32, u_mask_shift: u8) -> Option<DeviceKey> {
|
|
||||||
for b in 0..u_mask_shift {
|
|
||||||
let dk = DeviceKey {
|
|
||||||
key: *key,
|
|
||||||
node: ((uv ^ (1u32 << b)) & 0xFFFF) as u16,
|
|
||||||
uv,
|
|
||||||
u_mask_shift,
|
|
||||||
};
|
|
||||||
if derive_media_key_from_dk(mkb, std::slice::from_ref(&dk)).is_some() {
|
|
||||||
return Some(dk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Degenerate MKB (no gating bit): fall back to the node itself.
|
|
||||||
Some(DeviceKey {
|
|
||||||
key: *key,
|
|
||||||
node: (uv & 0xFFFF) as u16,
|
|
||||||
uv,
|
|
||||||
u_mask_shift,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 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<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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Full VUK resolution chain ───────────────────────────────────────────────
|
// ── Full VUK resolution chain ───────────────────────────────────────────────
|
||||||
|
|
||||||
/// Result of resolving a disc's VUK.
|
/// Result of resolving a disc's VUK.
|
||||||
@@ -823,7 +104,7 @@ pub fn resolve_keys_with_reason(
|
|||||||
///
|
///
|
||||||
/// Reads only what the resolver already had (provider material + the VID
|
/// Reads only what the resolver already had (provider material + the VID
|
||||||
/// sentinel) — no key derivation, no descramble.
|
/// sentinel) — no key derivation, no descramble.
|
||||||
fn classify_resolve_failure(ctx: &ResolveContext<'_>) -> ResolveFailure {
|
pub(crate) fn classify_resolve_failure(ctx: &ResolveContext<'_>) -> ResolveFailure {
|
||||||
let has_vid = *ctx.volume_id != [0u8; 16];
|
let has_vid = *ctx.volume_id != [0u8; 16];
|
||||||
let providers = super::provider::Providers(ctx.providers);
|
let providers = super::provider::Providers(ctx.providers);
|
||||||
let has_derivation_material =
|
let has_derivation_material =
|
||||||
@@ -1156,7 +437,7 @@ fn resolve_keys_classical(ctx: &ResolveContext<'_>, version: AacsVersion) -> Opt
|
|||||||
/// CPS unit the disc declares must have a matching entry in KEYDB;
|
/// CPS unit the disc declares must have a matching entry in KEYDB;
|
||||||
/// partial coverage returns `None` so the resolver doesn't half-decrypt
|
/// partial coverage returns `None` so the resolver doesn't half-decrypt
|
||||||
/// a disc.
|
/// a disc.
|
||||||
fn match_keydb_unit_keys(
|
pub(crate) fn match_keydb_unit_keys(
|
||||||
uk_file: &UnitKeyFile,
|
uk_file: &UnitKeyFile,
|
||||||
keydb_unit_keys: &[(u32, [u8; 16])],
|
keydb_unit_keys: &[(u32, [u8; 16])],
|
||||||
) -> Option<Vec<(u32, [u8; 16])>> {
|
) -> Option<Vec<(u32, [u8; 16])>> {
|
||||||
@@ -1173,8 +454,16 @@ fn match_keydb_unit_keys(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
// This suite predates the module split; it white-box-tests items now living
|
||||||
|
// in sibling modules. Pull them all in so the tests keep exercising them.
|
||||||
|
use super::super::crypto::*;
|
||||||
|
use super::super::inf::*;
|
||||||
|
use super::super::media_key::*;
|
||||||
|
use super::super::mkb::*;
|
||||||
use super::super::provider::SuppliedKey;
|
use super::super::provider::SuppliedKey;
|
||||||
use super::super::types::DiscEntry;
|
use super::super::types::DiscEntry;
|
||||||
|
use super::super::types::*;
|
||||||
|
use super::super::volume_key::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
||||||
+2
-2
@@ -151,7 +151,7 @@ pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
|
|||||||
// on) are shared with the classical walk in [`super::keys`] — a single
|
// on) are shared with the classical walk in [`super::keys`] — a single
|
||||||
// definition keeps the variant SD tree byte-identical to the classical one.
|
// definition keeps the variant SD tree byte-identical to the classical one.
|
||||||
// (`aesg3` itself is imported separately in the test module.)
|
// (`aesg3` itself is imported separately in the test module.)
|
||||||
use super::keys::{calc_pk_from_dk, calc_v_mask};
|
use super::media_key::{calc_pk_from_dk, calc_v_mask};
|
||||||
|
|
||||||
/// Outcome of a subset-difference walk against an MKB. Carries the
|
/// Outcome of a subset-difference walk against an MKB. Carries the
|
||||||
/// processing key and the matching `uv` slot — both needed as inputs
|
/// processing key and the matching `uv` slot — both needed as inputs
|
||||||
@@ -483,7 +483,7 @@ mod tests {
|
|||||||
// `use super::*` does not re-export the parent module's private `use`
|
// `use super::*` does not re-export the parent module's private `use`
|
||||||
// imports, so pull them in directly for the tests below.
|
// imports, so pull them in directly for the tests below.
|
||||||
use super::super::crypto::aesg3;
|
use super::super::crypto::aesg3;
|
||||||
use super::super::keys::calc_pk_from_dk;
|
use super::super::media_key::calc_pk_from_dk;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! Volume-key layer: derive the Volume Unique Key from the Media Key + Volume
|
||||||
|
//! ID, unwrap unit keys with it. [PR] §3.3 / §3.5, [BD] §3.3 / §3.9.3.
|
||||||
|
|
||||||
|
use super::crypto::*;
|
||||||
|
use super::inf::*;
|
||||||
|
|
||||||
|
/// Derive VUK from Media Key and Volume ID. [PR] §3.3 / [BD] §3.3
|
||||||
|
/// (`Kvu = AES-G(Km, IDv)`; AES-G uses AES-128D):
|
||||||
|
/// 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). [PR] §3.5
|
||||||
|
/// (Title Key unwrap `Kt = AES-128D(Ku, Kte)`); the BD "CPS Unit Key" synonym is [BD] §3.9.3.
|
||||||
|
pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] {
|
||||||
|
aes_ecb_decrypt(vuk, encrypted_uk)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt every encrypted unit key in a parsed `Unit_Key_RO.inf` with a VUK,
|
||||||
|
/// paired with its declared CPS-unit number. THE single VUK→unit-keys step:
|
||||||
|
/// both classical/v21 resolvers and `boil::resolve_candidate` call this, so the
|
||||||
|
/// map cannot drift between the player and harvest paths.
|
||||||
|
pub(crate) fn derive_unit_keys(uk_file: &UnitKeyFile, vuk: &[u8; 16]) -> Vec<(u32, [u8; 16])> {
|
||||||
|
uk_file
|
||||||
|
.encrypted_keys
|
||||||
|
.iter()
|
||||||
|
.map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user