diff --git a/Cargo.toml b/Cargo.toml index 2c86c64..c3a2c36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,3 @@ libfreemkv = { version = "0.27", path = "../libfreemkv" } ureq = { version = "2", features = ["json"] } serde_json = "1" base64 = "0.22" -tracing = "0.1" diff --git a/src/online.rs b/src/online.rs index feddeda..fd76c31 100644 --- a/src/online.rs +++ b/src/online.rs @@ -1,38 +1,19 @@ -//! Online key-service source (source #2). -//! -//! Sends the disc's `Unit_Key_RO.inf`, MKB, Volume ID, and a few encrypted -//! content samples to a remote key service and receives a Unit Key. autorip's -//! original `OnlineKeyService` lived in the app; it moves here so the online -//! lookup is a first-class published source. The library never makes the -//! request — this crate does, keeping libfreemkv network-free. -//! -//! The service does all derivation server-side and returns a final UK, so this -//! source yields a single [`Key::Unit`] candidate (or none). +//! Online key-service source. use std::time::Duration; use base64::Engine; use libfreemkv::{DiscInputs, Key, KeySource, Result}; -/// A real MKB is at most a few MB (a UHD MKB ~3.8 MB). Far larger means -/// something is wrong (e.g. the padded MKB_RW region was read); don't ship a -/// giant body — skip the query. const MAX_MKB_BYTES: usize = 10 * 1024 * 1024; +const TIMEOUT_SECS: u64 = 180; -/// Generous deadline: the body carries the MKB (~5 MB base64) plus samples and -/// the service is often remote on a slow link. A down server still fails fast -/// (connection refused returns immediately). -const KEYSERVICE_TIMEOUT_SECS: u64 = 180; - -/// Client for a remote AACS key service. Opaque third party: it is sent the -/// disc's files + samples and returns a Unit Key or nothing. pub struct OnlineSource { base_url: String, secret: String, } impl OnlineSource { - /// A source posting to `base_url` with an optional bearer `secret`. pub fn new(base_url: impl Into, secret: impl Into) -> Self { Self { base_url: base_url.into(), @@ -43,21 +24,9 @@ impl OnlineSource { impl KeySource for OnlineSource { fn resolve(&self, inputs: &DiscInputs) -> Result> { - if self.base_url.is_empty() { - tracing::warn!(phase = "keyservice_query", "no key service URL configured"); + if self.base_url.is_empty() || inputs.mkb.len() > MAX_MKB_BYTES { return Ok(Vec::new()); } - if inputs.mkb.len() > MAX_MKB_BYTES { - tracing::warn!( - phase = "keyservice_query", - mkb_bytes = inputs.mkb.len(), - "MKB unexpectedly large ({} MB) — not querying the key service", - inputs.mkb.len() / 1024 / 1024 - ); - return Ok(Vec::new()); - } - - let url = format!("{}/decode", self.base_url.trim_end_matches('/')); let b64 = base64::engine::general_purpose::STANDARD; let mut body = serde_json::json!({ "inf_b64": b64.encode(&inputs.unit_key_ro), @@ -75,71 +44,29 @@ impl KeySource for OnlineSource { .collect(), ); } - - let mut req = ureq::post(&url).timeout(Duration::from_secs(KEYSERVICE_TIMEOUT_SECS)); + let mut req = ureq::post(&self.base_url).timeout(Duration::from_secs(TIMEOUT_SECS)); if !self.secret.is_empty() { req = req.set("Authorization", &format!("Bearer {}", self.secret)); } - tracing::info!( - phase = "keyservice_query", - url = %url, - inf = inputs.unit_key_ro.len(), - mkb = inputs.mkb.len(), - has_vid = inputs.volume_id != [0u8; 16], - units = inputs.samples.len(), - "querying key service" - ); - - // A source never fails the whole resolve: network / status / parse - // problems are logged (so the device log shows unreachable vs no-key) - // and surface as "no candidate", letting the next source try. let resp = match req.send_json(body) { Ok(r) => r, - Err(ureq::Error::Status(code, _)) => { - tracing::warn!( - phase = "keyservice_query", - status = code, - "key service returned no key" - ); - return Ok(Vec::new()); - } - Err(e) => { - tracing::warn!(phase = "keyservice_query", error = %e, "key service unreachable"); - return Ok(Vec::new()); - } + Err(_) => return Ok(Vec::new()), }; let json: serde_json::Value = match resp.into_json() { Ok(j) => j, - Err(e) => { - tracing::warn!(phase = "keyservice_query", error = %e, "key service reply unreadable"); - return Ok(Vec::new()); - } + Err(_) => return Ok(Vec::new()), }; match json.get("UK").and_then(|u| u.as_str()).and_then(parse_uk) { - Some(uk) => { - tracing::info!(phase = "keyservice_query", "key service returned a key"); - // The service resolves a final unit key server-side; hand it in - // as the terminal level for CPS unit 1 (matching the prior - // rescan-with-unit-key behavior). - Ok(vec![Key::Unit(vec![(1, uk)])]) - } - None => { - tracing::warn!( - phase = "keyservice_query", - "key service reply had no usable key" - ); - Ok(Vec::new()) - } + Some(uk) => Ok(vec![Key::Unit(vec![(1, uk)])]), + None => Ok(Vec::new()), } } - /// The service validates against real ciphertext, so it needs samples. fn needs_samples(&self) -> bool { true } } -/// Parse a 32-char hex Unit Key into 16 bytes. fn parse_uk(hex: &str) -> Option<[u8; 16]> { if hex.len() != 32 { return None; @@ -150,34 +77,3 @@ fn parse_uk(hex: &str) -> Option<[u8; 16]> { } Some(out) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_uk_roundtrip() { - assert_eq!( - parse_uk("1deb13ba851d8fbc01e169dca7d2f258").unwrap(), - [ - 0x1d, 0xeb, 0x13, 0xba, 0x85, 0x1d, 0x8f, 0xbc, 0x01, 0xe1, 0x69, 0xdc, 0xa7, 0xd2, - 0xf2, 0x58 - ] - ); - assert!(parse_uk("deadbeef").is_none()); - assert!(parse_uk("zz").is_none()); - } - - #[test] - fn empty_url_yields_no_candidate() { - let src = OnlineSource::new("", ""); - let inputs = DiscInputs { - disc_hash: "0xaabb".into(), - volume_id: [0u8; 16], - mkb: Vec::new(), - unit_key_ro: Vec::new(), - samples: Vec::new(), - }; - assert!(src.resolve(&inputs).unwrap().is_empty()); - } -}