mirror of
https://github.com/freemkv/freemkv-keysources.git
synced 2026-10-07 18:13:11 +00:00
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:
Generated
+5
-5
@@ -306,7 +306,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freemkv-keysources"
|
name = "freemkv-keysources"
|
||||||
version = "1.6.14"
|
version = "1.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -319,7 +319,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freemkv-unlock"
|
name = "freemkv-unlock"
|
||||||
version = "1.6.14"
|
version = "1.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -534,7 +534,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "1.6.14"
|
version = "1.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -1271,8 +1271,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[patch.unused]]
|
[[patch.unused]]
|
||||||
name = "freemkv-engine"
|
name = "freemkv-engine"
|
||||||
version = "1.6.14"
|
version = "1.7.0"
|
||||||
|
|
||||||
[[patch.unused]]
|
[[patch.unused]]
|
||||||
name = "freemkv-i18n"
|
name = "freemkv-i18n"
|
||||||
version = "1.6.14"
|
version = "1.7.0"
|
||||||
|
|||||||
@@ -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
|
function has ever existed in this crate; the real consumer is
|
||||||
`KeydbSource::unit_keys_from` (keydb.rs), which reads `unit_keys`, `vuk`,
|
`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
|
`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 —
|
kat_c / kat_d KATs pin — it is NOT unused). `DiscEntry::title` is likewise
|
||||||
`empty`, `find_vuk`, `DiscEntry::title` — are part of the faithful copy and
|
read — by `to_keydb_cfg` and the redacting `Debug` impl — so it is not
|
||||||
are retained rather than pruned; allow dead_code so the byte-for-byte copy
|
unused either. The genuinely unused items — `empty` and `find_vuk` — are
|
||||||
compiles clean without diverging from the libfreemkv original.
|
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
|
## ParseStats — what `KeyDb::parse` threw away
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -21,9 +21,9 @@ use libfreemkv::{Error, KeySource};
|
|||||||
|
|
||||||
use crate::keydb_format::KeyDb;
|
use crate::keydb_format::KeyDb;
|
||||||
|
|
||||||
// Upper bound on decompressed keydb size: caps a decompression bomb (a tiny
|
// Upper bound on decompressed keydb size (decompression-bomb cap): a tiny
|
||||||
// zip/gz could otherwise inflate to GiB and OOM the daily refresh thread).
|
// zip/gz could otherwise inflate to GiB and OOM the refresh thread. Mirrors
|
||||||
// The public UHD keydb is ~62 MiB and growing; 128 MiB leaves headroom.
|
// keydb_format::MAX_KEYDB_BYTES (the on-disk load cap); keep the two equal.
|
||||||
const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024;
|
const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024;
|
||||||
|
|
||||||
/// Result of a KEYDB save/update -- path written, entry count, and byte size.
|
/// Result of a KEYDB save/update -- path written, entry count, and byte size.
|
||||||
|
|||||||
+15
-10
@@ -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.
|
/// A keydb per-disc unit key: the CPS-unit number paired with its 16-byte key.
|
||||||
pub type NumberedUnitKey = (u32, [u8; 16]);
|
pub type NumberedUnitKey = (u32, [u8; 16]);
|
||||||
|
|
||||||
// Upper bound on the on-disk keydb.cfg size accepted by `KeyDb::load`. The
|
// 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
|
// public UHD keydb is ~62 MiB; 128 MiB bounds a hostile/corrupt file).
|
||||||
// allocation from a hostile or corrupt file.
|
// Mirror of keydb.rs's MAX_KEYDB_BYTES decompression cap; keep them equal.
|
||||||
const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024;
|
const MAX_KEYDB_BYTES: u64 = 128 * 1024 * 1024;
|
||||||
|
|
||||||
/// Upper bound on parsed disc entries. The real public keydb carries
|
/// Upper bound on parsed disc entries. The real public keydb carries
|
||||||
@@ -1017,18 +1017,23 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_disc_entry() {
|
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 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!(
|
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();
|
let entry = KeyDb::parse_disc_entry(&line).unwrap();
|
||||||
assert_eq!(entry.title, "SAMPLE_FILM (Sample Film)"); // faithful, verbatim
|
assert_eq!(entry.title, "SAMPLE_FILM (Sample Film)"); // faithful, verbatim
|
||||||
assert!(entry.media_key.is_some());
|
assert_eq!(entry.media_key, Some([0x11u8; 16]), "M field");
|
||||||
assert!(entry.vuk.is_some());
|
assert_eq!(entry.vid, Some([0x22u8; 16]), "I field");
|
||||||
assert_eq!(entry.unit_keys.len(), 1);
|
assert_eq!(entry.vuk, Some([0x33u8; 16]), "V field");
|
||||||
assert_eq!(entry.unit_keys[0].0, 1);
|
assert_eq!(entry.unit_keys, vec![(1u32, [0x44u8; 16])], "U field");
|
||||||
|
assert_eq!(entry.mkb_version, Some(77));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+27
-3
@@ -1049,8 +1049,16 @@ mod tests {
|
|||||||
assert_eq!(keys.len(), 2);
|
assert_eq!(keys.len(), 2);
|
||||||
assert_eq!(keys[0].idx, 0);
|
assert_eq!(keys[0].idx, 0);
|
||||||
assert_eq!(keys[1].idx, 1);
|
assert_eq!(keys[1].idx, 1);
|
||||||
assert_eq!(keys[0].key[0], 0x00);
|
// Full 16 bytes of each key, not just byte 0 — a byte-transposition bug
|
||||||
assert_eq!(keys[1].key[0], 0x0f);
|
// 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)
|
/// 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");
|
.expect("a string UK must still resolve");
|
||||||
assert_eq!(keys.len(), 1);
|
assert_eq!(keys.len(), 1);
|
||||||
assert_eq!(keys[0].idx, 0);
|
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,
|
// 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[0].idx, 0);
|
||||||
assert_eq!(keys[1].idx, 1);
|
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
|
/// 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)
|
let keys = interpret_reply(Ok(reply(200, &at_cap)), &BareCtx, 1)
|
||||||
.expect("a reply exactly at the cap is legal and must be read");
|
.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");
|
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 ────────────────────────────────
|
// ── A bad port is CONFIG, not an outage ────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user