use crate::error::AacsError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnitKeyFileHeader {
pub application_type: u8,
pub num_of_bd_directory: u8,
pub use_skb_unified_mkb: bool,
pub bd_directories: Vec<BdDirectoryHeader>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BdDirectoryHeader {
pub cps_unit_number_for_first_playback: u16,
pub cps_unit_number_for_top_menu: u16,
pub cps_unit_numbers_for_titles: Vec<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CpsUnitRecord {
pub mac_of_pmsn: [u8; 16],
pub mac_of_device_binding_nonce: [u8; 16],
pub encrypted_cps_unit_key: [u8; 16],
}
#[derive(Debug, Clone)]
pub struct UnitKeyFile {
pub unit_key_block_start_address: u32,
pub header: UnitKeyFileHeader,
pub cps_units: Vec<CpsUnitRecord>,
}
impl UnitKeyFile {
pub fn parse(bytes: &[u8]) -> Result<Self, AacsError> {
if bytes.len() < 16 {
return Err(AacsError::Truncated("Unit_Key_RO.inf"));
}
let unit_key_block_start_address =
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let header = parse_header(&bytes[16..])?;
let kbs = unit_key_block_start_address as usize;
if kbs >= bytes.len() {
return Err(AacsError::OversizedRecord {
what: "Unit_Key_Block",
declared: kbs,
available: bytes.len(),
});
}
if kbs % 16 != 0 {
return Err(AacsError::InvalidValue {
what: "Unit_Key_Block_start_address (not 16-byte aligned)",
value: kbs as u64,
});
}
let cps_units = parse_unit_key_block(&bytes[kbs..])?;
Ok(Self {
unit_key_block_start_address,
header,
cps_units,
})
}
}
fn parse_header(slice: &[u8]) -> Result<UnitKeyFileHeader, AacsError> {
if slice.len() < 4 {
return Err(AacsError::Truncated("Unit_Key_File_Header"));
}
let application_type = slice[0];
let num_of_bd_directory = slice[1];
let use_skb_unified_mkb = (slice[2] & 0x80) != 0;
let mut cursor = 4;
let mut bd_directories = Vec::with_capacity(num_of_bd_directory as usize);
for _ in 0..num_of_bd_directory {
if cursor + 6 > slice.len() {
return Err(AacsError::Truncated("Unit_Key_File_Header (per-BD)"));
}
let cps_first = u16::from_be_bytes([slice[cursor], slice[cursor + 1]]);
let cps_topmenu = u16::from_be_bytes([slice[cursor + 2], slice[cursor + 3]]);
let num_titles = u16::from_be_bytes([slice[cursor + 4], slice[cursor + 5]]);
cursor += 6;
let mut titles = Vec::with_capacity(num_titles as usize);
for _ in 0..num_titles {
if cursor + 4 > slice.len() {
return Err(AacsError::Truncated("Unit_Key_File_Header (per-Title)"));
}
let _ = u16::from_be_bytes([slice[cursor], slice[cursor + 1]]);
let cps = u16::from_be_bytes([slice[cursor + 2], slice[cursor + 3]]);
titles.push(cps);
cursor += 4;
}
bd_directories.push(BdDirectoryHeader {
cps_unit_number_for_first_playback: cps_first,
cps_unit_number_for_top_menu: cps_topmenu,
cps_unit_numbers_for_titles: titles,
});
}
Ok(UnitKeyFileHeader {
application_type,
num_of_bd_directory,
use_skb_unified_mkb,
bd_directories,
})
}
fn parse_unit_key_block(slice: &[u8]) -> Result<Vec<CpsUnitRecord>, AacsError> {
if slice.len() < 16 {
return Err(AacsError::Truncated("Unit_Key_Block header"));
}
let n = u16::from_be_bytes([slice[0], slice[1]]) as usize;
let mut cursor: usize = 16; let need = n
.checked_mul(48)
.and_then(|n| cursor.checked_add(n))
.ok_or(AacsError::InvalidValue {
what: "Num_of_CPS_Unit (overflow)",
value: n as u64,
})?;
if need > slice.len() {
return Err(AacsError::OversizedRecord {
what: "Unit_Key_Block entries",
declared: need,
available: slice.len(),
});
}
let mut out = Vec::with_capacity(n);
for _ in 0..n {
let mut mac_pmsn = [0u8; 16];
let mut mac_dbn = [0u8; 16];
let mut enc_cuk = [0u8; 16];
mac_pmsn.copy_from_slice(&slice[cursor..cursor + 16]);
mac_dbn.copy_from_slice(&slice[cursor + 16..cursor + 32]);
enc_cuk.copy_from_slice(&slice[cursor + 32..cursor + 48]);
cursor += 48;
out.push(CpsUnitRecord {
mac_of_pmsn: mac_pmsn,
mac_of_device_binding_nonce: mac_dbn,
encrypted_cps_unit_key: enc_cuk,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn build_minimal(n: u16) -> (Vec<u8>, Vec<[u8; 16]>) {
let kbs = 0x80u32;
let mut out = vec![0u8; kbs as usize];
out[0..4].copy_from_slice(&kbs.to_be_bytes());
out[16] = 0x01; out[17] = 0x01; out[18] = 0x00; out[19] = 0x00;
out[20..22].copy_from_slice(&1u16.to_be_bytes()); out[22..24].copy_from_slice(&1u16.to_be_bytes()); out[24..26].copy_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&n.to_be_bytes());
out.extend_from_slice(&[0u8; 14]);
let mut encrypted_keys = Vec::with_capacity(n as usize);
for i in 0..n {
out.extend_from_slice(&[0u8; 16]); out.extend_from_slice(&[0u8; 16]); let mut k = [0u8; 16];
for (j, byte) in k.iter_mut().enumerate() {
*byte = ((i as u8).wrapping_add(j as u8)).wrapping_add(0x10);
}
out.extend_from_slice(&k);
encrypted_keys.push(k);
}
(out, encrypted_keys)
}
#[test]
fn parses_minimal_unit_key_file() {
let (bytes, keys) = build_minimal(2);
let parsed = UnitKeyFile::parse(&bytes).unwrap();
assert_eq!(parsed.unit_key_block_start_address, 0x80);
assert_eq!(parsed.header.application_type, 0x01);
assert_eq!(parsed.header.num_of_bd_directory, 1);
assert_eq!(parsed.cps_units.len(), 2);
assert_eq!(parsed.cps_units[0].encrypted_cps_unit_key, keys[0]);
assert_eq!(parsed.cps_units[1].encrypted_cps_unit_key, keys[1]);
}
#[test]
fn rejects_truncated_file() {
let bytes = vec![0u8; 4];
assert!(matches!(
UnitKeyFile::parse(&bytes),
Err(AacsError::Truncated(_))
));
}
#[test]
fn rejects_misaligned_start_address() {
let mut bytes = vec![0u8; 64];
bytes[0..4].copy_from_slice(&33u32.to_be_bytes());
bytes[16] = 1;
bytes[17] = 1;
assert!(matches!(
UnitKeyFile::parse(&bytes),
Err(AacsError::InvalidValue { .. })
));
}
}