diff --git a/Cargo.lock b/Cargo.lock index 122b372..1cf0665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,7 @@ dependencies = [ [[package]] name = "freemkv-keysources" -version = "1.6.14" +version = "1.7.0" dependencies = [ "base64", "flate2", @@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "freemkv-unlock" -version = "1.6.14" +version = "1.7.0" dependencies = [ "aes", "base64", @@ -534,7 +534,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libfreemkv" -version = "1.6.14" +version = "1.7.0" dependencies = [ "aes", "base64", @@ -1271,8 +1271,8 @@ dependencies = [ [[patch.unused]] name = "freemkv-engine" -version = "1.6.14" +version = "1.7.0" [[patch.unused]] name = "freemkv-i18n" -version = "1.6.14" +version = "1.7.0" diff --git a/docs/keydb-format.md b/docs/keydb-format.md index 645bd71..e2b9cee 100644 --- a/docs/keydb-format.md +++ b/docs/keydb-format.md @@ -10,10 +10,12 @@ revision of this comment cited a `candidates_from` function — no such function has ever existed in this crate; the real consumer is `KeydbSource::unit_keys_from` (keydb.rs), which reads `unit_keys`, `vuk`, `media_key` AND `vid` (`vid` is load-bearing for the MK+VID derivation the -kat_c / kat_d KATs pin — it is NOT unused). The genuinely unused items — -`empty`, `find_vuk`, `DiscEntry::title` — are part of the faithful copy and -are retained rather than pruned; allow dead_code so the byte-for-byte copy -compiles clean without diverging from the libfreemkv original. +kat_c / kat_d KATs pin — it is NOT unused). `DiscEntry::title` is likewise +read — by `to_keydb_cfg` and the redacting `Debug` impl — so it is not +unused either. The genuinely unused items — `empty` and `find_vuk` — are +part of the faithful copy and are retained rather than pruned; allow +dead_code so the byte-for-byte copy compiles clean without diverging from +the libfreemkv original. ## ParseStats — what `KeyDb::parse` threw away diff --git a/src/keydb.rs b/src/keydb.rs index 3930657..3327108 100644 --- a/src/keydb.rs +++ b/src/keydb.rs @@ -21,9 +21,9 @@ use libfreemkv::{Error, KeySource}; use crate::keydb_format::KeyDb; -// Upper bound on decompressed keydb size: caps a decompression bomb (a tiny -// zip/gz could otherwise inflate to GiB and OOM the daily refresh thread). -// The public UHD keydb is ~62 MiB and growing; 128 MiB leaves headroom. +// Upper bound on decompressed keydb size (decompression-bomb cap): a tiny +// zip/gz could otherwise inflate to GiB and OOM the refresh thread. Mirrors +// keydb_format::MAX_KEYDB_BYTES (the on-disk load cap); keep the two equal. const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024; /// Result of a KEYDB save/update -- path written, entry count, and byte size. diff --git a/src/keydb_format.rs b/src/keydb_format.rs index 0d6e45a..a048251 100644 --- a/src/keydb_format.rs +++ b/src/keydb_format.rs @@ -17,9 +17,9 @@ use libfreemkv::aacs::types::{DeviceKey, HostCert}; /// A keydb per-disc unit key: the CPS-unit number paired with its 16-byte key. pub type NumberedUnitKey = (u32, [u8; 16]); -// Upper bound on the on-disk keydb.cfg size accepted by `KeyDb::load`. The -// public UHD keydb is ~62 MiB and growing; 128 MiB bounds worst-case -// allocation from a hostile or corrupt file. +// Upper bound on the on-disk keydb.cfg size accepted by `KeyDb::load` (the +// public UHD keydb is ~62 MiB; 128 MiB bounds a hostile/corrupt file). +// Mirror of keydb.rs's MAX_KEYDB_BYTES decompression cap; keep them equal. const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024; /// Upper bound on parsed disc entries. The real public keydb carries @@ -1017,18 +1017,23 @@ mod tests { #[test] fn test_parse_disc_entry() { - // All-zero placeholders — synthetic; no real key material in code. + // Synthetic placeholders, DISTINCT per field so a field mix-up (M read + // as V, I as U, …) is caught — no real key material in code. let z40 = "00".repeat(20); - let z32 = "00".repeat(16); + let mk = "11".repeat(16); + let id = "22".repeat(16); + let vuk = "33".repeat(16); + let uk = "44".repeat(16); let line = format!( - "0x{z40} = SAMPLE_FILM (Sample Film) | D | 2024-01-01 | M | 0x{z32} | I | 0x{z32} | V | 0x{z32} | U | 1-0x{z32} ; MKBv77" + "0x{z40} = SAMPLE_FILM (Sample Film) | D | 2024-01-01 | M | 0x{mk} | I | 0x{id} | V | 0x{vuk} | U | 1-0x{uk} ; MKBv77" ); let entry = KeyDb::parse_disc_entry(&line).unwrap(); assert_eq!(entry.title, "SAMPLE_FILM (Sample Film)"); // faithful, verbatim - assert!(entry.media_key.is_some()); - assert!(entry.vuk.is_some()); - assert_eq!(entry.unit_keys.len(), 1); - assert_eq!(entry.unit_keys[0].0, 1); + assert_eq!(entry.media_key, Some([0x11u8; 16]), "M field"); + assert_eq!(entry.vid, Some([0x22u8; 16]), "I field"); + assert_eq!(entry.vuk, Some([0x33u8; 16]), "V field"); + assert_eq!(entry.unit_keys, vec![(1u32, [0x44u8; 16])], "U field"); + assert_eq!(entry.mkb_version, Some(77)); } #[test] diff --git a/src/online.rs b/src/online.rs index 29e017b..c64533b 100644 --- a/src/online.rs +++ b/src/online.rs @@ -1049,8 +1049,16 @@ mod tests { assert_eq!(keys.len(), 2); assert_eq!(keys[0].idx, 0); assert_eq!(keys[1].idx, 1); - assert_eq!(keys[0].key[0], 0x00); - assert_eq!(keys[1].key[0], 0x0f); + // Full 16 bytes of each key, not just byte 0 — a byte-transposition bug + // would pass a first-byte-only check. + assert_eq!( + keys[0].key, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + ); + assert_eq!( + keys[1].key, + [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + ); } /// Backward-compatible form: `"UK"` as a bare hex STRING (not an array) @@ -1065,7 +1073,11 @@ mod tests { .expect("a string UK must still resolve"); assert_eq!(keys.len(), 1); assert_eq!(keys[0].idx, 0); - assert_eq!(keys[0].key[0], 0x00); + // Full 16-byte key, not just byte 0. + assert_eq!( + keys[0].key, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + ); } // A malformed `"UK"` STRING falls through to the genuine-miss path, @@ -1117,6 +1129,12 @@ mod tests { ); assert_eq!(keys[0].idx, 0); assert_eq!(keys[1].idx, 1); + // Full-byte KAT: the derived keys must equal the local VUK boil over + // the disc's encrypted title keys (the same primitive the code calls). + let vuk = parse_uk(vuk_hex).unwrap(); + let expected = crate::uks_from_vuk(&vuk, &[[0x11u8; 16], [0x22u8; 16]]); + assert_eq!(keys[0].key, expected[0].key); + assert_eq!(keys[1].key, expected[1].key); } /// The service answered correctly with a VUK, but the DISC's encrypted @@ -1289,6 +1307,12 @@ mod tests { let keys = interpret_reply(Ok(reply(200, &at_cap)), &BareCtx, 1) .expect("a reply exactly at the cap is legal and must be read"); assert_eq!(keys.len(), 1, "the key in an at-cap reply must survive"); + // The surviving key must be the planted UK bytes, not merely present. + assert_eq!( + keys[0].key, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "the at-cap key must be the exact planted UK" + ); } // ── A bad port is CONFIG, not an outage ────────────────────────────────