diff --git a/Cargo.toml b/Cargo.toml index 7d361e9..8e3545e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,7 @@ path = "src/bin/freemkv_info.rs" [[bin]] name = "freemkv-test" path = "src/bin/freemkv_test.rs" + +[[bin]] +name = "aacs-test" +path = "src/bin/aacs_test.rs" diff --git a/src/aacs.rs b/src/aacs.rs index 3a8fd95..95cdd01 100644 --- a/src/aacs.rs +++ b/src/aacs.rs @@ -365,37 +365,421 @@ pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] { // ── Unit_Key_RO.inf parsing ───────────────────────────────────────────────── -/// Extract encrypted unit keys from Unit_Key_RO.inf data. -/// Returns vec of (cps_unit_number, encrypted_key). -pub fn parse_unit_key_ro(data: &[u8]) -> Vec<(u32, [u8; 16])> { - // Minimum size check - if data.len() < 0xA0 { - return Vec::new(); +/// 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, + /// Whether this is AACS 2.0 + pub aacs2: bool, + /// 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, +} + +/// Compute disc hash (SHA1 of Unit_Key_RO.inf content). +pub fn disc_hash(data: &[u8]) -> [u8; 20] { + use sha1::{Sha1, Digest}; + 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!("{:02X}", b)); + } + 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: 64-byte stride (48 + 16 extra) +pub fn parse_unit_key_ro(data: &[u8], aacs2: bool) -> Option { + if data.len() < 20 { + return None; } - // Number of unit keys at offset 0x10 (big-endian u16) - // But the first key is always at offset 0x90 - let mut keys = Vec::new(); + let hash = disc_hash(data); - // Read number of keys from offset 0x20 (varies by format) - // Simple approach: key table starts at 0x90, each key is 16 bytes - // First key is CPS unit 1 - let mut offset = 0x90; - let mut unit_num = 1u32; + // Header + let app_type = data[16]; + let num_bdmv_dir = data[17]; + let use_skb_mkb = (data[18] >> 7) & 1 == 1; - while offset + 16 <= data.len() { - let mut key = [0u8; 16]; - key.copy_from_slice(&data[offset..offset + 16]); - // Stop if we hit all zeros (no more keys) - if key == [0u8; 16] { + // 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, + aacs2, encrypted_keys: Vec::new(), title_cps_unit: Vec::new(), + }); + } + + // Stride between keys + let stride = if aacs2 { 64 } else { 48 }; + + // 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; } - keys.push((unit_num, key)); - unit_num += 1; - offset += 16; + let mut key = [0u8; 16]; + key.copy_from_slice(&data[pos..pos + 16]); + encrypted_keys.push(((i + 1) as u32, key)); + pos += stride; } - keys + // Title → CPS unit mapping + 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; // 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(cps); + } + } + } + + Some(UnitKeyFile { + disc_hash: hash, + app_type, + num_bdmv_dir, + use_skb_mkb, + aacs2, + encrypted_keys, + title_cps_unit, + }) +} + +// ── MKB processing ────────────────────────────────────────────────────────── + +/// Derive Media Key from MKB data using processing keys. +/// +/// Processing keys are pre-computed keys that work for specific MKB versions. +/// This is the fast path — no subset-difference tree traversal needed. +/// +/// MKB format: +/// Record type 0x10 = Verify Media Key Record (has mk_dv) +/// Record type 0x81 = Type and Version Record (has MKB version) +/// Record type 0x04 = Subset-Difference Index (has UVS entries) +/// Record type 0x07 = Explicit Subset-Difference Record (has cvalues) +pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> { + // Parse MKB records + let mk_dv = mkb_find_mk_dv(mkb)?; + let uvs = mkb_find_subdiff_records(mkb)?; + let cvalues = mkb_find_cvalues(mkb)?; + + // Count UV entries (each 5 bytes, stop when high bits set) + let num_uvs = uvs.chunks(5).take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0).count(); + + // Try each processing key against each UV/cvalue pair + for pk in processing_keys { + for i in 0..num_uvs { + let uv = &uvs[1 + i * 5..]; // skip first byte + 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. +fn validate_processing_key(pk: &[u8; 16], cvalue: &[u8], _uv: &[u8], mk_dv: &[u8; 16]) -> Option<[u8; 16]> { + if cvalue.len() < 16 { + return None; + } + // mk = AES-DEC(pk, cvalue) XOR cvalue + let mut cv = [0u8; 16]; + cv.copy_from_slice(&cvalue[..16]); + let mut mk = aes_ecb_decrypt(pk, &cv); + for i in 0..16 { + mk[i] ^= cv[i]; + } + + // Verify: AES-ECB(mk, mk_dv) should produce a specific pattern + let verify = aes_ecb_encrypt(&mk, mk_dv); + // mk_dv verification: the first 12 bytes of AES(mk, mk_dv) should be all 0xDEADBEEF... + // Actually per AACS spec: verify record value is AES(mk, all_zeros) + // No — the mk_dv IS the verification value. We compute AES-ECB(mk, verify_data) + // and check it matches. + // From libaacs _validate_pk: + // crypto_aes128d(pk, rec + a*16, mk) → decrypt cvalue with PK + // mk[i] ^= rec[i] → XOR with cvalue + // crypto_aes128e(mk, mk_dv, test) → encrypt mk_dv with derived mk + // if first 12 bytes of test are zero → valid media key + let test = aes_ecb_encrypt(&mk, mk_dv); + // AACS spec: Verify Media Key record — first 12 bytes must be zero + if test[..12] == [0u8; 12] { + return Some(mk); + } + None +} + +/// Find Verify Media Key Record (type 0x10) in MKB. +fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { + 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 >= 20 { + // mk_dv is at offset 4 (after record header) + let mut dv = [0u8; 16]; + dv.copy_from_slice(&mkb[pos + 4..pos + 20]); + return Some(dv); + } + pos += rec_len; + } + None +} + +/// Find Subset-Difference records (type 0x04) in MKB. +fn mkb_find_subdiff_records(mkb: &[u8]) -> Option> { + 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 +} + +/// Find Conditional Values (cvalues) record (type 0x07) in MKB. +fn mkb_find_cvalues(mkb: &[u8]) -> Option> { + 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 == 0x07 && rec_len > 4 { + return Some(mkb[pos + 4..pos + rec_len].to_vec()); + } + pos += rec_len; + } + None +} + +/// Get MKB version from Type and Version Record (type 0x81). +pub fn mkb_version(mkb: &[u8]) -> Option { + 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 == 0x81 && rec_len >= 8 { + return Some(u32::from_be_bytes([mkb[pos + 4], mkb[pos + 5], mkb[pos + 6], mkb[pos + 7]])); + } + pos += rec_len; + } + None +} + +// ── 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 version: false = AACS 1.0, true = AACS 2.0 + pub aacs2: bool, +} + +/// Parse a Content Certificate (ContentXXX.cer) file. +pub fn parse_content_cert(data: &[u8]) -> Option { + if data.len() < 8 { + return None; + } + + // Content Certificate format: + // [0] certificate type (0x00 = AACS1, 0x01 = AACS2) + // [1] bus_encryption_enabled (bit 0) + // [2..8] cc_id (6 bytes) + let aacs2 = data[0] != 0x00; + 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, + aacs2, + }) +} + +// ── Full VUK resolution chain ─────────────────────────────────────────────── + +/// Result of resolving a disc's VUK. +#[derive(Debug)] +pub struct ResolvedKeys { + /// Disc hash (SHA1 of Unit_Key_RO.inf) + pub disc_hash: [u8; 20], + /// Volume Unique Key + pub vuk: [u8; 16], + /// Decrypted unit keys (CPS unit number, key) + pub unit_keys: Vec<(u32, [u8; 16])>, + /// Title → CPS unit index mapping + pub title_cps_unit: Vec, + /// Whether AACS 2.0 + pub aacs2: bool, + /// Whether bus encryption is enabled (from Content Certificate) + pub bus_encryption: bool, +} + +/// Resolve all AACS keys for a disc given: +/// - Unit_Key_RO.inf raw data +/// - Content Certificate raw data (optional, for AACS version detection) +/// - Volume ID (from SCSI handshake) +/// - KEYDB +/// +/// Tries in order: +/// 1. Disc hash → KEYDB → VUK (fast path) +/// 2. KEYDB media key + volume ID → VUK (if disc hash not in KEYDB but MK is) +/// 3. MKB + processing keys → media key → VUK (full derivation) +pub fn resolve_keys( + unit_key_ro_data: &[u8], + content_cert_data: Option<&[u8]>, + volume_id: &[u8; 16], + keydb: &KeyDb, + mkb_data: Option<&[u8]>, +) -> Option { + // Detect AACS version + let aacs2 = content_cert_data + .and_then(|d| parse_content_cert(d)) + .map(|cc| cc.aacs2) + .unwrap_or(false); + + let bus_encryption = content_cert_data + .and_then(|d| parse_content_cert(d)) + .map(|cc| cc.bus_encryption) + .unwrap_or(false); + + // Parse Unit_Key_RO.inf + let uk_file = parse_unit_key_ro(unit_key_ro_data, aacs2)?; + + let hash_hex = disc_hash_hex(&uk_file.disc_hash); + + // Path 1: Look up VUK by disc hash in KEYDB + if let Some(entry) = keydb.find_disc(&hash_hex) { + if let Some(vuk) = entry.vuk { + // Decrypt unit keys with VUK + let unit_keys: Vec<(u32, [u8; 16])> = uk_file.encrypted_keys.iter() + .map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key))) + .collect(); + + return Some(ResolvedKeys { + disc_hash: uk_file.disc_hash, + vuk, + unit_keys, + title_cps_unit: uk_file.title_cps_unit, + aacs2, + bus_encryption, + }); + } + } + + // Path 2: Find entry with matching VID → derive VUK from MK + VID + for entry in keydb.disc_entries.values() { + if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { + if did == *volume_id { + let vuk = derive_vuk(&mk, volume_id); + let unit_keys: Vec<(u32, [u8; 16])> = uk_file.encrypted_keys.iter() + .map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key))) + .collect(); + + return Some(ResolvedKeys { + disc_hash: uk_file.disc_hash, + vuk, + unit_keys, + title_cps_unit: uk_file.title_cps_unit, + aacs2, + bus_encryption, + }); + } + } + } + + // Path 3: MKB + processing keys → media key → VUK + if let Some(mkb) = mkb_data { + if let Some(mk) = derive_media_key_from_pk(mkb, &keydb.processing_keys) { + let vuk = derive_vuk(&mk, volume_id); + let unit_keys: Vec<(u32, [u8; 16])> = uk_file.encrypted_keys.iter() + .map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key))) + .collect(); + + return Some(ResolvedKeys { + disc_hash: uk_file.disc_hash, + vuk, + unit_keys, + title_cps_unit: uk_file.title_cps_unit, + aacs2, + bus_encryption, + }); + } + } + + None } // ── Content decryption ────────────────────────────────────────────────────── @@ -750,14 +1134,14 @@ mod tests { fn test_parse_full_keydb() { let path = std::path::Path::new(""); if !path.exists() { return; } // skip if not available - + let db = KeyDb::load(path).unwrap(); - + assert_eq!(db.device_keys.len(), 4); assert_eq!(db.processing_keys.len(), 3); assert!(db.host_cert.is_some()); assert!(db.disc_entries.len() > 170000); - + // Look up Dune: Part Two let dune = db.disc_entries.values() .find(|e| e.title.contains("Dune: Part Two") && e.vuk.is_some()) @@ -765,8 +1149,132 @@ mod tests { assert!(dune.media_key.is_some()); assert!(dune.vuk.is_some()); assert!(!dune.unit_keys.is_empty()); - + eprintln!("Parsed {} disc entries, {} DK, {} PK", db.disc_entries.len(), db.device_keys.len(), db.processing_keys.len()); } + + #[test] + fn test_disc_hash() { + // SHA1 of a known byte sequence + let data = b"test unit key ro inf data"; + let hash = disc_hash(data); + assert_ne!(hash, [0u8; 20]); + // Same input → same hash + assert_eq!(hash, disc_hash(data)); + } + + #[test] + fn test_disc_hash_hex() { + let hash = [***REMOVED***]; + let hex = disc_hash_hex(&hash); + assert_eq!(hex, "***REMOVED***"); + } + + #[test] + fn test_parse_unit_key_ro_synthetic() { + // Build a synthetic Unit_Key_RO.inf + // Header: uk_pos at offset 0 (BE32), points to key storage + // Keys at uk_pos + 48 (16 bytes each, 48-byte stride for AACS 1.0) + let mut data = vec![0u8; 256]; + + // uk_pos = 0x60 (96) + data[0] = 0x00; data[1] = 0x00; data[2] = 0x00; data[3] = 0x60; + + // Header fields at 16-18 + data[16] = 1; // app_type = BD-ROM + data[17] = 1; // num_bdmv_dir + data[18] = 0; // no SKB + + // Title mapping at 20-25 + data[20] = 0; data[21] = 1; // first_play = CPS unit 1 + data[22] = 0; data[23] = 1; // top_menu = CPS unit 1 + data[24] = 0; data[25] = 1; // num_titles = 1 + // Title 0 entry: 2 bytes pad + CPS unit + data[28] = 0; data[29] = 1; // CPS unit 1 + + // Key storage at offset 0x60 + let uk_pos = 0x60usize; + data[uk_pos] = 0; data[uk_pos + 1] = 2; // 2 unit keys + + // Key 1 at uk_pos + 48 + let key1_pos = uk_pos + 48; + for i in 0..16 { data[key1_pos + i] = 0xAA; } + + // Key 2 at uk_pos + 48 + 48 + let key2_pos = key1_pos + 48; + for i in 0..16 { data[key2_pos + i] = 0xBB; } + + let parsed = parse_unit_key_ro(&data, false).unwrap(); + assert_eq!(parsed.app_type, 1); + assert_eq!(parsed.num_bdmv_dir, 1); + assert!(!parsed.aacs2); + assert_eq!(parsed.encrypted_keys.len(), 2); + assert_eq!(parsed.encrypted_keys[0].0, 1); // CPS unit 1 + assert_eq!(parsed.encrypted_keys[0].1, [0xAA; 16]); + assert_eq!(parsed.encrypted_keys[1].0, 2); // CPS unit 2 + assert_eq!(parsed.encrypted_keys[1].1, [0xBB; 16]); + } + + #[test] + fn test_mkb_version_parse() { + // Synthetic MKB with Type and Version record (0x81) + let mut mkb = vec![0u8; 32]; + // Record: type=0x81, length=12 (BE24) + mkb[0] = 0x81; + mkb[1] = 0x00; mkb[2] = 0x00; mkb[3] = 0x0C; + // Version = 77 + mkb[4] = 0x00; mkb[5] = 0x00; mkb[6] = 0x00; mkb[7] = 77; + + assert_eq!(mkb_version(&mkb), Some(77)); + } + + #[test] + fn test_resolve_keys_vuk_path() { + // Test the full resolve chain using VUK path + let path = std::path::Path::new(""); + if !path.exists() { return; } + let db = KeyDb::load(path).unwrap(); + + // Find V for Vendetta BD — has VUK and unit keys + // hash: ***REMOVED*** + let entry = db.find_disc("***REMOVED***"); + if entry.is_none() { return; } + let entry = entry.unwrap(); + let vuk = entry.vuk.unwrap(); + let vid = entry.disc_id.unwrap(); + + // We need the actual Unit_Key_RO.inf from the disc to compute disc hash. + // Since we don't have it, we can at least test that the KEYDB lookup + // works with a known hash. + let hash_hex = "***REMOVED***"; + let found = db.find_disc(hash_hex); + assert!(found.is_some()); + assert_eq!(found.unwrap().vuk, Some(vuk)); + + // Verify VUK derivation if we have MK + VID + if let Some(mk) = entry.media_key { + let derived = derive_vuk(&mk, &vid); + assert_eq!(derived, vuk, "VUK derivation mismatch for V for Vendetta"); + eprintln!("V for Vendetta VUK derivation verified"); + } + } + + #[test] + fn test_content_cert_parse() { + // AACS 1.0 cert + let mut data = vec![0u8; 16]; + data[0] = 0x00; // AACS 1.0 + data[1] = 0x00; // no bus encryption + let cc = parse_content_cert(&data).unwrap(); + assert!(!cc.aacs2); + assert!(!cc.bus_encryption); + + // AACS 2.0 with bus encryption + data[0] = 0x01; // AACS 2.0 + data[1] = 0x01; // bus encryption enabled + let cc = parse_content_cert(&data).unwrap(); + assert!(cc.aacs2); + assert!(cc.bus_encryption); + } } diff --git a/src/bin/aacs_test.rs b/src/bin/aacs_test.rs new file mode 100644 index 0000000..133508f --- /dev/null +++ b/src/bin/aacs_test.rs @@ -0,0 +1,105 @@ +//! aacs-test — Test AACS handshake against a real drive. +//! +//! Usage: aacs-test /dev/sr0 /path/to/keydb.cfg + +use std::env; +use std::path::Path; + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: aacs-test "); + std::process::exit(1); + } + + let device = Path::new(&args[1]); + let keydb_path = Path::new(&args[2]); + + println!("aacs-test v{}", env!("CARGO_PKG_VERSION")); + println!(); + + // Open drive + print!("Opening {}... ", device.display()); + let mut session = match libfreemkv::DriveSession::open(device) { + Ok(s) => { println!("OK"); s } + Err(e) => { println!("FAILED: {}", e); std::process::exit(1); } + }; + println!(" Drive: {} {}", session.profile.drive_id.trim(), session.profile.chipset.name()); + + // Load KEYDB + print!("Loading KEYDB... "); + let keydb = match libfreemkv::aacs::KeyDb::load(keydb_path) { + Ok(db) => { + println!("OK ({} disc entries, {} DK, {} PK)", + db.disc_entries.len(), db.device_keys.len(), db.processing_keys.len()); + db + } + Err(e) => { println!("FAILED: {}", e); std::process::exit(1); } + }; + + let host_cert = match &keydb.host_cert { + Some(hc) => { + println!(" Host cert: {} bytes, priv_key[0]=0x{:02x}", + hc.certificate.len(), hc.private_key[0]); + hc + } + None => { println!(" No host cert in KEYDB"); std::process::exit(1); } + }; + + // AACS handshake + println!(); + print!("AACS authenticate... "); + let mut auth = match libfreemkv::aacs_handshake::aacs_authenticate( + &mut session, + &host_cert.private_key, + &host_cert.certificate, + ) { + Ok(a) => { + println!("OK"); + println!(" Bus key: {:02x?}", &a.bus_key); + println!(" AGID: {}", a.agid); + println!(" Drive cert type: 0x{:02x}", a.drive_cert[0]); + a + } + Err(e) => { + println!("FAILED: {}", e); + std::process::exit(1); + } + }; + + // Read Volume ID + print!("Reading Volume ID... "); + match libfreemkv::aacs_handshake::read_volume_id(&mut session, &mut auth) { + Ok(vid) => { + println!("OK"); + println!(" VID: {:02x?}", vid); + + // Try to find matching disc in KEYDB + let matched = keydb.disc_entries.values() + .find(|e| e.disc_id == Some(vid)); + if let Some(entry) = matched { + println!(" KEYDB match: {} (hash {})", entry.title, entry.disc_hash); + if let Some(vuk) = entry.vuk { + println!(" VUK: {:02x?}", vuk); + } + } else { + println!(" No exact VID match in KEYDB"); + } + } + Err(e) => println!("FAILED: {}", e), + } + + // Read data keys (AACS 2.0) + print!("Reading data keys... "); + match libfreemkv::aacs_handshake::read_data_keys(&mut session, &mut auth) { + Ok((rdk, wdk)) => { + println!("OK (AACS 2.0 bus encryption)"); + println!(" Read data key: {:02x?}", rdk); + println!(" Write data key: {:02x?}", wdk); + } + Err(e) => println!("not available: {} (likely AACS 1.0)", e), + } + + println!(); + println!("Done."); +} diff --git a/src/disc.rs b/src/disc.rs index 4627767..3fc6e4e 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -384,7 +384,7 @@ impl Disc { session: &mut DriveSession, keydb_path: &std::path::Path, ) -> Result { - use crate::aacs::{KeyDb, derive_vuk, decrypt_unit_key}; + use crate::aacs::{self, KeyDb}; use crate::aacs_handshake; // Load KEYDB @@ -396,76 +396,53 @@ impl Disc { detail: "no host certificate in KEYDB".into(), })?; - // Authenticate with drive + // Step 1: SCSI handshake → bus key + Volume ID let mut auth = aacs_handshake::aacs_authenticate( session, &host_cert.private_key, &host_cert.certificate, )?; - // Read Volume ID let vid = aacs_handshake::read_volume_id(session, &mut auth)?; - // Try to read data keys (AACS 2.0) - let (read_data_key, bus_encryption) = match aacs_handshake::read_data_keys(session, &mut auth) { - Ok((rdk, _wdk)) => (Some(rdk), true), - Err(_) => (None, false), + // Try to read data keys (AACS 2.0 bus encryption) + let read_data_key = match aacs_handshake::read_data_keys(session, &mut auth) { + Ok((rdk, _wdk)) => Some(rdk), + Err(_) => None, }; - // Compute disc hash (SHA1 of Unit_Key_RO.inf) for KEYDB lookup - // First try: look up by VID-derived entries - // The KEYDB has entries indexed by disc_hash, but we can also - // find entries that match our MK+VID combination + // Step 2: Read Unit_Key_RO.inf from disc via UDF + let udf_fs = udf::read_filesystem(session)?; + let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf") + .or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf")) + .map_err(|_| Error::AacsError { + detail: "failed to read Unit_Key_RO.inf from disc".into(), + })?; - // Try all entries — find one whose MK+VID produces a VUK that decrypts unit keys - let mut found_vuk = None; + // Step 3: Read Content Certificate (optional — for AACS version detection) + let cc_data = udf_fs.read_file(session, "/AACS/Content000.cer") + .or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer")) + .ok(); - // First: try entries that have a disc_id matching our VID - for entry in keydb.disc_entries.values() { - if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { - if did == vid { - let vuk = derive_vuk(&mk, &vid); - found_vuk = Some((vuk, entry.unit_keys.clone())); - break; - } - } - } - - // Fallback: if we have a VUK that works, use it - if found_vuk.is_none() { - for entry in keydb.disc_entries.values() { - if let Some(vuk) = entry.vuk { - if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { - if did == vid { - found_vuk = Some((vuk, entry.unit_keys.clone())); - break; - } - } - } - } - } - - let (vuk, keydb_unit_keys) = found_vuk.ok_or_else(|| Error::AacsError { - detail: format!("no matching disc found in KEYDB for VID {:02x?}", &vid[..4]), + // Step 4: Resolve all keys via the full chain + // Path 1: disc hash → KEYDB → VUK (fast, 99% of discs) + // Path 2: KEYDB media key + VID → VUK + // Path 3: MKB + processing keys → media key → VUK (fallback) + let resolved = aacs::resolve_keys( + &uk_ro_data, + cc_data.as_deref(), + &vid, + &keydb, + None, // MKB: TODO read via REPORT DISC STRUCTURE 0x83 + ).ok_or_else(|| Error::AacsError { + detail: "failed to resolve AACS keys".into(), })?; - // If KEYDB has pre-decrypted unit keys, use them directly - // Otherwise we'd need to read Unit_Key_RO.inf and decrypt with VUK - let unit_keys = if !keydb_unit_keys.is_empty() { - keydb_unit_keys - } else { - // Would need to read AACS/Unit_Key_RO.inf from disc and decrypt - // For now, require KEYDB to have unit keys - return Err(Error::AacsError { - detail: "no unit keys in KEYDB entry — Unit_Key_RO.inf parsing not yet implemented".into(), - }); - }; - Ok(AacsState { - vuk, - unit_keys, + vuk: resolved.vuk, + unit_keys: resolved.unit_keys, read_data_key, - bus_encryption, + bus_encryption: resolved.bus_encryption, }) }