mirror of
https://github.com/freemkv/freemkv-keysources.git
synced 2026-10-07 18:13:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65e08c6dc2 | ||
|
|
10c6838506 | ||
|
|
1911e362a8 | ||
|
|
74160f00e9 | ||
|
|
22c37f1b50 | ||
|
|
00dd77d49b | ||
|
|
fe78d7445f |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [1.4.2] — 2026-07-15
|
||||
|
||||
Version sync with the workspace; inherits libfreemkv 1.4.2. The keydb test that
|
||||
feeds real key material into the AACS crypto was adapted to the segregated
|
||||
`decrypt_unit` + `is_clean` primitives (behaviour unchanged).
|
||||
|
||||
## [1.4.1] — 2026-07-14
|
||||
|
||||
Version sync with the workspace; inherits libfreemkv 1.4.1.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "freemkv-keysources"
|
||||
version = "1.4.1"
|
||||
version = "1.4.4"
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
license = "MIT"
|
||||
@@ -40,4 +40,4 @@ codegen-units = 1
|
||||
# overrides it with a path patch via the gitignored .cargo/config.toml (a
|
||||
# config-level [patch.crates-io] wins over this manifest one for the same crate).
|
||||
[patch.crates-io]
|
||||
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.4.1" }
|
||||
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.4.4" }
|
||||
|
||||
+15
-15
@@ -1412,7 +1412,7 @@ mod tests {
|
||||
//
|
||||
// These exercise the parser (KeyDb::load) end-to-end against a real
|
||||
// keydb.cfg and feed its material into libfreemkv's AACS crypto
|
||||
// (derive_vuk / decrypt_unit_try_keys). They live here now that the
|
||||
// (derive_vuk, then decrypt_unit + is_clean_ts). They live here now that the
|
||||
// parser lives here. All are KEYDB_PATH-env-gated and no-op in CI when
|
||||
// the env is unset; they must still COMPILE.
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
@@ -1466,7 +1466,7 @@ mod tests {
|
||||
let original = std::fs::read(&unit_path).unwrap();
|
||||
assert_eq!(original.len(), libfreemkv::aacs::content::ALIGNED_UNIT_LEN);
|
||||
assert!(
|
||||
libfreemkv::aacs::content::ts_sync_destroyed(&original),
|
||||
!libfreemkv::aacs::content::is_clean(&original, libfreemkv::disc::ContentFormat::BdTs),
|
||||
"Unit should be encrypted"
|
||||
);
|
||||
|
||||
@@ -1485,20 +1485,20 @@ mod tests {
|
||||
|
||||
eprintln!("Found {} entries with unit keys", candidate_entries.len());
|
||||
|
||||
// Try each entry's unit keys
|
||||
// Try each entry's unit keys: apply the key, then ask whether it opened
|
||||
// the unit (the segregated primitives — decrypt, then structural check).
|
||||
for entry in &candidate_entries {
|
||||
let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect();
|
||||
let mut unit = original.clone();
|
||||
|
||||
if let Some(res) = libfreemkv::aacs::content::decrypt_unit_try_keys(&mut unit, &keys) {
|
||||
eprintln!(
|
||||
"SUCCESS: Decrypted with entry {} ({res:?})",
|
||||
entry.disc_hash
|
||||
);
|
||||
// Count TS sync bytes
|
||||
let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count();
|
||||
eprintln!(" TS sync bytes: {}/32", ts);
|
||||
return;
|
||||
for (_, key) in &entry.unit_keys {
|
||||
let mut unit = original.clone();
|
||||
libfreemkv::aacs::content::decrypt_unit(&mut unit, key);
|
||||
if libfreemkv::aacs::content::is_clean(&unit, libfreemkv::disc::ContentFormat::BdTs)
|
||||
{
|
||||
eprintln!("SUCCESS: Decrypted with entry {}", entry.disc_hash);
|
||||
// Count TS sync bytes
|
||||
let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count();
|
||||
eprintln!(" TS sync bytes: {}/32", ts);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ mod paths;
|
||||
|
||||
pub use keydb::{KeydbSource, UpdateResult};
|
||||
pub use keydb_format::{DiscEntry, KeyDb};
|
||||
pub use online::{OnlineSource, validate_keyserver_url};
|
||||
pub use online::{MIN_SAMPLE_UNITS, OnlineSource, validate_keyserver_url};
|
||||
pub use paths::{default_keydb_path, existing_keydb_path, keydb_search_paths};
|
||||
|
||||
// Re-exported for downstream convenience so apps need only depend on this crate
|
||||
|
||||
+62
-17
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
use crate::uks_from_vuk;
|
||||
use base64::Engine;
|
||||
use libfreemkv::aacs::types::UnitKey;
|
||||
use libfreemkv::keysource::ResolveCtx;
|
||||
use libfreemkv::keysource::{DecodeSampleSet, ResolveCtx};
|
||||
use libfreemkv::{Error, KeySource};
|
||||
|
||||
// Upper bound on the MKB forwarded to the key service — kept in lockstep with
|
||||
@@ -16,6 +16,19 @@ use libfreemkv::{Error, KeySource};
|
||||
// record stream is normally a few MiB; this is headroom, not an expected size).
|
||||
const MAX_MKB_BYTES: usize = 64 * 1024 * 1024;
|
||||
const TIMEOUT_SECS: u64 = 180;
|
||||
/// Minimum encrypted-content samples the online source will send in one key
|
||||
/// request — re-exported from the base crate ([`libfreemkv::keysource::MIN_SAMPLE_UNITS`])
|
||||
/// so this crate and libfreemkv's own FMTS forensic query share ONE value.
|
||||
///
|
||||
/// The service identifies the key by which of the submitted units it decrypts,
|
||||
/// so too few samples — especially on FMTS, where a segment interleaves several
|
||||
/// variants at the unit level — can return a key that matches an incidental unit
|
||||
/// rather than the one asked about (a false positive). A request carrying fewer
|
||||
/// is refused (empty result → the resolver moves to the next source) rather than
|
||||
/// sent and trusted. Kept public so callers that GATHER the samples (the CLI,
|
||||
/// autorip) sample at least this many — sampling fewer guarantees the request is
|
||||
/// skipped and the online source never consulted.
|
||||
pub use libfreemkv::keysource::MIN_SAMPLE_UNITS;
|
||||
/// Hard cap on the key-service response body. A real unit-key reply is a few
|
||||
/// hundred bytes; bound the read so a malicious/compromised server can't drive
|
||||
/// the client to OOM with an unbounded body.
|
||||
@@ -219,6 +232,23 @@ impl OnlineSource {
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
// Gather encrypted-content samples and prove the minimum by TYPE: a
|
||||
// `DecodeSampleSet` only exists with >= MIN_SAMPLE_UNITS units, so from here
|
||||
// on the request cannot be built under-sized. The service resolves a key by
|
||||
// which submitted unit it decrypts, so a request carrying too few can return
|
||||
// a key matching an incidental unit (a false positive, seen on FMTS variant
|
||||
// units) — too few → skip this source and fall through to the next.
|
||||
let gathered = ctx.samples(64).unwrap_or_default();
|
||||
let n = gathered.len();
|
||||
let Some(samples) = DecodeSampleSet::new(gathered) else {
|
||||
tracing::info!(
|
||||
target: "freemkv::keysource",
|
||||
samples = n,
|
||||
min = MIN_SAMPLE_UNITS,
|
||||
"too few content samples for a reliable online key request; skipping the online source"
|
||||
);
|
||||
return Vec::new();
|
||||
};
|
||||
let b64 = base64::engine::general_purpose::STANDARD;
|
||||
let mut body = serde_json::json!({
|
||||
// Raw Unit_Key_RO.inf, verbatim — the server does its own parse /
|
||||
@@ -229,18 +259,15 @@ impl OnlineSource {
|
||||
if let Some(vid) = ctx.vid() {
|
||||
body["vid_b64"] = serde_json::Value::String(b64.encode(vid.0));
|
||||
}
|
||||
// Up to a generous cap of encrypted content samples for server-side
|
||||
// ciphertext validation.
|
||||
if let Ok(samples) = ctx.samples(64) {
|
||||
if !samples.is_empty() {
|
||||
body["units_b64"] = serde_json::Value::Array(
|
||||
samples
|
||||
.iter()
|
||||
.map(|u| serde_json::Value::String(b64.encode(u)))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Encrypted-content samples for server-side ciphertext validation (already
|
||||
// gathered + minimum-checked above).
|
||||
body["units_b64"] = serde_json::Value::Array(
|
||||
samples
|
||||
.units()
|
||||
.iter()
|
||||
.map(|u| serde_json::Value::String(b64.encode(u)))
|
||||
.collect(),
|
||||
);
|
||||
// The disc's own title (UDF/ISO volume id), plain text. The key service
|
||||
// catalogs it by disc_hash (its disc-titles.json) — independent of keydb.
|
||||
if let Some(label) = ctx.title().map(str::trim) {
|
||||
@@ -303,10 +330,28 @@ impl OnlineSource {
|
||||
Ok(j) => j,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
// A terminal UK is used directly (CPS unit 0 → committed cps 1, matching
|
||||
// the old `Key::Unit(vec![(1, uk)])`).
|
||||
if let Some(uk) = json.get("UK").and_then(|u| u.as_str()).and_then(parse_uk) {
|
||||
return vec![UnitKey::new(0, uk)];
|
||||
// `UK` is an ARRAY of hex keys (the service always returns an array now,
|
||||
// even of one). A single element is the base Unit Key. A full set (one per
|
||||
// forensic index, ordered index 1..N) is returned for a forensic sample.
|
||||
// Preserve array order and tag each key with its array position, so the
|
||||
// caller can map position → index (element i = index i+1). A bare string is
|
||||
// still accepted for backward compatibility.
|
||||
if let Some(uk) = json.get("UK") {
|
||||
let mut out = Vec::new();
|
||||
if let Some(s) = uk.as_str() {
|
||||
if let Some(k) = parse_uk(s) {
|
||||
out.push(UnitKey::new(0, k));
|
||||
}
|
||||
} else if let Some(arr) = uk.as_array() {
|
||||
for (i, v) in arr.iter().enumerate() {
|
||||
if let Some(k) = v.as_str().and_then(parse_uk) {
|
||||
out.push(UnitKey::new(i as u32, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !out.is_empty() {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// A VUK is derived to the terminal keys locally, via the disc's
|
||||
// encrypted title keys from the context — the library owns the crypto.
|
||||
|
||||
Reference in New Issue
Block a user