use super::decrypt::aes_ecb_decrypt;
use super::keydb::DeviceKey;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AacsVersion {
V10,
V20,
V21,
}
impl AacsVersion {
fn unit_key_stride(self) -> usize {
match self {
AacsVersion::V10 => 48,
AacsVersion::V20 | AacsVersion::V21 => 64,
}
}
}
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
}
pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] {
aes_ecb_decrypt(vuk, encrypted_uk)
}
#[derive(Debug)]
pub struct UnitKeyFile {
pub disc_hash: [u8; 20],
pub app_type: u8,
pub num_bdmv_dir: u8,
pub use_skb_mkb: bool,
pub version: AacsVersion,
pub encrypted_keys: Vec<(u32, [u8; 16])>,
pub title_cps_unit: Vec<u16>,
}
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
}
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
}
pub fn parse_unit_key_ro(data: &[u8], version: AacsVersion) -> Option<UnitKeyFile> {
if data.len() < 20 {
return None;
}
let hash = disc_hash(data);
let app_type = data[16];
let num_bdmv_dir = data[17];
let use_skb_mkb = (data[18] >> 7) & 1 == 1;
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;
}
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(),
});
}
let stride = version.unit_key_stride();
let keys_start = uk_pos + 48; if keys_start + 16 > data.len() {
return None;
}
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;
}
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; 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,
})
}
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
derive_media_key_from_pk_walked(mkb, processing_keys, PK_WALK_MAX_DEPTH)
}
const PK_WALK_MAX_DEPTH: u8 = 3;
const PK_WALK_MAX_DEPTH_CAP: u8 = 5;
pub fn derive_media_key_from_pk_walked(
mkb: &[u8],
processing_keys: &[[u8; 16]],
max_depth: u8,
) -> Option<[u8; 16]> {
let mk_dv = mkb_find_mk_dv(mkb)?;
let uvs = mkb_find_subdiff_records(mkb)?;
let cvalues = mkb_find_cvalues(mkb)?;
walk_pk_against_tables_impl(processing_keys, &uvs, &cvalues, &mk_dv, max_depth)
}
fn walk_pk_against_tables_impl(
processing_keys: &[[u8; 16]],
uvs: &[u8],
cvalues: &[u8],
mk_dv: &[u8; 16],
max_depth: u8,
) -> Option<[u8; 16]> {
let max_depth = max_depth.min(PK_WALK_MAX_DEPTH_CAP);
let num_uvs = uvs
.chunks(5)
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
.count();
let try_against_mkb = |pk: &[u8; 16]| -> Option<[u8; 16]> {
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
};
for entry in processing_keys {
if let Some(mk) = try_against_mkb(entry) {
return Some(mk);
}
let pk_at_node = aesg3(entry, 1);
if let Some(mk) = try_against_mkb(&pk_at_node) {
return Some(mk);
}
if max_depth == 0 {
continue;
}
let mut frontier: Vec<[u8; 16]> = vec![aesg3(entry, 0), aesg3(entry, 2)];
for depth in 1..=max_depth {
let mut next = Vec::with_capacity(frontier.len() * 2);
for nk in &frontier {
let pk_here = aesg3(nk, 1);
if let Some(mk) = try_against_mkb(&pk_here) {
return Some(mk);
}
if let Some(mk) = try_against_mkb(nk) {
return Some(mk);
}
if depth < max_depth {
next.push(aesg3(nk, 0));
next.push(aesg3(nk, 2));
}
}
frontier = next;
}
}
None
}
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;
}
let mut cv = [0u8; 16];
cv.copy_from_slice(&cvalue[..16]);
let mut mk = aes_ecb_decrypt(pk, &cv);
for a in 0..4 {
mk[12 + a] ^= uv[a];
}
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
}
pub mod probe {
use super::aes_ecb_decrypt;
pub fn mkb_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
super::mkb_find_mk_dv(mkb)
}
pub fn mkb_subdiff(mkb: &[u8]) -> Option<Vec<u8>> {
super::mkb_find_subdiff_records(mkb)
}
pub fn mkb_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
super::mkb_find_cvalues(mkb)
}
pub fn mkb_record_body(mkb: &[u8], rec_type: u8) -> Option<Vec<u8>> {
super::find_record_body(mkb, rec_type)
}
pub fn aes_dec(key: &[u8; 16], block: &[u8; 16]) -> [u8; 16] {
aes_ecb_decrypt(key, block)
}
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,
}
}
pub fn walk_pk_against_tables(
keys: &[[u8; 16]],
subdiff: &[u8],
cvalues: &[u8],
mk_dv: &[u8; 16],
max_depth: u8,
) -> Option<[u8; 16]> {
super::walk_pk_against_tables_impl(keys, subdiff, cvalues, mk_dv, max_depth)
}
}
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 {
let mut dv = [0u8; 16];
dv.copy_from_slice(&mkb[pos + 4..pos + 20]);
tracing::debug!(
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
}
fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
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
}
fn mkb_find_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
if let Some(body) = find_record_body(mkb, 0x05) {
return Some(body);
}
find_record_body(mkb, 0x07)
}
fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option<Vec<u8>> {
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());
}
pos += rec_len;
}
None
}
pub fn mkb_content_len(mkb: &[u8]) -> usize {
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_type == 0x00 || rec_len < 4 || pos + rec_len > mkb.len() {
break;
}
pos += rec_len;
}
pos
}
pub fn trim_mkb(mut mkb: Vec<u8>) -> Vec<u8> {
let n = mkb_content_len(&mkb);
if n > 0 && n < mkb.len() {
mkb.truncate(n);
}
mkb
}
pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
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
}
const AESG3_SEED: [u8; 16] = [
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
];
pub(super) 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
}
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
}
pub(super) fn calc_pk_from_dk(
dk: &[u8; 16],
uv: u32,
v_mask: u32,
dev_key_v_mask: u32,
) -> [u8; 16] {
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;
let mut steps = 0u32;
while current_v_mask != v_mask {
if steps >= 32 {
break;
}
steps += 1;
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
}
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)
}
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)?;
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;
for uvs_idx in 0..num_uvs {
let p_uv = &uvs[1 + 5 * uvs_idx..];
let u_mask_shift = uvs[5 * uvs_idx];
if u_mask_shift & 0xC0 != 0 {
break; }
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;
}
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))
{
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) {
let pk = calc_pk_from_dk(&dk.key, uv, v_mask, dev_key_v_mask);
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
}
const MKB_DISC_STRUCTURE_FORMAT: u8 = 0x83;
const MKB_PACK_SIZE: usize = 32772;
pub fn read_mkb_from_drive(session: &mut crate::drive::Drive) -> 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.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]);
}
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,
];
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)
}
#[derive(Debug)]
pub struct ContentCert {
pub bus_encryption: bool,
pub cc_id: [u8; 6],
pub version: AacsVersion,
}
pub fn parse_content_cert(data: &[u8]) -> Option<ContentCert> {
if data.len() < 8 {
return None;
}
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,
})
}
#[derive(Debug)]
pub struct ResolvedKeys {
pub disc_hash: [u8; 20],
pub vuk: Option<[u8; 16]>,
pub unit_keys: Vec<(u32, [u8; 16])>,
pub title_cps_unit: Vec<u16>,
pub version: AacsVersion,
pub bus_encryption: bool,
pub key_source: u8,
}
pub struct ResolveContext<'a> {
pub unit_key_ro: &'a [u8],
pub content_cert: Option<&'a [u8]>,
pub volume_id: &'a [u8; 16],
pub providers: &'a [&'a dyn super::provider::KeyProvider],
pub mkb: Option<&'a [u8]>,
}
pub fn resolve_keys_v1(ctx: &ResolveContext<'_>) -> Option<ResolvedKeys> {
resolve_keys_classical(ctx, AacsVersion::V10)
}
pub fn resolve_keys_v2(ctx: &ResolveContext<'_>) -> Option<ResolvedKeys> {
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)
}
pub fn resolve_keys_v21(ctx: &ResolveContext<'_>) -> Option<ResolvedKeys> {
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 has_vid = *ctx.volume_id != [0u8; 16];
let derive_uks = |vuk: &[u8; 16]| -> Vec<(u32, [u8; 16])> {
uk_file
.encrypted_keys
.iter()
.map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key)))
.collect()
};
let build =
|vuk: Option<[u8; 16]>, unit_keys: Vec<(u32, [u8; 16])>, key_source: u8| -> ResolvedKeys {
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::info!(
target: "freemkv::disc",
phase = "resolve_keys_v21_start",
bus_encryption,
disc_hash = %hash_hex,
has_vid,
mkb_present = ctx.mkb.is_some(),
"resolve_keys_v21: starting"
);
let providers = super::provider::Providers(ctx.providers);
if has_vid {
if let Some(mkb) = ctx.mkb {
let recs = super::variants::walk_mkb(mkb);
let all_dks = providers.device_keys();
match super::variants::derive_media_key_variant(
&recs,
&all_dks,
&super::variants::KEY_CORRECTION_DATA_PLACEHOLDER,
ctx.volume_id,
) {
Ok((_km, kvu)) => {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_v21_path1_hit",
"Variant chain produced Km + Kvu"
);
return Some(build(Some(kvu), derive_uks(&kvu), 1));
}
Err(e) => {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_v21_path1_miss",
error_code = %e,
"Variant chain failed"
);
}
}
}
if let Some(entry) = providers.lookup_disc_by_vid(ctx.volume_id) {
if let Some(mk) = entry.media_key {
let vuk = derive_vuk(&mk, ctx.volume_id);
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_v21_path3_hit", "MK+VID entry matched volume_id");
return Some(build(Some(vuk), derive_uks(&vuk), 3));
}
}
} else {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_v21_no_vid",
"VID unavailable; paths 1/3 skipped"
);
}
if let Some(entry) = providers.lookup_disc_by_hash(&uk_file.disc_hash) {
if let Some(vuk) = entry.vuk {
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_v21_path4_hit", "VUK from KEYDB");
return Some(build(Some(vuk), derive_uks(&vuk), 4));
} else if let Some(unit_keys) = match_keydb_unit_keys(&uk_file, &entry.unit_keys) {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_v21_path5_hit",
uk_count = unit_keys.len(),
"unit keys from KEYDB (no VUK)"
);
return Some(build(None, unit_keys, 5));
}
}
None
}
fn resolve_keys_classical(ctx: &ResolveContext<'_>, version: AacsVersion) -> Option<ResolvedKeys> {
let bus_encryption = ctx
.content_cert
.and_then(parse_content_cert)
.map(|cc| cc.bus_encryption)
.unwrap_or(false);
let uk_file = parse_unit_key_ro(ctx.unit_key_ro, version)?;
let hash_hex = disc_hash_hex(&uk_file.disc_hash);
let has_vid = *ctx.volume_id != [0u8; 16];
let derive_uks = |vuk: &[u8; 16]| -> Vec<(u32, [u8; 16])> {
uk_file
.encrypted_keys
.iter()
.map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key)))
.collect()
};
let build =
|vuk: Option<[u8; 16]>, unit_keys: Vec<(u32, [u8; 16])>, key_source: u8| -> ResolvedKeys {
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::info!(
target: "freemkv::disc",
phase = "resolve_keys_start",
version = ?version,
bus_encryption,
disc_hash = %hash_hex,
has_vid,
mkb_present = ctx.mkb.is_some(),
"resolve_keys: starting"
);
let providers = super::provider::Providers(ctx.providers);
if has_vid {
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::debug!(
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"
);
let all_dks = providers.device_keys();
if let Some(mk) = derive_media_key_from_dk(mkb, &all_dks) {
let vuk = derive_vuk(&mk, ctx.volume_id);
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path1_hit", "media key derived from device key");
return Some(build(Some(vuk), derive_uks(&vuk), 1));
}
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path1_miss", dk_count = all_dks.len(), "DK derivation failed");
let all_pks = providers.processing_keys();
if let Some(mk) = derive_media_key_from_pk(mkb, &all_pks) {
let vuk = derive_vuk(&mk, ctx.volume_id);
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path2_hit", "media key derived from processing key");
return Some(build(Some(vuk), derive_uks(&vuk), 2));
}
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path2_miss", pk_count = all_pks.len(), "PK derivation failed");
let mks = providers.media_keys();
let mut mk_hits: Vec<[u8; 16]> = Vec::new();
if let Some(mk_dv) = mkb_find_mk_dv(mkb) {
for mk in &mks {
let verifies = aes_ecb_decrypt(mk, &mk_dv)[..8]
== [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
if verifies && !mk_hits.contains(mk) {
mk_hits.push(*mk);
if mk_hits.len() > 1 {
break; }
}
}
}
if mk_hits.len() == 1 {
let vuk = derive_vuk(&mk_hits[0], ctx.volume_id);
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path2_5_hit", mk_pool = mks.len(), "media key from keydb MK-pool brute (km_verifies)");
return Some(build(Some(vuk), derive_uks(&vuk), 3));
}
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path2_5_miss", mk_pool = mks.len(), mk_hits = mk_hits.len(), "MK-pool brute: no unique verifying MK");
} else {
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_no_mkb", "no MKB; paths 1/2 skipped");
}
if let Some(entry) = providers.lookup_disc_by_vid(ctx.volume_id) {
if let Some(mk) = entry.media_key {
let vuk = derive_vuk(&mk, ctx.volume_id);
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path3_hit", "MK+VID entry matched volume_id");
return Some(build(Some(vuk), derive_uks(&vuk), 3));
}
}
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path3_miss", "no MK+VID entry matched volume_id");
} else {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_no_vid",
"VID unavailable; paths 1/2/3 require VID and are skipped"
);
}
if let Some(entry) = providers.lookup_disc_by_hash(&uk_file.disc_hash) {
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_keydb_hit_entry", "disc hash found in provider");
if let Some(vuk) = entry.vuk {
tracing::debug!(target: "freemkv::disc", phase = "resolve_keys_path4_hit", "VUK from provider");
return Some(build(Some(vuk), derive_uks(&vuk), 4));
} else if let Some(unit_keys) = match_keydb_unit_keys(&uk_file, &entry.unit_keys) {
tracing::debug!(
target: "freemkv::disc",
phase = "resolve_keys_path5_hit",
uk_count = unit_keys.len(),
"unit keys from provider (no VUK)"
);
return Some(build(None, unit_keys, 5));
}
tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_keydb_no_keys", "provider entry has neither VUK nor matching unit keys");
} else {
tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_keydb_miss", "disc hash NOT found in any provider");
}
None
}
fn match_keydb_unit_keys(
uk_file: &UnitKeyFile,
keydb_unit_keys: &[(u32, [u8; 16])],
) -> Option<Vec<(u32, [u8; 16])>> {
if keydb_unit_keys.is_empty() {
return None;
}
let mut matched = Vec::with_capacity(uk_file.encrypted_keys.len());
for (disc_num, _enc_key) in &uk_file.encrypted_keys {
let entry = keydb_unit_keys.iter().find(|(n, _)| n == disc_num)?;
matched.push(*entry);
}
Some(matched)
}
#[cfg(test)]
mod tests {
use super::super::decrypt::{ALIGNED_UNIT_LEN, aes_ecb_encrypt};
use super::super::keydb::{DiscEntry, KeyDb};
use super::*;
fn keydb_path() -> Option<std::path::PathBuf> {
let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
if path.exists() { Some(path) } else { None }
}
#[test]
fn derive_media_key_from_dk_survives_out_of_range_u_mask_shift() {
let mut mkb: Vec<u8> = Vec::new();
mkb.extend_from_slice(&[0x81, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&[0xAB; 16]);
mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]);
mkb.extend_from_slice(&[0x30, 0x00, 0x00, 0x00, 0x01]);
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&[0xCD; 16]);
let dk = DeviceKey {
key: [0x11; 16],
node: 1,
uv: 1,
u_mask_shift: 0x30, };
let _ = derive_media_key_from_dk(&mkb, &[dk]);
}
#[test]
fn test_vuk_derivation() {
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
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() {
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
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);
}
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() {
let unit_path = match std::env::var("ENCRYPTED_UNIT_PATH").ok() {
Some(p) => std::path::PathBuf::from(p),
None => return,
};
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_aacs_scrambled(&original),
"Unit should be encrypted"
);
let kp = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&kp).unwrap();
let candidate_entries: Vec<&DiscEntry> = db
.disc_entries
.values()
.filter(|e| !e.unit_keys.is_empty())
.collect();
eprintln!("Found {} entries with unit keys", candidate_entries.len());
for entry in &candidate_entries {
let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect();
let mut unit = original.clone();
if let Some(res) = super::super::decrypt::decrypt_unit_try_keys(&mut unit, &keys) {
eprintln!(
"SUCCESS: Decrypted with entry {} ({res:?})",
entry.disc_hash
);
let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count();
eprintln!(" TS sync bytes: {}/32", ts);
return;
}
}
eprintln!("No unit key worked (expected for AACS 2.0 BEE disc — needs read_data_key)");
}
#[test]
fn test_disc_hash() {
let data = b"test unit key ro inf data";
let hash = disc_hash(data);
assert_ne!(hash, [0u8; 20]);
assert_eq!(hash, disc_hash(data));
}
#[test]
fn test_disc_hash_hex() {
let hash = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
];
let hex = disc_hash_hex(&hash);
assert_eq!(hex, "0x000102030405060708090A0B0C0D0E0F10111213");
}
#[test]
fn test_parse_unit_key_ro_synthetic() {
let mut data = vec![0u8; 256];
data[0] = 0x00;
data[1] = 0x00;
data[2] = 0x00;
data[3] = 0x60;
data[16] = 1; data[17] = 1; data[18] = 0;
data[20] = 0;
data[21] = 1; data[22] = 0;
data[23] = 1; data[24] = 0;
data[25] = 1; data[28] = 0;
data[29] = 1;
let uk_pos = 0x60usize;
data[uk_pos] = 0;
data[uk_pos + 1] = 2;
let key1_pos = uk_pos + 48;
for i in 0..16 {
data[key1_pos + i] = 0xAA;
}
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); assert_eq!(parsed.encrypted_keys[0].1, [0xAA; 16]);
assert_eq!(parsed.encrypted_keys[1].0, 2); assert_eq!(parsed.encrypted_keys[1].1, [0xBB; 16]);
}
#[test]
fn mkb_version_recognizes_type_0x10() {
let mkb = [
0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D,
];
assert_eq!(mkb_version(&mkb), Some(77));
}
#[test]
fn mkb_content_len_trims_trailing_padding() {
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(&[0xAB; 16]);
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let records_len = mkb.len();
mkb.extend(std::iter::repeat(0u8).take(128 * 1024)); assert_eq!(mkb_content_len(&mkb), records_len);
assert_eq!(mkb_content_len(&mkb[..records_len]), records_len);
assert_eq!(mkb_content_len(&[]), 0);
}
#[test]
fn trim_mkb_never_zeroes_an_unrecognised_mkb() {
let unrecognised = vec![0xFFu8; 4096]; assert_eq!(
mkb_content_len(&unrecognised),
0,
"precondition: unparseable → 0"
);
assert_eq!(
trim_mkb(unrecognised.clone()),
unrecognised,
"unrecognised MKB must be returned untouched, never zeroed"
);
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(&[0xAB; 16]);
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let records_len = mkb.len();
mkb.extend(std::iter::repeat(0u8).take(1024));
assert_eq!(
trim_mkb(mkb).len(),
records_len,
"padded MKB trims to records"
);
assert!(trim_mkb(Vec::new()).is_empty());
}
#[test]
fn mkb_version_returns_none_on_empty() {
assert_eq!(mkb_version(&[]), None);
assert_eq!(mkb_version(&[0x10, 0x00]), None);
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() {
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,
];
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 probe_walk_pk_against_tables_accepts_planted_pk_rejects_corrupt() {
use super::super::decrypt::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];
let mut mk_raw = mk;
for a in 0..4 {
mk_raw[12 + a] ^= uv[a];
}
let cv = enc(&pk, &mk_raw); let mut vd = [0u8; 16];
vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
let mk_dv = enc(&mk, &vd);
let mut subdiff = vec![0u8];
subdiff.extend_from_slice(&uv);
assert_eq!(
probe::walk_pk_against_tables(std::slice::from_ref(&pk), &subdiff, &cv, &mk_dv, 1),
Some(mk),
"planted terminal PK must verify"
);
let mut bad = pk;
bad[0] ^= 0xFF;
assert_eq!(
probe::walk_pk_against_tables(std::slice::from_ref(&bad), &subdiff, &cv, &mk_dv, 1),
None,
"corrupted PK must be rejected"
);
}
#[test]
fn validate_processing_key_round_trip_with_nonzero_uv() {
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];
let mut mk_raw = mk;
for a in 0..4 {
mk_raw[12 + a] ^= uv[a];
}
let cvalue = enc(&pk, &mk_raw);
let mut plaintext_vd = [0u8; 16];
plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
plaintext_vd[8..].copy_from_slice(&[0x11; 8]);
let mk_dv = enc(&mk, &plaintext_vd);
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");
let mut wrong_pk = pk;
wrong_pk[0] ^= 0xFF;
assert!(validate_processing_key(&wrong_pk, &cvalue, &uv, &mk_dv).is_none());
let wrong_uv = [0x00u8, 0x00, 0x00, 0x00];
assert!(validate_processing_key(&pk, &cvalue, &wrong_uv, &mk_dv).is_none());
}
fn mkb_record(rec_type: u8, body: &[u8]) -> Vec<u8> {
let total = 4 + body.len();
let mut rec = Vec::with_capacity(total);
rec.push(rec_type);
rec.push(((total >> 16) & 0xFF) as u8);
rec.push(((total >> 8) & 0xFF) as u8);
rec.push((total & 0xFF) as u8);
rec.extend_from_slice(body);
rec
}
fn synth_aacs2_mkb(n: usize, decoy: usize) -> Vec<u8> {
let mut mkb = Vec::new();
mkb.extend_from_slice(&mkb_record(0x10, &[0, 0, 0, 0x20, 0, 0, 0, 0x52]));
mkb.extend_from_slice(&mkb_record(0x86, &[0xABu8; 16]));
let mut sd = Vec::with_capacity(n * 5);
for i in 0..n {
sd.push(0x00); sd.extend_from_slice(&((i as u32) + 1).to_be_bytes());
}
mkb.extend_from_slice(&mkb_record(0x04, &sd));
mkb.extend_from_slice(&mkb_record(0x07, &vec![0x11u8; decoy * 16])); mkb.extend_from_slice(&mkb_record(0x05, &vec![0x22u8; n * 16])); mkb
}
#[test]
fn cvalue_selection_prefers_0x05_over_0x07() {
let n = 1500;
let decoy = 96;
let mkb = synth_aacs2_mkb(n, decoy);
let sd = probe::mkb_subdiff(&mkb).expect("0x04 present");
let r05 = probe::mkb_record_body(&mkb, 0x05).expect("0x05 present");
let r07 = probe::mkb_record_body(&mkb, 0x07).expect("0x07 present");
let selected = mkb_find_cvalues(&mkb).expect("cvalues selected");
assert_eq!(sd.len() / 5, n, "0x04 SD index entry count");
assert_eq!(r05.len() / 16, n, "0x05 cvalue entry count");
assert_eq!(r07.len() / 16, decoy, "0x07 decoy entry count");
assert_eq!(
selected.len() / 16,
n,
"cvalue selection must use the large 0x05 table, not the {decoy}-entry 0x07 decoy"
);
assert_eq!(
selected, r05,
"selected body must be the 0x05 record verbatim"
);
assert_eq!(
selected.len() / 16,
sd.len() / 5,
"cvalue table must be 1:1 with the 0x04 Subset-Difference index"
);
}
#[test]
fn cvalue_selection_falls_back_to_0x07_when_no_0x05() {
let mut mkb = Vec::new();
mkb.extend_from_slice(&mkb_record(0x10, &[0, 0, 0, 0x10, 0, 0, 0, 1]));
mkb.extend_from_slice(&mkb_record(0x86, &[0xCDu8; 16]));
mkb.extend_from_slice(&mkb_record(0x04, &[0x00, 0, 0, 0, 1]));
let only07 = vec![0x33u8; 16];
mkb.extend_from_slice(&mkb_record(0x07, &only07));
assert!(probe::mkb_record_body(&mkb, 0x05).is_none());
let selected = mkb_find_cvalues(&mkb).expect("falls back to 0x07");
assert_eq!(selected, only07, "fallback returns the 0x07 body");
}
fn mkb_sample(rel: &str) -> Option<std::path::PathBuf> {
let dir = std::env::var("MKB_SAMPLE_DIR").ok()?;
let p = std::path::Path::new(&dir).join(rel);
if p.exists() { Some(p) } else { None }
}
#[test]
fn real_aacs2_samples_select_large_0x05_not_small_0x07() {
let samples = [
"sample-a/MKB_RO.inf",
"sample-b/MKB_RO.inf",
"sample-c/MKB_RO.inf",
];
let mut checked = 0;
for rel in samples {
let path = match mkb_sample(rel) {
Some(p) => p,
None => continue,
};
let data = std::fs::read(&path).expect("read sample MKB");
let r05 = probe::mkb_record_body(&data, 0x05)
.unwrap_or_else(|| panic!("{rel}: expected a 0x05 Media Key Data record"));
let r07 = probe::mkb_record_body(&data, 0x07)
.unwrap_or_else(|| panic!("{rel}: expected a 0x07 record"));
let sd = probe::mkb_subdiff(&data)
.unwrap_or_else(|| panic!("{rel}: expected a 0x04 Subset-Difference index"));
let n05 = r05.len() / 16;
let n07 = r07.len() / 16;
assert!(
n05 > n07 * 100,
"{rel}: 0x05 ({n05}) must dwarf 0x07 ({n07})"
);
assert_eq!(n05, 181270, "{rel}: full 0x05 cvalue table size");
assert_eq!(n07, 96, "{rel}: small 0x07 record size");
let selected = mkb_find_cvalues(&data)
.unwrap_or_else(|| panic!("{rel}: cvalue selection returned None"));
assert_eq!(
selected, r05,
"{rel}: selector must return the large 0x05 body, not 0x07"
);
let uv_entries = sd
.chunks(5)
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
.count();
assert!(
uv_entries >= n05 - 2 && uv_entries <= n05,
"{rel}: 0x04 UV count ({uv_entries}) should match 0x05 cvalue count ({n05})"
);
eprintln!(
"{rel}: 0x05={n05} cvalues, 0x07={n07}, 0x04 UVs={uv_entries} — selected 0x05"
);
checked += 1;
}
if checked == 0 {
eprintln!("no MKB samples present; skipping real-sample assertion");
}
}
#[test]
fn mkb_find_mk_dv_recognizes_type_0x86() {
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() {
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
let entry = db
.disc_entries
.values()
.find(|e| e.vuk.is_some() && !e.unit_keys.is_empty() && e.disc_id.is_some());
if entry.is_none() {
return;
}
let entry = entry.unwrap();
let vuk = entry.vuk.unwrap();
let vid = entry.disc_id.unwrap();
let hash_hex = format!("0x{}", entry.disc_hash.trim_start_matches("0x"));
let found = db.find_disc(&hash_hex);
assert!(found.is_some());
assert_eq!(found.unwrap().vuk, Some(vuk));
if let Some(mk) = entry.media_key {
let derived = derive_vuk(&mk, &vid);
assert_eq!(derived, vuk, "VUK derivation mismatch");
eprintln!("VUK derivation verified");
}
}
fn minimal_unit_key_ro() -> Vec<u8> {
let mut data = vec![0u8; 256];
data[3] = 0x60;
data[16] = 1; data[17] = 1; let uk_pos = 0x60usize;
data[uk_pos + 1] = 1; 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() {
let uk_ro = minimal_unit_key_ro();
let zero_vid = [0u8; 16];
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 providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &zero_vid,
providers,
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_path4_still_runs_when_vid_is_zero() {
let uk_ro = minimal_unit_key_ro();
let hash = disc_hash(&uk_ro);
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 providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &vid,
providers,
mkb: None,
};
let resolved =
resolve_keys_v1(&ctx).expect("path 4 must run regardless of VID availability");
assert_eq!(resolved.vuk, Some(known_vuk));
assert_eq!(resolved.key_source, 4);
}
#[test]
fn resolve_keys_path5_uses_keydb_unit_keys_when_vuk_absent() {
let uk_ro = minimal_unit_key_ro();
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let known_uk = [0xCDu8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: None,
unit_keys: vec![(1, known_uk)],
},
);
let vid = [0u8; 16];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &vid,
providers,
mkb: None,
};
let resolved =
resolve_keys_v1(&ctx).expect("path 5 must succeed when KEYDB carries unit keys");
assert_eq!(resolved.vuk, None, "path 5 has no VUK to return");
assert_eq!(resolved.key_source, 5);
assert_eq!(resolved.unit_keys, vec![(1, known_uk)]);
}
#[test]
fn resolve_keys_path5_rejects_partial_unit_key_coverage() {
let uk_ro = minimal_unit_key_ro();
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: None,
unit_keys: vec![(99, [0xEEu8; 16])],
},
);
let vid = [0u8; 16];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &vid,
providers,
mkb: None,
};
assert!(
resolve_keys_v1(&ctx).is_none(),
"partial CPS-unit coverage must not produce a half-decrypted result"
);
}
#[test]
fn resolve_keys_path2_5_mk_pool_brute_resolves_unkeyed_disc() {
use super::super::decrypt::aes_ecb_encrypt as enc;
let km = [0x11u8; 16];
let vid = [0x22u8; 16];
let mut vd = [0u8; 16];
vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
let mk_dv = enc(&km, &vd);
let mut mkb = mkb_record(0x10, &[0, 0, 0, 0x20, 0, 0, 0, 0x4D]);
mkb.extend_from_slice(&mkb_record(0x86, &mk_dv));
assert!(
probe::km_verifies(&mkb, &km),
"fixture: km must verify the MKB"
);
let uk_ro = minimal_unit_key_ro();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
"0xsibling".to_string(),
DiscEntry {
disc_hash: "0xsibling".to_string(),
title: "sibling".to_string(),
media_key: Some(km),
disc_id: Some([0x99u8; 16]),
vuk: None,
unit_keys: Vec::new(),
},
);
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &vid,
providers,
mkb: Some(&mkb),
};
let resolved = resolve_keys_v1(&ctx)
.expect("MK-pool brute (path 2.5) must resolve a disc whose MK is in keydb");
assert_eq!(
resolved.key_source, 3,
"MK-pool brute is the KEYDB-derived class"
);
assert_eq!(
resolved.vuk,
Some(derive_vuk(&km, &vid)),
"VUK must derive from the verified Km + this disc's VID"
);
}
#[test]
fn test_content_cert_parse() {
let mut data = vec![0u8; 16];
data[0] = 0x00; data[1] = 0x00; let cc = parse_content_cert(&data).unwrap();
assert_eq!(cc.version, AacsVersion::V10);
assert!(!cc.bus_encryption);
data[0] = 0x01; data[1] = 0x01; let cc = parse_content_cert(&data).unwrap();
assert_eq!(cc.version, AacsVersion::V20);
assert!(cc.bus_encryption);
}
#[test]
fn derive_vuk_matches_spec_relation_explicitly() {
use super::super::decrypt::aes_ecb_decrypt as dec;
let mk = [
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E, 0x1F,
];
let vid = [
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D,
0x2E, 0x2F,
];
let mut expected = dec(&mk, &vid);
for i in 0..16 {
expected[i] ^= vid[i];
}
assert_eq!(derive_vuk(&mk, &vid), expected);
}
#[test]
fn decrypt_unit_key_is_plain_aes_ecb_decrypt_under_vuk() {
use super::super::decrypt::aes_ecb_encrypt as enc;
let vuk = [0x9Eu8; 16];
let uk = [0x3Cu8; 16];
let enc_uk = enc(&vuk, &uk);
assert_eq!(decrypt_unit_key(&vuk, &enc_uk), uk);
}
fn build_unit_key_ro(num_uk: usize, stride: usize) -> Vec<u8> {
let uk_pos = 0x60usize;
let size = uk_pos + 48 + stride * num_uk + 64;
let mut data = vec![0u8; size];
data[0..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
data[16] = 1; data[17] = 1; data[uk_pos..uk_pos + 2].copy_from_slice(&(num_uk as u16).to_be_bytes());
let mut pos = uk_pos + 48;
for i in 0..num_uk {
for b in &mut data[pos..pos + 16] {
*b = 0x10 + i as u8;
}
pos += stride;
}
data
}
#[test]
fn stride_v10_is_48_v20_is_64_and_picks_distinct_keys() {
let data = build_unit_key_ro(2, 64);
let v20 = parse_unit_key_ro(&data, AacsVersion::V20).unwrap();
assert_eq!(v20.encrypted_keys.len(), 2);
assert_eq!(v20.encrypted_keys[0].1, [0x10; 16]);
assert_eq!(v20.encrypted_keys[1].1, [0x11; 16]);
let v10 = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert_eq!(v10.encrypted_keys[0].1, [0x10; 16]);
assert_ne!(
v10.encrypted_keys[1].1, [0x11; 16],
"48-byte stride must read different bytes than 64-byte stride"
);
}
#[test]
fn v21_uses_same_64_byte_stride_as_v20() {
let data = build_unit_key_ro(2, 64);
let v20 = parse_unit_key_ro(&data, AacsVersion::V20).unwrap();
let v21 = parse_unit_key_ro(&data, AacsVersion::V21).unwrap();
assert_eq!(v20.encrypted_keys, v21.encrypted_keys);
assert_eq!(v21.version, AacsVersion::V21);
}
#[test]
fn parse_unit_key_ro_rejects_too_short_header() {
assert!(parse_unit_key_ro(&[0u8; 19], AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_rejects_uk_pos_past_end() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&1000u32.to_be_bytes()); assert!(parse_unit_key_ro(&data, AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_zero_keys_returns_empty_set() {
let uk_pos = 0x60usize;
let mut data = vec![0u8; uk_pos + 48];
data[0..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
data[16] = 1;
let parsed = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert!(parsed.encrypted_keys.is_empty());
assert_eq!(parsed.app_type, 1);
}
#[test]
fn parse_unit_key_ro_truncated_key_region_returns_none() {
let uk_pos = 0x60usize;
let mut data = vec![0u8; uk_pos + 48 + 8]; data[0..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
data[uk_pos + 1] = 1; assert!(parse_unit_key_ro(&data, AacsVersion::V10).is_none());
}
#[test]
fn parse_unit_key_ro_stops_early_when_keys_run_off_end() {
let uk_pos = 0x60usize;
let stride = 48usize;
let size = uk_pos + 48 + stride + 16 + 8;
let mut data = vec![0u8; size];
data[0..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
data[uk_pos + 1] = 3; let parsed = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert_eq!(
parsed.encrypted_keys.len(),
2,
"must stop at the buffer end, not read past it"
);
}
#[test]
fn parse_unit_key_ro_app_type_and_skb_flag() {
let mut data = build_unit_key_ro(1, 48);
data[16] = 0x02;
data[17] = 0x05;
data[18] = 0x80; let p = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert_eq!(p.app_type, 0x02);
assert_eq!(p.num_bdmv_dir, 0x05);
assert!(p.use_skb_mkb, "bit 7 of byte 18 → use_skb_mkb true");
data[18] = 0x7F;
let p2 = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert!(!p2.use_skb_mkb);
}
#[test]
fn parse_unit_key_ro_cps_unit_numbers_are_1_based() {
let data = build_unit_key_ro(3, 48);
let p = parse_unit_key_ro(&data, AacsVersion::V10).unwrap();
assert_eq!(
p.encrypted_keys.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
vec![1, 2, 3]
);
}
#[test]
fn parse_unit_key_ro_title_cps_mapping_first_play_top_menu_then_titles() {
let mut data = build_unit_key_ro(2, 64);
data[20..22].copy_from_slice(&7u16.to_be_bytes()); data[22..24].copy_from_slice(&9u16.to_be_bytes()); data[24..26].copy_from_slice(&2u16.to_be_bytes()); data[28..30].copy_from_slice(&3u16.to_be_bytes()); data[32..34].copy_from_slice(&4u16.to_be_bytes()); let p = parse_unit_key_ro(&data, AacsVersion::V20).unwrap();
assert_eq!(p.title_cps_unit, vec![7, 9, 3, 4]);
}
#[test]
fn mkb_version_uses_be24_length_and_reads_offset_8() {
let mkb = [
0x10, 0x00, 0x00, 0x0C, 0x11, 0x22, 0x33, 0x44, 0x01, 0x02, 0x03, 0x04,
];
assert_eq!(mkb_version(&mkb), Some(0x0102_0304));
}
#[test]
fn mkb_find_mk_dv_skips_short_verify_record() {
let mut mkb = vec![0x81, 0x00, 0x00, 0x10]; mkb.extend_from_slice(&[0x00; 12]);
let expected = [0xC1u8; 16];
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]);
mkb.extend_from_slice(&expected);
mkb.extend_from_slice(&[0x00; 4]);
assert_eq!(mkb_find_mk_dv(&mkb), Some(expected));
}
#[test]
fn mkb_find_mk_dv_stops_on_overrun_length() {
let mkb = [0x81, 0x00, 0xFF, 0xFF, 0x00, 0x00]; assert_eq!(mkb_find_mk_dv(&mkb), None);
}
#[test]
fn mkb_find_mk_dv_stops_on_zero_length_record() {
let mkb = [0x81, 0x00, 0x00, 0x00, 0x99];
assert_eq!(mkb_find_mk_dv(&mkb), None);
}
#[test]
fn mkb_content_len_stops_at_zero_type_padding_byte() {
let mut mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1]; mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 9, 9, 9, 9]); let content = mkb.len();
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x08]); assert_eq!(mkb_content_len(&mkb), content);
}
#[test]
fn mkb_content_len_returns_full_len_when_no_padding() {
let mut mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x08, 9, 9, 9, 9]);
assert_eq!(mkb_content_len(&mkb), mkb.len());
}
#[test]
fn trim_mkb_leaves_exactly_sized_buffer_untouched() {
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
assert_eq!(trim_mkb(mkb.clone()), mkb);
}
#[test]
fn parse_content_cert_rejects_short_buffer() {
assert!(parse_content_cert(&[0x00; 7]).is_none());
}
#[test]
fn parse_content_cert_extracts_cc_id_and_nonzero_type_is_v20() {
let mut data = vec![0u8; 8];
data[0] = 0x02; data[1] = 0x00;
data[2..8].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
let cc = parse_content_cert(&data).unwrap();
assert_eq!(cc.version, AacsVersion::V20);
assert_eq!(cc.cc_id, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
assert!(!cc.bus_encryption);
}
#[test]
fn parse_content_cert_bus_encryption_only_reads_bit0() {
let mut data = vec![0u8; 8];
data[1] = 0x02; assert!(!parse_content_cert(&data).unwrap().bus_encryption);
data[1] = 0x03; assert!(parse_content_cert(&data).unwrap().bus_encryption);
}
#[test]
fn resolve_keys_v2_upgrades_to_v21_on_variant_mkb() {
let uk_ro = build_unit_key_ro(1, 64);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "fixture".to_string(),
media_key: None,
disc_id: None,
vuk: Some([0x5Au8; 16]),
unit_keys: Vec::new(),
},
);
let mut mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
mkb.extend_from_slice(&[0x83, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&[0x55; 16]);
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &[0u8; 16],
providers,
mkb: Some(&mkb),
};
let resolved = resolve_keys_v2(&ctx).expect("path 4 resolves");
assert_eq!(
resolved.version,
AacsVersion::V21,
"variant MKB must upgrade V20 result to V21"
);
}
#[test]
fn resolve_keys_v2_stays_v20_on_classical_mkb() {
let uk_ro = build_unit_key_ro(1, 64);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some([0x5Au8; 16]),
unit_keys: Vec::new(),
},
);
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &[0u8; 16],
providers,
mkb: Some(&mkb),
};
assert_eq!(resolve_keys_v2(&ctx).unwrap().version, AacsVersion::V20);
}
#[test]
fn resolve_keys_bus_encryption_flag_flows_from_content_cert() {
let uk_ro = build_unit_key_ro(1, 48);
let hash = disc_hash(&uk_ro);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some([1u8; 16]),
unit_keys: Vec::new(),
},
);
let mut cc = vec![0u8; 8];
cc[0] = 0x01;
cc[1] = 0x01;
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: Some(&cc),
volume_id: &[0u8; 16],
providers,
mkb: None,
};
assert!(resolve_keys_v1(&ctx).unwrap().bus_encryption);
}
#[test]
fn resolve_keys_v21_path4_resolves_by_hash() {
use super::super::decrypt::aes_ecb_encrypt as enc;
let data = build_unit_key_ro(1, 64);
let hash = disc_hash(&data);
let hash_hex = disc_hash_hex(&hash).to_lowercase();
let vuk = [0x77u8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
hash_hex.clone(),
DiscEntry {
disc_hash: hash_hex,
title: "f".to_string(),
media_key: None,
disc_id: None,
vuk: Some(vuk),
unit_keys: Vec::new(),
},
);
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &data,
content_cert: None,
volume_id: &[0u8; 16],
providers,
mkb: None,
};
let r = resolve_keys_v21(&ctx).expect("v21 path 4");
assert_eq!(r.version, AacsVersion::V21);
assert_eq!(r.key_source, 4);
assert_eq!(r.vuk, Some(vuk));
assert_eq!(r.unit_keys[0].1, decrypt_unit_key(&vuk, &[0x10u8; 16]));
assert_eq!(enc(&vuk, &r.unit_keys[0].1), [0x10u8; 16]);
}
#[test]
fn resolve_keys_path3_derives_vuk_from_vid_match() {
let uk_ro = minimal_unit_key_ro();
let vid = [0x42u8; 16];
let mk = [0x24u8; 16];
let mut keydb = KeyDb::empty();
keydb.disc_entries.insert(
"0xnotthishash".to_string(),
DiscEntry {
disc_hash: "0xnotthishash".to_string(),
title: "sibling".to_string(),
media_key: Some(mk),
disc_id: Some(vid),
vuk: None,
unit_keys: Vec::new(),
},
);
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &vid,
providers,
mkb: None, };
let r = resolve_keys_v1(&ctx).expect("path 3 by VID");
assert_eq!(r.key_source, 3);
assert_eq!(r.vuk, Some(derive_vuk(&mk, &vid)));
}
#[test]
fn resolve_keys_returns_none_when_no_provider_has_anything() {
let uk_ro = minimal_unit_key_ro();
let providers: &[&dyn super::super::KeyProvider] = &[];
let ctx = ResolveContext {
unit_key_ro: &uk_ro,
content_cert: None,
volume_id: &[0x42u8; 16],
providers,
mkb: None,
};
assert!(resolve_keys_v1(&ctx).is_none());
}
#[test]
fn match_keydb_unit_keys_empty_keydb_returns_none() {
let uk_file = parse_unit_key_ro(&minimal_unit_key_ro(), AacsVersion::V10).unwrap();
assert!(match_keydb_unit_keys(&uk_file, &[]).is_none());
}
#[test]
fn derive_media_key_from_dk_breaks_on_revoked_marker() {
let mut mkb: Vec<u8> = Vec::new();
mkb.extend_from_slice(&[0x81, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&[0xAB; 16]);
mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]);
mkb.extend_from_slice(&[0xC0, 0x00, 0x00, 0x00, 0x01]);
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&[0xCD; 16]);
let dk = DeviceKey {
key: [0x11; 16],
node: 1,
uv: 1,
u_mask_shift: 0,
};
assert!(derive_media_key_from_dk(&mkb, &[dk]).is_none());
}
#[test]
fn derive_media_key_from_dk_returns_none_when_records_missing() {
let mkb = vec![
0x81, 0x00, 0x00, 0x14, 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0,
];
assert!(derive_media_key_from_dk(&mkb, &[]).is_none());
}
#[test]
fn find_record_body_returns_none_for_empty_body_record() {
let mkb = [0x05, 0x00, 0x00, 0x04]; assert!(probe::mkb_record_body(&mkb, 0x05).is_none());
}
#[test]
fn derive_media_key_and_pk_from_dk_returns_intermediate_pk() {
use super::super::decrypt::aes_ecb_encrypt as enc;
let dk_bytes: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
0xFF, 0x00,
];
let expected_pk = aesg3(&dk_bytes, 1);
let mk: [u8; 16] = [
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
0xAE, 0xAF,
];
let uv_bytes: [u8; 4] = [0x00, 0x00, 0x00, 0x02];
let mut mk_raw = mk;
for a in 0..4 {
mk_raw[12 + a] ^= uv_bytes[a];
}
let cvalue = enc(&expected_pk, &mk_raw);
let mut plaintext_vd = [0u8; 16];
plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
plaintext_vd[8..].copy_from_slice(&[0x11; 8]);
let mk_dv = enc(&mk, &plaintext_vd);
let mut mkb = vec![
0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D,
];
mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]);
mkb.extend_from_slice(&[0x03]);
mkb.extend_from_slice(&uv_bytes);
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&cvalue);
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
mkb.extend_from_slice(&mk_dv);
let dk = DeviceKey {
key: dk_bytes,
node: 4,
uv: 2,
u_mask_shift: 3,
};
let dks = [dk];
let (got_mk, got_pk) = derive_media_key_and_pk_from_dk(&mkb, &dks)
.expect("classical DK boil must derive (mk, pk)");
assert_eq!(got_mk, mk, "recovered Media Key must match the planted MK");
assert_eq!(
got_pk, expected_pk,
"returned Processing Key must equal aesg3(dk, 1) for the no-op walk"
);
assert_eq!(derive_media_key_from_dk(&mkb, &dks), Some(mk));
}
}