1use crate::error::AacsError;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct UnitKeyFileHeader {
30 pub application_type: u8,
33 pub num_of_bd_directory: u8,
35 pub use_skb_unified_mkb: bool,
38 pub bd_directories: Vec<BdDirectoryHeader>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct BdDirectoryHeader {
46 pub cps_unit_number_for_first_playback: u16,
48 pub cps_unit_number_for_top_menu: u16,
50 pub cps_unit_numbers_for_titles: Vec<u16>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CpsUnitRecord {
60 pub mac_of_pmsn: [u8; 16],
64 pub mac_of_device_binding_nonce: [u8; 16],
67 pub encrypted_cps_unit_key: [u8; 16],
69}
70
71#[derive(Debug, Clone)]
73pub struct UnitKeyFile {
74 pub unit_key_block_start_address: u32,
78 pub header: UnitKeyFileHeader,
80 pub cps_units: Vec<CpsUnitRecord>,
82}
83
84impl UnitKeyFile {
85 pub fn parse(bytes: &[u8]) -> Result<Self, AacsError> {
88 if bytes.len() < 16 {
89 return Err(AacsError::Truncated("Unit_Key_RO.inf"));
90 }
91 let unit_key_block_start_address =
92 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
93 let header = parse_header(&bytes[16..])?;
97
98 let kbs = unit_key_block_start_address as usize;
99 if kbs >= bytes.len() {
100 return Err(AacsError::OversizedRecord {
101 what: "Unit_Key_Block",
102 declared: kbs,
103 available: bytes.len(),
104 });
105 }
106 if kbs % 16 != 0 {
107 return Err(AacsError::InvalidValue {
108 what: "Unit_Key_Block_start_address (not 16-byte aligned)",
109 value: kbs as u64,
110 });
111 }
112 let cps_units = parse_unit_key_block(&bytes[kbs..])?;
113
114 Ok(Self {
115 unit_key_block_start_address,
116 header,
117 cps_units,
118 })
119 }
120}
121
122fn parse_header(slice: &[u8]) -> Result<UnitKeyFileHeader, AacsError> {
123 if slice.len() < 4 {
136 return Err(AacsError::Truncated("Unit_Key_File_Header"));
137 }
138 let application_type = slice[0];
139 let num_of_bd_directory = slice[1];
140 let use_skb_unified_mkb = (slice[2] & 0x80) != 0;
141 let mut cursor = 4;
143 let mut bd_directories = Vec::with_capacity(num_of_bd_directory as usize);
144 for _ in 0..num_of_bd_directory {
145 if cursor + 6 > slice.len() {
146 return Err(AacsError::Truncated("Unit_Key_File_Header (per-BD)"));
147 }
148 let cps_first = u16::from_be_bytes([slice[cursor], slice[cursor + 1]]);
149 let cps_topmenu = u16::from_be_bytes([slice[cursor + 2], slice[cursor + 3]]);
150 let num_titles = u16::from_be_bytes([slice[cursor + 4], slice[cursor + 5]]);
151 cursor += 6;
152 let mut titles = Vec::with_capacity(num_titles as usize);
153 for _ in 0..num_titles {
154 if cursor + 4 > slice.len() {
155 return Err(AacsError::Truncated("Unit_Key_File_Header (per-Title)"));
156 }
157 let _ = u16::from_be_bytes([slice[cursor], slice[cursor + 1]]);
159 let cps = u16::from_be_bytes([slice[cursor + 2], slice[cursor + 3]]);
160 titles.push(cps);
161 cursor += 4;
162 }
163 bd_directories.push(BdDirectoryHeader {
164 cps_unit_number_for_first_playback: cps_first,
165 cps_unit_number_for_top_menu: cps_topmenu,
166 cps_unit_numbers_for_titles: titles,
167 });
168 }
169 Ok(UnitKeyFileHeader {
170 application_type,
171 num_of_bd_directory,
172 use_skb_unified_mkb,
173 bd_directories,
174 })
175}
176
177fn parse_unit_key_block(slice: &[u8]) -> Result<Vec<CpsUnitRecord>, AacsError> {
178 if slice.len() < 16 {
186 return Err(AacsError::Truncated("Unit_Key_Block header"));
187 }
188 let n = u16::from_be_bytes([slice[0], slice[1]]) as usize;
189 let mut cursor: usize = 16; let need = n
191 .checked_mul(48)
192 .and_then(|n| cursor.checked_add(n))
193 .ok_or(AacsError::InvalidValue {
194 what: "Num_of_CPS_Unit (overflow)",
195 value: n as u64,
196 })?;
197 if need > slice.len() {
198 return Err(AacsError::OversizedRecord {
199 what: "Unit_Key_Block entries",
200 declared: need,
201 available: slice.len(),
202 });
203 }
204 let mut out = Vec::with_capacity(n);
205 for _ in 0..n {
206 let mut mac_pmsn = [0u8; 16];
207 let mut mac_dbn = [0u8; 16];
208 let mut enc_cuk = [0u8; 16];
209 mac_pmsn.copy_from_slice(&slice[cursor..cursor + 16]);
210 mac_dbn.copy_from_slice(&slice[cursor + 16..cursor + 32]);
211 enc_cuk.copy_from_slice(&slice[cursor + 32..cursor + 48]);
212 cursor += 48;
213 out.push(CpsUnitRecord {
214 mac_of_pmsn: mac_pmsn,
215 mac_of_device_binding_nonce: mac_dbn,
216 encrypted_cps_unit_key: enc_cuk,
217 });
218 }
219 Ok(out)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 pub(crate) fn build_minimal(n: u16) -> (Vec<u8>, Vec<[u8; 16]>) {
229 let kbs = 0x80u32;
233 let mut out = vec![0u8; kbs as usize];
234 out[0..4].copy_from_slice(&kbs.to_be_bytes());
236 out[16] = 0x01; out[17] = 0x01; out[18] = 0x00; out[19] = 0x00;
242 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());
249 out.extend_from_slice(&[0u8; 14]);
250 let mut encrypted_keys = Vec::with_capacity(n as usize);
251 for i in 0..n {
252 out.extend_from_slice(&[0u8; 16]); out.extend_from_slice(&[0u8; 16]); let mut k = [0u8; 16];
255 for (j, byte) in k.iter_mut().enumerate() {
256 *byte = ((i as u8).wrapping_add(j as u8)).wrapping_add(0x10);
257 }
258 out.extend_from_slice(&k);
259 encrypted_keys.push(k);
260 }
261 (out, encrypted_keys)
262 }
263
264 #[test]
265 fn parses_minimal_unit_key_file() {
266 let (bytes, keys) = build_minimal(2);
267 let parsed = UnitKeyFile::parse(&bytes).unwrap();
268 assert_eq!(parsed.unit_key_block_start_address, 0x80);
269 assert_eq!(parsed.header.application_type, 0x01);
270 assert_eq!(parsed.header.num_of_bd_directory, 1);
271 assert_eq!(parsed.cps_units.len(), 2);
272 assert_eq!(parsed.cps_units[0].encrypted_cps_unit_key, keys[0]);
273 assert_eq!(parsed.cps_units[1].encrypted_cps_unit_key, keys[1]);
274 }
275
276 #[test]
277 fn rejects_truncated_file() {
278 let bytes = vec![0u8; 4];
279 assert!(matches!(
280 UnitKeyFile::parse(&bytes),
281 Err(AacsError::Truncated(_))
282 ));
283 }
284
285 #[test]
286 fn rejects_misaligned_start_address() {
287 let mut bytes = vec![0u8; 64];
288 bytes[0..4].copy_from_slice(&33u32.to_be_bytes());
290 bytes[16] = 1;
292 bytes[17] = 1;
293 assert!(matches!(
294 UnitKeyFile::parse(&bytes),
295 Err(AacsError::InvalidValue { .. })
296 ));
297 }
298}