test: assert full key bytes; correct keydb-format docs

Several tests asserted only key[0] (the first byte), which a byte-transposition bug could pass — assert the full 16-byte key everywhere, including a KAT that recomputes the expected VUK-derived keys directly. test_parse_disc_entry used identical all-zero placeholders for M/I/V/U, which couldn't catch a field mix-up; give each field a distinct byte pattern and assert every field.

Docs: keydb-format.md claimed DiscEntry::title was genuinely unused dead code, but it's read by to_keydb_cfg and the redacting Debug impl — correct the comment to only list empty/find_vuk as truly unused. Also cross-reference the two independent MAX_KEYDB_BYTES constants (keydb.rs's decompression cap and keydb_format.rs's on-disk load cap) so they're kept in sync, and bump Cargo.lock's version metadata.
This commit is contained in:
Matthew Jackson
2026-09-05 12:38:16 -07:00
parent 5bf6c4875b
commit 6e4f05dec4
5 changed files with 56 additions and 25 deletions
Generated
+5 -5
View File
@@ -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"
+6 -4
View File
@@ -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
+3 -3
View File
@@ -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.
+15 -10
View File
@@ -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]
+27 -3
View File
@@ -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 ────────────────────────────────