use crate::aacs::{HostCert, UnitKey, Vid};
use crate::disc::Key;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct DiscInputs {
pub disc_hash: String,
pub volume_id: [u8; 16],
pub mkb: Vec<u8>,
pub unit_key_ro: Vec<u8>,
pub samples: Vec<Vec<u8>>,
pub volume_label: Option<String>,
}
pub trait ResolveCtx {
fn disc_hash(&self) -> &str;
fn title(&self) -> Option<&str>;
fn vid(&self) -> Option<Vid>;
fn mkb(&self) -> Result<&[u8], Error>;
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error>;
fn samples(&self, n: usize) -> Result<Vec<Vec<u8>>, Error>;
fn unit_key_ro(&self) -> &[u8] {
&[]
}
}
pub struct DiscInputsCtx<'a> {
inner: &'a DiscInputs,
enc_keys: Vec<[u8; 16]>,
}
impl<'a> DiscInputsCtx<'a> {
pub fn new(inputs: &'a DiscInputs, version_u8: u8) -> Self {
use crate::aacs::{AacsVersion, parse_unit_key_ro};
let enc_keys = if inputs.unit_key_ro.is_empty() {
Vec::new()
} else {
let version = if version_u8 == 1 {
AacsVersion::V10
} else {
AacsVersion::V20
};
parse_unit_key_ro(&inputs.unit_key_ro, version)
.map(|f| f.encrypted_keys.into_iter().map(|(_, k)| k).collect())
.unwrap_or_default()
};
Self {
inner: inputs,
enc_keys,
}
}
}
impl ResolveCtx for DiscInputsCtx<'_> {
fn disc_hash(&self) -> &str {
&self.inner.disc_hash
}
fn title(&self) -> Option<&str> {
self.inner.volume_label.as_deref()
}
fn vid(&self) -> Option<Vid> {
if self.inner.volume_id == [0u8; 16] {
None
} else {
Some(Vid(self.inner.volume_id))
}
}
fn mkb(&self) -> Result<&[u8], Error> {
Ok(&self.inner.mkb)
}
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error> {
Ok(&self.enc_keys)
}
fn samples(&self, n: usize) -> Result<Vec<Vec<u8>>, Error> {
Ok(self.inner.samples.iter().take(n).cloned().collect())
}
fn unit_key_ro(&self) -> &[u8] {
&self.inner.unit_key_ro
}
}
pub trait KeySource {
fn get_uk(&self, ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error>;
fn host_certs(&self, _mkb: Option<u32>) -> Vec<HostCert> {
Vec::new()
}
fn label(&self) -> &'static str {
"source"
}
}
pub fn resolve_and_apply(
sources: &[Box<dyn KeySource>],
inputs: &DiscInputs,
disc: &mut crate::Disc,
) -> bool {
resolve_and_apply_traced(sources, inputs, disc).0
}
pub fn resolve_and_apply_traced(
sources: &[Box<dyn KeySource>],
inputs: &DiscInputs,
disc: &mut crate::Disc,
) -> (bool, crate::aacs::ResolutionTrace) {
use crate::aacs::trace::{KeyNode, KeyOutcome, KeyStep};
let mut trace = crate::aacs::ResolutionTrace::new();
let version_u8 = disc.aacs.as_ref().map(|a| a.version).unwrap_or(2);
let ctx = DiscInputsCtx::new(inputs, version_u8);
for source in sources {
let who = source.label().to_string();
match source.get_uk(&ctx) {
Ok(uks) if !uks.is_empty() => {
let unit_keys: Vec<(u32, [u8; 16])> = uks
.iter()
.map(|uk| (uk.idx.saturating_add(1), uk.key))
.collect();
if disc
.decrypt_with(Key::Unit(unit_keys), &inputs.samples)
.is_ok()
{
trace.keys.push(KeyStep {
who,
path: vec![KeyNode::FoundUnitKeys, KeyNode::DerivedUnitKeys],
outcome: KeyOutcome::Resolved,
});
return (true, trace);
}
trace.keys.push(KeyStep {
who,
path: vec![KeyNode::FoundUnitKeys],
outcome: KeyOutcome::NoKey,
});
}
Ok(_) | Err(_) => {
trace.keys.push(KeyStep {
who,
path: vec![KeyNode::NoEntry],
outcome: KeyOutcome::NoKey,
});
}
}
}
(false, trace)
}
pub fn fetch_unit_keys(sources: &[Box<dyn KeySource>], ctx: &dyn ResolveCtx) -> Vec<UnitKey> {
for source in sources {
if let Ok(uks) = source.get_uk(ctx) {
if !uks.is_empty() {
return uks;
}
}
}
Vec::new()
}
pub fn key_fetch(
inputs: DiscInputs,
make_sources: std::sync::Arc<dyn Fn() -> Vec<Box<dyn KeySource>> + Send + Sync>,
) -> crate::sector::KeyFetch {
std::sync::Arc::new(move |samples: &[Vec<u8>]| -> Vec<[u8; 16]> {
let sources = make_sources();
let mut di = inputs.clone();
di.samples = samples.to_vec();
let ctx = DiscInputsCtx::new(&di, 2);
fetch_unit_keys(&sources, &ctx)
.into_iter()
.map(|u| u.key)
.collect()
})
}
pub fn read_encrypted_units(
reader: &mut dyn crate::sector::SectorSource,
title: &crate::disc::DiscTitle,
n: usize,
) -> Vec<Vec<u8>> {
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
const CHUNK_UNITS: u32 = 15; const MAX_CHUNKS_PER_EXTENT: u32 = 4;
let mut out: Vec<Vec<u8>> = Vec::new();
for ext in &title.extents {
let total_units = ext.sector_count / ALIGNED_UNIT_SECTORS;
if total_units == 0 {
continue;
}
let mut unit = total_units / 2; for _ in 0..MAX_CHUNKS_PER_EXTENT {
if unit >= total_units {
break;
}
let units_this = CHUNK_UNITS.min(total_units - unit);
let lba = ext
.start_lba
.saturating_add(unit.saturating_mul(ALIGNED_UNIT_SECTORS));
let count = (units_this * ALIGNED_UNIT_SECTORS) as u16;
let mut buf = vec![0u8; count as usize * 2048];
if reader.read_sectors(lba, count, &mut buf, false).is_err() {
break;
}
for i in 0..units_this as usize {
let o = i * ALIGNED_UNIT_LEN;
if o + ALIGNED_UNIT_LEN > buf.len() {
break;
}
let u = &buf[o..o + ALIGNED_UNIT_LEN];
if ts_sync_destroyed(u) {
out.push(u.to_vec());
if out.len() >= n {
return out;
}
}
}
unit += units_this;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aacs::UnitKey;
use std::sync::{Arc, Mutex};
#[test]
fn key_source_host_certs_defaults_to_empty() {
struct MinimalSource;
impl KeySource for MinimalSource {
fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
Ok(Vec::new())
}
}
let s = MinimalSource;
assert!(s.host_certs(None).is_empty());
assert!(s.host_certs(Some(68)).is_empty());
}
#[test]
fn disc_inputs_ctx_maps_fields() {
let mut uk_ro = vec![0u8; 96];
let uk_pos = 32usize;
uk_ro[0..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
uk_ro[uk_pos] = 0x00;
uk_ro[uk_pos + 1] = 0x01; let key_bytes = [0x7Eu8; 16];
uk_ro[80..96].copy_from_slice(&key_bytes);
let inputs = DiscInputs {
disc_hash: "0xABC".into(),
volume_id: [0u8; 16],
mkb: vec![1, 2, 3],
unit_key_ro: uk_ro,
samples: vec![vec![9u8; 4], vec![8u8; 4], vec![7u8; 4]],
volume_label: Some("TITLE_X".into()),
};
let ctx = DiscInputsCtx::new(&inputs, 1);
assert_eq!(ctx.disc_hash(), "0xABC");
assert_eq!(ctx.title(), Some("TITLE_X"));
assert!(ctx.vid().is_none(), "all-zero VID is the no-VID sentinel");
assert_eq!(ctx.mkb().unwrap(), &[1, 2, 3]);
assert_eq!(ctx.enc_title_keys().unwrap(), &[key_bytes]);
assert_eq!(ctx.samples(2).unwrap().len(), 2, "samples truncates to n");
let mut inputs2 = inputs.clone();
inputs2.volume_id = [0x42u8; 16];
let ctx2 = DiscInputsCtx::new(&inputs2, 1);
assert_eq!(ctx2.vid(), Some(Vid([0x42u8; 16])));
}
#[test]
fn trace_who_is_the_source_label_verbatim() {
struct LabeledSource(&'static str);
impl KeySource for LabeledSource {
fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
Ok(Vec::new())
}
fn label(&self) -> &'static str {
self.0
}
}
let mut disc = crate::Disc {
volume_id: String::new(),
meta_title: None,
format: crate::DiscFormat::BluRay,
capacity_sectors: 0,
capacity_bytes: 0,
layers: 1,
titles: Vec::new(),
region: crate::disc::DiscRegion::Free,
aacs: None,
css: None,
encrypted: false,
aacs_error: None,
css_error: None,
content_format: crate::ContentFormat::BdTs,
};
let inputs = DiscInputs {
disc_hash: "0x00".into(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
let sources: Vec<Box<dyn KeySource>> = vec![
Box::new(LabeledSource("keydb")),
Box::new(LabeledSource("my-custom-source")),
];
let (_ok, trace) = resolve_and_apply_traced(&sources, &inputs, &mut disc);
let whos: Vec<&str> = trace.keys.iter().map(|s| s.who.as_str()).collect();
assert_eq!(whos, vec!["keydb", "my-custom-source"]);
}
fn empty_inputs() -> DiscInputs {
DiscInputs {
disc_hash: String::new(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
}
}
struct EmptySource;
impl KeySource for EmptySource {
fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
Ok(Vec::new())
}
}
struct ErroringSource;
impl KeySource for ErroringSource {
fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
Err(Error::AacsNoKeys)
}
}
struct HasKey([u8; 16]);
impl KeySource for HasKey {
fn get_uk(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
Ok(vec![UnitKey {
idx: 0,
key: self.0,
}])
}
}
#[test]
fn fetch_unit_keys_first_nonempty_skips_empty_and_errors() {
let inputs = empty_inputs();
let ctx = DiscInputsCtx::new(&inputs, 2);
let key = [0xABu8; 16];
let sources: Vec<Box<dyn KeySource>> = vec![
Box::new(EmptySource),
Box::new(ErroringSource),
Box::new(HasKey(key)),
];
let got = fetch_unit_keys(&sources, &ctx);
assert_eq!(got.len(), 1, "the first source that answers wins");
assert_eq!(got[0].key, key);
let none: Vec<Box<dyn KeySource>> = vec![Box::new(EmptySource), Box::new(ErroringSource)];
assert!(
fetch_unit_keys(&none, &ctx).is_empty(),
"no source answers ⇒ empty"
);
}
#[test]
fn key_fetch_closure_forwards_samples_and_returns_keys() {
let key = [0x5au8; 16];
let seen: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
let builds = Arc::new(Mutex::new(0usize));
struct Probe {
key: [u8; 16],
seen: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl KeySource for Probe {
fn get_uk(&self, ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, Error> {
if let Ok(s) = ctx.samples(8) {
self.seen.lock().unwrap().extend(s);
}
Ok(vec![UnitKey {
idx: 0,
key: self.key,
}])
}
}
let seen_c = Arc::clone(&seen);
let builds_c = Arc::clone(&builds);
let make: Arc<dyn Fn() -> Vec<Box<dyn KeySource>> + Send + Sync> = Arc::new(move || {
*builds_c.lock().unwrap() += 1;
vec![Box::new(Probe {
key,
seen: Arc::clone(&seen_c),
}) as Box<dyn KeySource>]
});
let cb = key_fetch(empty_inputs(), make);
let samples = vec![vec![0xEEu8; crate::aacs::ALIGNED_UNIT_LEN]];
let got = cb(&samples);
assert_eq!(
got,
vec![key],
"the source's key flows back through the closure"
);
assert_eq!(
seen.lock().unwrap().len(),
1,
"the failing ciphertext sample is forwarded to the source"
);
assert_eq!(*builds.lock().unwrap(), 1, "make_sources invoked per fetch");
}
}