diff --git a/.claude/worktrees/agent-a1b258ca2bea02faf b/.claude/worktrees/agent-a1b258ca2bea02faf new file mode 160000 index 0000000..aa536f6 --- /dev/null +++ b/.claude/worktrees/agent-a1b258ca2bea02faf @@ -0,0 +1 @@ +Subproject commit aa536f6ab30e71b19683b9819c69ac04dfc08b97 diff --git a/.claude/worktrees/agent-afd076fb7145099f7 b/.claude/worktrees/agent-afd076fb7145099f7 new file mode 160000 index 0000000..5950156 --- /dev/null +++ b/.claude/worktrees/agent-afd076fb7145099f7 @@ -0,0 +1 @@ +Subproject commit 5950156cf0c727d8afa57cfe0a52ac72b257ac2e diff --git a/.claude/worktrees/halt-token b/.claude/worktrees/halt-token new file mode 160000 index 0000000..8693add --- /dev/null +++ b/.claude/worktrees/halt-token @@ -0,0 +1 @@ +Subproject commit 8693add66c5df6fbb8ce55cdbd497652a0319f6f diff --git a/.claude/worktrees/pes-source-sink b/.claude/worktrees/pes-source-sink new file mode 160000 index 0000000..919096b --- /dev/null +++ b/.claude/worktrees/pes-source-sink @@ -0,0 +1 @@ +Subproject commit 919096b67f3318169fe1a407a36a07088ba3ce53 diff --git a/.claude/worktrees/pipeline b/.claude/worktrees/pipeline new file mode 160000 index 0000000..8e7d1be --- /dev/null +++ b/.claude/worktrees/pipeline @@ -0,0 +1 @@ +Subproject commit 8e7d1bea96ba58984a89315f1ca9a511f4d4bbfc diff --git a/.claude/worktrees/round1-fixes b/.claude/worktrees/round1-fixes new file mode 160000 index 0000000..78b50dd --- /dev/null +++ b/.claude/worktrees/round1-fixes @@ -0,0 +1 @@ +Subproject commit 78b50dd5a63691ead9df9b083daedad132928f7d diff --git a/.claude/worktrees/round2-decrypt-decorator b/.claude/worktrees/round2-decrypt-decorator new file mode 160000 index 0000000..9e51ee9 --- /dev/null +++ b/.claude/worktrees/round2-decrypt-decorator @@ -0,0 +1 @@ +Subproject commit 9e51ee994660b82abf17f58108648b7ac752616b diff --git a/.claude/worktrees/round2-discstream-source b/.claude/worktrees/round2-discstream-source new file mode 160000 index 0000000..f10c83f --- /dev/null +++ b/.claude/worktrees/round2-discstream-source @@ -0,0 +1 @@ +Subproject commit f10c83ffe41d90cf274dfc268a8053e6d28557e4 diff --git a/.claude/worktrees/round2-framesink b/.claude/worktrees/round2-framesink new file mode 160000 index 0000000..eb04fda --- /dev/null +++ b/.claude/worktrees/round2-framesink @@ -0,0 +1 @@ +Subproject commit eb04fdaffa99437ba8ec0f41be5618efca6909c2 diff --git a/.claude/worktrees/round2-halt b/.claude/worktrees/round2-halt new file mode 160000 index 0000000..3ca6323 --- /dev/null +++ b/.claude/worktrees/round2-halt @@ -0,0 +1 @@ +Subproject commit 3ca63235e0c7a3ccf5add029c81050eb0cb50e7e diff --git a/.claude/worktrees/sector-source-sink b/.claude/worktrees/sector-source-sink new file mode 160000 index 0000000..8c592d0 --- /dev/null +++ b/.claude/worktrees/sector-source-sink @@ -0,0 +1 @@ +Subproject commit 8c592d08e90cef7d388e38f1192d520edef07b20 diff --git a/.claude/worktrees/writeback-file-rename b/.claude/worktrees/writeback-file-rename new file mode 160000 index 0000000..e5a32a8 --- /dev/null +++ b/.claude/worktrees/writeback-file-rename @@ -0,0 +1 @@ +Subproject commit e5a32a8f16e6d3f46479d54ad7fae844adf65021 diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index d6724b7..f5d1862 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -247,6 +247,7 @@ fn validate_processing_key( /// Find Verify Media Key Record (type 0x10) in MKB. fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { let mut pos = 0; + let mut type10_seen: Vec<(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; @@ -254,14 +255,32 @@ fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { break; } + if rec_type == 0x10 { + type10_seen.push((pos, rec_len)); + } + 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]); + tracing::warn!( + target: "freemkv::disc", + phase = "mkb_mk_dv_found", + 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", + type10_seen = ?type10_seen, + scanned_bytes = pos, + "no 0x10 record with rec_len>=20 found" + ); None } @@ -628,32 +647,70 @@ pub fn resolve_keys( } }; + tracing::warn!( + target: "freemkv::disc", + phase = "resolve_keys_start", + aacs2, + bus_encryption, + disc_hash = %hash_hex, + mkb_present = mkb_data.is_some(), + "resolve_keys: starting" + ); + // Path 1: Look up VUK by disc hash in KEYDB if let Some(entry) = keydb.find_disc(&hash_hex) { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_hit_entry", "disc hash found in keydb"); if let Some(vuk) = entry.vuk { return Some(build(vuk, 1)); } + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_no_vuk", "disc hash entry has no VUK"); + } else { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path1_miss", "disc hash NOT in keydb"); } // Path 2: Find entry with matching VID → derive VUK from MK + VID + let mut path2_mk_did_count = 0usize; for entry in keydb.disc_entries.values() { if let (Some(mk), Some(did)) = (entry.media_key, entry.disc_id) { + path2_mk_did_count += 1; if did == *volume_id { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_hit", "MK+VID entry matched volume_id"); return Some(build(derive_vuk(&mk, volume_id), 2)); } } } + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path2_miss", mk_did_entries = path2_mk_did_count, "no MK+VID entry matched volume_id"); // Path 3: MKB + processing keys → media key → VUK if let Some(mkb) = mkb_data { + let mk_dv = mkb_find_mk_dv(mkb); + let subdiff = mkb_find_subdiff_records(mkb); + let cvalues = mkb_find_cvalues(mkb); + tracing::warn!( + 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" + ); + if let Some(mk) = derive_media_key_from_pk(mkb, &keydb.processing_keys) { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path3_hit", "media key derived from processing key"); return Some(build(derive_vuk(&mk, volume_id), 3)); } + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path3_miss", pk_count = keydb.processing_keys.len(), "PK derivation failed"); // Path 4: MKB + device keys → processing key → media key → VUK if let Some(mk) = derive_media_key_from_dk(mkb, &keydb.device_keys) { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path4_hit", "media key derived from device key"); return Some(build(derive_vuk(&mk, volume_id), 4)); } + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_path4_miss", dk_count = keydb.device_keys.len(), "DK derivation failed"); + } else { + tracing::warn!(target: "freemkv::disc", phase = "resolve_keys_no_mkb", "no MKB data available; paths 3/4 skipped"); } None diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index effaa6f..f2b8a45 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -22,12 +22,27 @@ impl Disc { ) -> Option { use crate::aacs::{self, KeyDb}; - let keydb_path = opts.resolve_keydb()?; + tracing::warn!( + target: "freemkv::disc", + phase = "handshake_entry", + "do_handshake entered" + ); + let keydb_path = match opts.resolve_keydb() { + Some(p) => p, + None => { + tracing::warn!( + target: "freemkv::disc", + phase = "handshake_no_keydb", + "no KEYDB found in search paths; handshake skipped" + ); + return None; + } + }; let keydb = match KeyDb::load(&keydb_path) { Ok(db) => db, Err(e) => { tracing::warn!( - target: "freemkv::aacs", + target: "freemkv::disc", phase = "handshake_keydb_load_failed", io_error_kind = ?e.kind(), keydb = %keydb_path.display(), @@ -38,11 +53,12 @@ impl Disc { }; let host_cert_count = keydb.host_certs.len(); - tracing::debug!( - target: "freemkv::aacs", + tracing::warn!( + target: "freemkv::disc", phase = "handshake_start", host_cert_count, keydb = %keydb_path.display(), + "handshake starting" ); const MAX_CERT_ATTEMPTS: usize = 16; @@ -54,7 +70,7 @@ impl Disc { Ok(vid) => vid, Err(e) => { tracing::warn!( - target: "freemkv::aacs", + target: "freemkv::disc", phase = "handshake_vid_read_failed", cert_index = idx, error_code = e.code(), @@ -67,7 +83,7 @@ impl Disc { .ok() .map(|(rdk, _)| rdk); tracing::debug!( - target: "freemkv::aacs", + target: "freemkv::disc", phase = "handshake_ok", cert_index = idx, has_read_data_key = read_data_key.is_some(), @@ -84,7 +100,7 @@ impl Disc { } } tracing::warn!( - target: "freemkv::aacs", + target: "freemkv::disc", phase = "handshake_all_certs_failed", host_cert_count, tried = host_cert_count.min(MAX_CERT_ATTEMPTS), @@ -118,6 +134,19 @@ impl Disc { .or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf")) .map_err(|_| Error::AacsNoKeys)?; + // Log the disc hash so we can confirm whether it's present in KEYDB + // when key resolution fails. The disc hash is SHA-1 of the full + // Unit_Key_RO.inf file bytes — same value KEYDB.cfg keys VUK entries by. + let dh = crate::aacs::disc_hash(&uk_ro_data); + let dh_hex = crate::aacs::disc_hash_hex(&dh); + tracing::warn!( + target: "freemkv::disc", + phase = "scan_aacs_disc_hash", + disc_hash = %dh_hex, + uk_ro_len = uk_ro_data.len(), + "disc hash computed (compare with keydb.cfg entries)" + ); + let cc_data = udf_fs .read_file(reader, "/AACS/Content000.cer") .or_else(|_| udf_fs.read_file(reader, "/AACS/Content001.cer")) @@ -129,6 +158,28 @@ impl Disc { .ok(); let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version); + let mkb_first_64_hex = mkb_data + .as_deref() + .map(|m| { + m.iter() + .take(64) + .map(|b| format!("{b:02x}")) + .collect::() + }) + .unwrap_or_default(); + tracing::warn!( + target: "freemkv::disc", + phase = "scan_aacs_mkb_info", + mkb_present = mkb_data.is_some(), + mkb_len = mkb_data.as_deref().map(|m| m.len()).unwrap_or(0), + mkb_version = ?mkb_ver, + mkb_first_64 = %mkb_first_64_hex, + keydb_disc_count = keydb.disc_entries.len(), + keydb_dk_count = keydb.device_keys.len(), + keydb_pk_count = keydb.processing_keys.len(), + "AACS resolution inputs" + ); + // Use handshake volume ID if available, otherwise zeros // (KEYDB VUK lookup by disc hash works without volume ID) let volume_id = handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]); diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index d8c7008..55dc363 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -204,7 +204,10 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result