online: parse UK as an array (1 key plain, 32 keys forensic)
The key service now always returns {"UK":[...]} - an array of one for a
plain movie sample, or all 32 index-ordered variant keys for a forensic
sample. OnlineSource::query accepts both the legacy string form and the
array form, emitting one UnitKey per element (index = array position).
Re-export MIN_SAMPLE_UNITS so callers size their samples correctly.
This commit is contained in:
+1
-1
@@ -1466,7 +1466,7 @@ mod tests {
|
|||||||
let original = std::fs::read(&unit_path).unwrap();
|
let original = std::fs::read(&unit_path).unwrap();
|
||||||
assert_eq!(original.len(), libfreemkv::aacs::content::ALIGNED_UNIT_LEN);
|
assert_eq!(original.len(), libfreemkv::aacs::content::ALIGNED_UNIT_LEN);
|
||||||
assert!(
|
assert!(
|
||||||
libfreemkv::aacs::content::ts_sync_destroyed(&original),
|
!libfreemkv::aacs::content::is_clean(&original, libfreemkv::disc::ContentFormat::BdTs),
|
||||||
"Unit should be encrypted"
|
"Unit should be encrypted"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ mod paths;
|
|||||||
|
|
||||||
pub use keydb::{KeydbSource, UpdateResult};
|
pub use keydb::{KeydbSource, UpdateResult};
|
||||||
pub use keydb_format::{DiscEntry, KeyDb};
|
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};
|
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
|
// Re-exported for downstream convenience so apps need only depend on this crate
|
||||||
|
|||||||
+53
-10
@@ -16,6 +16,19 @@ use libfreemkv::{Error, KeySource};
|
|||||||
// record stream is normally a few MiB; this is headroom, not an expected size).
|
// record stream is normally a few MiB; this is headroom, not an expected size).
|
||||||
const MAX_MKB_BYTES: usize = 64 * 1024 * 1024;
|
const MAX_MKB_BYTES: usize = 64 * 1024 * 1024;
|
||||||
const TIMEOUT_SECS: u64 = 180;
|
const TIMEOUT_SECS: u64 = 180;
|
||||||
|
/// Minimum encrypted-content samples the online source will send in one key
|
||||||
|
/// request. 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). Eight
|
||||||
|
/// units drawn from distinct segments make the request unambiguous. A request
|
||||||
|
/// carrying fewer is refused (empty result → the resolver moves to the next
|
||||||
|
/// source) rather than sent and trusted.
|
||||||
|
///
|
||||||
|
/// Public so callers that GATHER the samples (the CLI, autorip) sample at least
|
||||||
|
/// this many — sampling fewer than the online source's minimum guarantees the
|
||||||
|
/// request is skipped and the online source never consulted.
|
||||||
|
pub const MIN_SAMPLE_UNITS: usize = 8;
|
||||||
/// Hard cap on the key-service response body. A real unit-key reply is a few
|
/// 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
|
/// hundred bytes; bound the read so a malicious/compromised server can't drive
|
||||||
/// the client to OOM with an unbounded body.
|
/// the client to OOM with an unbounded body.
|
||||||
@@ -219,6 +232,22 @@ impl OnlineSource {
|
|||||||
);
|
);
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
// Gather encrypted-content samples FIRST and enforce the minimum: the
|
||||||
|
// service resolves a key by which submitted unit it decrypts, so a
|
||||||
|
// request carrying fewer than `MIN_SAMPLE_UNITS` can return a key matching
|
||||||
|
// an incidental unit (a false positive, seen on FMTS variant units). Refuse
|
||||||
|
// to send an under-sampled request — return empty so the resolver falls
|
||||||
|
// through to the next source rather than trusting an ambiguous key.
|
||||||
|
let samples = ctx.samples(64).unwrap_or_default();
|
||||||
|
if samples.len() < MIN_SAMPLE_UNITS {
|
||||||
|
tracing::info!(
|
||||||
|
target: "freemkv::keysource",
|
||||||
|
samples = samples.len(),
|
||||||
|
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 b64 = base64::engine::general_purpose::STANDARD;
|
||||||
let mut body = serde_json::json!({
|
let mut body = serde_json::json!({
|
||||||
// Raw Unit_Key_RO.inf, verbatim — the server does its own parse /
|
// Raw Unit_Key_RO.inf, verbatim — the server does its own parse /
|
||||||
@@ -229,18 +258,14 @@ impl OnlineSource {
|
|||||||
if let Some(vid) = ctx.vid() {
|
if let Some(vid) = ctx.vid() {
|
||||||
body["vid_b64"] = serde_json::Value::String(b64.encode(vid.0));
|
body["vid_b64"] = serde_json::Value::String(b64.encode(vid.0));
|
||||||
}
|
}
|
||||||
// Up to a generous cap of encrypted content samples for server-side
|
// Encrypted-content samples for server-side ciphertext validation (already
|
||||||
// ciphertext validation.
|
// gathered + minimum-checked above).
|
||||||
if let Ok(samples) = ctx.samples(64) {
|
|
||||||
if !samples.is_empty() {
|
|
||||||
body["units_b64"] = serde_json::Value::Array(
|
body["units_b64"] = serde_json::Value::Array(
|
||||||
samples
|
samples
|
||||||
.iter()
|
.iter()
|
||||||
.map(|u| serde_json::Value::String(b64.encode(u)))
|
.map(|u| serde_json::Value::String(b64.encode(u)))
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
// The disc's own title (UDF/ISO volume id), plain text. The key service
|
// 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.
|
// catalogs it by disc_hash (its disc-titles.json) — independent of keydb.
|
||||||
if let Some(label) = ctx.title().map(str::trim) {
|
if let Some(label) = ctx.title().map(str::trim) {
|
||||||
@@ -303,10 +328,28 @@ impl OnlineSource {
|
|||||||
Ok(j) => j,
|
Ok(j) => j,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
// A terminal UK is used directly (CPS unit 0 → committed cps 1, matching
|
// `UK` is an ARRAY of hex keys (the service always returns an array now,
|
||||||
// the old `Key::Unit(vec![(1, uk)])`).
|
// even of one). A single element is the base Unit Key. A full set (one per
|
||||||
if let Some(uk) = json.get("UK").and_then(|u| u.as_str()).and_then(parse_uk) {
|
// forensic index, ordered index 1..N) is returned for a forensic sample.
|
||||||
return vec![UnitKey::new(0, uk)];
|
// 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
|
// A VUK is derived to the terminal keys locally, via the disc's
|
||||||
// encrypted title keys from the context — the library owns the crypto.
|
// encrypted title keys from the context — the library owns the crypto.
|
||||||
|
|||||||
Reference in New Issue
Block a user