Skip to main content

oxideav_aacs/
unit_key.rs

1//! Unit_Key_RO.inf parser per BD-Prerecorded spec §3.9.3.
2//!
3//! Layout (Table 3-12):
4//!
5//! ```text
6//! +----------------------------------------+
7//! | Unit_Key_Block_start_address (32 bits) | offset 0
8//! +----------------------------------------+
9//! | Reserved (96 bits)                     |
10//! +----------------------------------------+
11//! | Unit_Key_File_Header()                 | (Table 3-13)
12//! +----------------------------------------+
13//! | padding (16-byte aligned)              |
14//! +----------------------------------------+
15//! | Unit_Key_Block()                       | (Table 3-15)
16//! +----------------------------------------+
17//! | padding to 65536-byte boundary         |
18//! +----------------------------------------+
19//! ```
20//!
21//! The Unit_Key_Block() per Table 3-15 holds, for each CPS Unit, the
22//! 16-byte `MAC of PMSN`, the 16-byte `MAC of Device Binding Nonce`,
23//! and the 16-byte `Encrypted CPS Unit Key`.
24
25use crate::error::AacsError;
26
27/// Parsed `Unit_Key_File_Header()` per BD-Prerecorded §3.9.3 Table 3-13.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct UnitKeyFileHeader {
30    /// `Application_Type` — `0x01` for BDMV (the only value the spec
31    /// currently defines).
32    pub application_type: u8,
33    /// `Num_of_BD_Directory` — `0x01` for BDMV.
34    pub num_of_bd_directory: u8,
35    /// `Use_SKB_Unified_MKB_Flag` — `true` if Sequence Key Blocks
36    /// and Unified MKBs are used on the disc.
37    pub use_skb_unified_mkb: bool,
38    /// Per BD-directory listings of CPS unit numbers for First
39    /// Playback / Top Menu / Titles.
40    pub bd_directories: Vec<BdDirectoryHeader>,
41}
42
43/// Per BD-Application-directory listing inside `Unit_Key_File_Header()`.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct BdDirectoryHeader {
46    /// CPS_Unit_number that First Playback maps to (0 if none).
47    pub cps_unit_number_for_first_playback: u16,
48    /// CPS_Unit_number that Top Menu maps to (0 if none).
49    pub cps_unit_number_for_top_menu: u16,
50    /// CPS_Unit_number assigned to each Title in this directory.
51    /// Note: spec Table 3-13 indexes titles from `J=1`, so this list
52    /// is 1-indexed in the *spec* but stored 0-indexed here.
53    pub cps_unit_numbers_for_titles: Vec<u16>,
54}
55
56/// One per-CPS-Unit record from `Unit_Key_Block()` per BD-Prerecorded
57/// §3.9.3 Table 3-15.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct CpsUnitRecord {
60    /// `MAC of Pre-recorded Media Serial Number` per spec text:
61    /// `CMAC(K_cu, PMSN)`. All-zero when this CPS Unit is not bound
62    /// to the PMSN.
63    pub mac_of_pmsn: [u8; 16],
64    /// `MAC of Device Binding Nonce` per spec: `CMAC(K_cu, DBN)`.
65    /// All-zero when this CPS Unit is not bound to the player.
66    pub mac_of_device_binding_nonce: [u8; 16],
67    /// `Encrypted CPS Unit Key` = `AES-128E(K_vu, K_cu)` per spec.
68    pub encrypted_cps_unit_key: [u8; 16],
69}
70
71/// A parsed `Unit_Key_RO.inf` file.
72#[derive(Debug, Clone)]
73pub struct UnitKeyFile {
74    /// `Unit_Key_Block_start_address` field — byte offset from the
75    /// start of the file at which `Unit_Key_Block()` begins. Always
76    /// a multiple of 16.
77    pub unit_key_block_start_address: u32,
78    /// Parsed file header.
79    pub header: UnitKeyFileHeader,
80    /// Per-CPS-Unit records from `Unit_Key_Block()`.
81    pub cps_units: Vec<CpsUnitRecord>,
82}
83
84impl UnitKeyFile {
85    /// Parse a `Unit_Key_RO.inf` byte stream per BD-Prerecorded spec
86    /// §3.9.3.
87    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        // Bytes [4..16] are 96 reserved bits.
94
95        // Header starts at byte 16.
96        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    // Layout per Table 3-13:
124    //   Application_Type            (8 bits)
125    //   Num_of_BD_Directory         (8 bits)
126    //   Use_SKB_Unified_MKB_Flag    (1 bit)
127    //   reserved                    (15 bits)
128    //   For each BD directory:
129    //     CPS_Unit_number for First Playback#I  (16 bits)
130    //     CPS_Unit_number for Top Menu#I        (16 bits)
131    //     Num_of_Title#I                        (16 bits)
132    //     For J=1..Num_of_Title:
133    //       reserved                            (16 bits)
134    //       CPS_Unit_number for Title#J         (16 bits)
135    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    // slice[2] low 7 bits + slice[3] are reserved.
142    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            // 16-bit reserved then 16-bit CPS_Unit_number
158            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    // Layout per Table 3-15:
179    //   Num_of_CPS_Unit (16 bits)
180    //   reserved        (112 bits = 14 bytes)
181    //   for I=1..Num_of_CPS_Unit:
182    //     MAC of PMSN                  (128 bits)
183    //     MAC of Device Binding Nonce  (128 bits)
184    //     Encrypted CPS Unit Key       (128 bits)
185    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; // 2 + 14
190    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    /// Build a minimal Unit_Key_RO.inf with `n` CPS units, each
227    /// initialised to per-unit deterministic 16-byte patterns.
228    pub(crate) fn build_minimal(n: u16) -> (Vec<u8>, Vec<[u8; 16]>) {
229        // Header layout: 4-byte start address + 12 reserved + header
230        // bytes. We'll put the Unit_Key_Block at offset 0x80 (128) for
231        // simplicity (16-aligned, comfortably past the header).
232        let kbs = 0x80u32;
233        let mut out = vec![0u8; kbs as usize];
234        // start address big-endian
235        out[0..4].copy_from_slice(&kbs.to_be_bytes());
236        // bytes 4..16 reserved zero (already)
237        // header bytes start at 16
238        out[16] = 0x01; // Application_Type = BDMV
239        out[17] = 0x01; // Num_of_BD_Directory = 1
240        out[18] = 0x00; // use_skb_unified_mkb = 0, reserved 0
241        out[19] = 0x00;
242        // per-BD: 6 bytes (first 16b, top menu 16b, num_titles 16b)
243        out[20..22].copy_from_slice(&1u16.to_be_bytes()); // First Playback -> unit 1
244        out[22..24].copy_from_slice(&1u16.to_be_bytes()); // Top Menu -> unit 1
245        out[24..26].copy_from_slice(&0u16.to_be_bytes()); // Num_of_Title = 0 (titles aren't required for our parser tests)
246
247        // Unit_Key_Block: 2-byte n, 14 reserved, then n*48 entries.
248        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]); // MAC of PMSN
253            out.extend_from_slice(&[0u8; 16]); // MAC of DBN
254            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        // start address = 33 (not 16-aligned)
289        bytes[0..4].copy_from_slice(&33u32.to_be_bytes());
290        // give it a minimal valid header at offset 16
291        bytes[16] = 1;
292        bytes[17] = 1;
293        assert!(matches!(
294            UnitKeyFile::parse(&bytes),
295            Err(AacsError::InvalidValue { .. })
296        ));
297    }
298}