sources: stateful one-key-at-a-time providers, UK-first keydb, shared resolve loop

Each source implements next_key (a cursor over its candidates) instead of
returning them all at once. The keydb hands its per-disc candidates out
UK-first (UK > VK > MK > DK) so a stale/wrong VUK never pre-empts a good UK in
the same entry; online and mapfile are one-shot. MultiSource composes sources
in the caller's chosen order and resolve_and_apply drives the
next_key -> decrypt_with loop, stopping at the first key that decrypts.
read_sample_units moves here so the CLI and autorip share one content sampler.
This commit is contained in:
MattJackson
2026-06-05 09:23:17 -07:00
parent e3f452166b
commit da5bd08d3f
4 changed files with 241 additions and 64 deletions
+42 -10
View File
@@ -3,7 +3,7 @@
use std::time::Duration;
use base64::Engine;
use libfreemkv::{DiscInputs, Key, KeySource, Result};
use libfreemkv::{DiscInputs, Key, KeySource};
const MAX_MKB_BYTES: usize = 10 * 1024 * 1024;
const TIMEOUT_SECS: u64 = 180;
@@ -11,6 +11,14 @@ const TIMEOUT_SECS: u64 = 180;
pub struct OnlineSource {
base_url: String,
secret: String,
/// The key service pre-validates server-side and returns a single UK, so it
/// is asked **at most once** — this flips true after the first `next_key`,
/// and every later ask returns `None` without re-hitting the network.
asked: bool,
/// Set when the round-trip itself failed (network down, bad response) — as
/// opposed to the service simply having no key. Lets the caller report
/// "key service unreachable" distinctly from "no key for this disc".
errored: bool,
}
impl OnlineSource {
@@ -18,14 +26,16 @@ impl OnlineSource {
Self {
base_url: base_url.into(),
secret: secret.into(),
asked: false,
errored: false,
}
}
}
impl KeySource for OnlineSource {
fn resolve(&self, inputs: &DiscInputs) -> Result<Vec<Key>> {
/// The single server-resolved UK for this disc, or `None`. Runs exactly the
/// one network round-trip; `next_key` gates it to one call per session.
fn query(&mut self, inputs: &DiscInputs) -> Option<Key> {
if self.base_url.is_empty() || inputs.mkb.len() > MAX_MKB_BYTES {
return Ok(Vec::new());
return None;
}
let b64 = base64::engine::general_purpose::STANDARD;
let mut body = serde_json::json!({
@@ -50,21 +60,43 @@ impl KeySource for OnlineSource {
}
let resp = match req.send_json(body) {
Ok(r) => r,
Err(_) => return Ok(Vec::new()),
Err(_) => {
self.errored = true;
return None;
}
};
let json: serde_json::Value = match resp.into_json() {
Ok(j) => j,
Err(_) => return Ok(Vec::new()),
Err(_) => {
self.errored = true;
return None;
}
};
match json.get("UK").and_then(|u| u.as_str()).and_then(parse_uk) {
Some(uk) => Ok(vec![Key::Unit(vec![(1, uk)])]),
None => Ok(Vec::new()),
json.get("UK")
.and_then(|u| u.as_str())
.and_then(parse_uk)
.map(|uk| Key::Unit(vec![(1, uk)]))
}
}
impl KeySource for OnlineSource {
fn next_key(&mut self, inputs: &DiscInputs) -> Option<Key> {
// One shot: the service pre-validates and returns a single UK, so a
// second ask has nothing new to offer — don't re-hit the network.
if self.asked {
return None;
}
self.asked = true;
self.query(inputs)
}
fn needs_samples(&self) -> bool {
true
}
fn errored(&self) -> bool {
self.errored
}
}
fn parse_uk(hex: &str) -> Option<[u8; 16]> {