aacs: validate a resolved key against content before applying it
decrypt_with now takes the disc's encrypted content samples and, after deriving the candidate unit keys, confirms at least one de-scrambles a real aligned unit before committing them. A wrong key (a keydb VK that doesn't match the disc, a stale UK) is rejected with AacsKeyRejected instead of silently applying garbage unit keys. Conservative by design: with no samples (resume / mapfile cache) it accepts as before, leaving those paths unchanged. The KeySource trait becomes a stateful provider — next_key hands one candidate at a time (the source owns the order) and reports exhaustion, replacing the all-at-once resolve; errored() distinguishes a failed source from a clean no-key.
This commit is contained in:
+233
-73
@@ -1555,6 +1555,43 @@ pub enum Key {
|
|||||||
Unit(Vec<(u32, [u8; 16])>),
|
Unit(Vec<(u32, [u8; 16])>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if `unit_keys` can de-scramble at least one of the supplied content
|
||||||
|
/// `samples` — the validation gate for [`Disc::decrypt_with`]. Conservative: a
|
||||||
|
/// sample that is not AACS-scrambled proves nothing, and with no scrambled
|
||||||
|
/// sample at all there is nothing to disprove against, so it returns `true`
|
||||||
|
/// (accept). It only returns `false` when a scrambled sample exists and NO unit
|
||||||
|
/// key restores it to clear MPEG-TS — i.e. the key is provably wrong for this
|
||||||
|
/// disc. Reuses the ecosystem's single `is_aacs_scrambled` predicate and the
|
||||||
|
/// full (bus + AACS) unit decrypt, so it agrees with the actual mux decrypt.
|
||||||
|
fn aligned_unit_keys_validate(
|
||||||
|
unit_keys: &[(u32, [u8; 16])],
|
||||||
|
read_data_key: Option<&[u8; 16]>,
|
||||||
|
samples: &[Vec<u8>],
|
||||||
|
) -> bool {
|
||||||
|
use crate::aacs::decrypt::{ALIGNED_UNIT_LEN, decrypt_unit_full, is_aacs_scrambled};
|
||||||
|
let scrambled: Vec<&[u8]> = samples
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_slice())
|
||||||
|
.filter(|s| s.len() >= ALIGNED_UNIT_LEN && is_aacs_scrambled(s))
|
||||||
|
.collect();
|
||||||
|
if scrambled.is_empty() {
|
||||||
|
return true; // nothing to disprove against — accept
|
||||||
|
}
|
||||||
|
if unit_keys.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut probe = vec![0u8; ALIGNED_UNIT_LEN];
|
||||||
|
for sample in scrambled {
|
||||||
|
for (_, k) in unit_keys {
|
||||||
|
probe.copy_from_slice(&sample[..ALIGNED_UNIT_LEN]);
|
||||||
|
if decrypt_unit_full(&mut probe, k, read_data_key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
/// Get the resolved decryption keys for this disc.
|
/// Get the resolved decryption keys for this disc.
|
||||||
/// Used by disc-to-ISO and other full-disc operations.
|
/// Used by disc-to-ISO and other full-disc operations.
|
||||||
@@ -1644,83 +1681,116 @@ impl Disc {
|
|||||||
/// in here. For [`Key::Unit`] this is the deferred-mux / resume path: the
|
/// in here. For [`Key::Unit`] this is the deferred-mux / resume path: the
|
||||||
/// unit keys came from a key source or the mapfile cache, and libfreemkv
|
/// unit keys came from a key source or the mapfile cache, and libfreemkv
|
||||||
/// decrypts directly (see [`Self::inject_unit_keys`]).
|
/// decrypts directly (see [`Self::inject_unit_keys`]).
|
||||||
pub fn decrypt_with(&mut self, key: Key) -> Result<()> {
|
/// `samples` are encrypted on-disc aligned units (each 6144 bytes), supplied
|
||||||
// Unit keys are terminal — no derivation. Inject directly (resume /
|
/// by the caller for content validation. A wrong key — a keydb VK that does
|
||||||
// mapfile cache / a source that already had the final UKs).
|
/// not match this disc, a stale UK — can still *derive* a non-empty (garbage)
|
||||||
if let Key::Unit(keys) = key {
|
/// unit-key set, so before the key touches disc state we confirm it actually
|
||||||
self.inject_unit_keys(keys);
|
/// de-scrambles real ciphertext. This is the single home of key validation:
|
||||||
return Ok(());
|
/// every caller loops a key source's candidates through `decrypt_with` and a
|
||||||
}
|
/// rejected key (`Err(AacsKeyRejected)`) transparently falls through to the
|
||||||
|
/// next. Pass `&[]` when no content sample is available (resume / mapfile
|
||||||
|
/// cache) — validation is then skipped and the key is applied as-is.
|
||||||
|
pub fn decrypt_with(&mut self, key: Key, samples: &[Vec<u8>]) -> Result<()> {
|
||||||
|
// The AACS 2.x bus key, needed to de-scramble a sample for validation;
|
||||||
|
// captured before any mutable borrow. None for AACS 1.0 and file-backed
|
||||||
|
// ISO units (bus encryption was already removed at read time).
|
||||||
|
let read_data_key = self.aacs.as_ref().and_then(|a| a.read_data_key);
|
||||||
|
|
||||||
// Every higher level derives DOWN to the unit keys, reusing the
|
// Resolve the supplied key DOWN to candidate unit keys WITHOUT
|
||||||
// version-dispatched resolver — the single home for all AACS
|
// committing them, so a wrong higher-level key can be rejected before it
|
||||||
// derivation (1.0 / 2.0 / 2.1 / 2.x). It needs the AACS inputs
|
// poisons disc state.
|
||||||
// (Unit_Key_RO.inf, MKB, VID) stashed on the disc at scan time.
|
let (candidate_unit_keys, candidate_vuk) = if let Key::Unit(keys) = key {
|
||||||
let aacs = self.aacs.as_ref().ok_or(crate::error::Error::AacsNoKeys)?;
|
// Terminal — the source / mapfile already holds the final UKs.
|
||||||
if aacs.uk_ro.is_empty() {
|
(keys, None)
|
||||||
// Scan captured no Unit_Key_RO.inf — nothing to derive into.
|
} else {
|
||||||
return Err(crate::error::Error::AacsNoKeys);
|
// Every higher level derives DOWN to the unit keys, reusing the
|
||||||
}
|
// version-dispatched resolver — the single home for all AACS
|
||||||
|
// derivation (1.0 / 2.0 / 2.1 / 2.x). It needs the AACS inputs
|
||||||
// Map the supplied Key -> raw material. The source handed in material
|
// (Unit_Key_RO.inf, MKB, VID) stashed on the disc at scan time.
|
||||||
// at exactly one level; choosing/applying it is the resolver's job.
|
let aacs = self.aacs.as_ref().ok_or(crate::error::Error::AacsNoKeys)?;
|
||||||
let mut supplied = crate::aacs::provider::SuppliedKey {
|
if aacs.uk_ro.is_empty() {
|
||||||
device_keys: Vec::new(),
|
// Scan captured no Unit_Key_RO.inf — nothing to derive into.
|
||||||
processing_keys: Vec::new(),
|
return Err(crate::error::Error::AacsNoKeys);
|
||||||
media_keys: Vec::new(),
|
|
||||||
disc_entry: None,
|
|
||||||
};
|
|
||||||
match key {
|
|
||||||
Key::Device(dks) => supplied.device_keys = dks,
|
|
||||||
Key::Processing(pks) => supplied.processing_keys = pks,
|
|
||||||
Key::Media(mks) => supplied.media_keys = mks,
|
|
||||||
Key::Volume(vuk) => {
|
|
||||||
supplied.disc_entry = Some(crate::aacs::DiscEntry {
|
|
||||||
disc_hash: aacs.disc_hash.clone(),
|
|
||||||
title: String::new(),
|
|
||||||
media_key: None,
|
|
||||||
disc_id: None,
|
|
||||||
vuk: Some(vuk),
|
|
||||||
unit_keys: Vec::new(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Key::Unit(_) => unreachable!("Key::Unit handled above"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot inputs (releases the &self borrow before the &mut below).
|
// Map the supplied Key -> raw material. The source handed in material
|
||||||
let volume_id = aacs.volume_id;
|
// at exactly one level; choosing/applying it is the resolver's job.
|
||||||
let mkb = aacs.mkb.clone();
|
let mut supplied = crate::aacs::provider::SuppliedKey {
|
||||||
let uk_ro = aacs.uk_ro.clone();
|
device_keys: Vec::new(),
|
||||||
let version_u8 = aacs.version;
|
processing_keys: Vec::new(),
|
||||||
|
media_keys: Vec::new(),
|
||||||
|
disc_entry: None,
|
||||||
|
};
|
||||||
|
match key {
|
||||||
|
Key::Device(dks) => supplied.device_keys = dks,
|
||||||
|
Key::Processing(pks) => supplied.processing_keys = pks,
|
||||||
|
Key::Media(mks) => supplied.media_keys = mks,
|
||||||
|
Key::Volume(vuk) => {
|
||||||
|
supplied.disc_entry = Some(crate::aacs::DiscEntry {
|
||||||
|
disc_hash: aacs.disc_hash.clone(),
|
||||||
|
title: String::new(),
|
||||||
|
media_key: None,
|
||||||
|
disc_id: None,
|
||||||
|
vuk: Some(vuk),
|
||||||
|
unit_keys: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Key::Unit(_) => unreachable!("Key::Unit handled above"),
|
||||||
|
}
|
||||||
|
|
||||||
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
// Snapshot inputs (releases the &self borrow before the &mut below).
|
||||||
let ctx = crate::aacs::ResolveContext {
|
let volume_id = aacs.volume_id;
|
||||||
unit_key_ro: &uk_ro,
|
let mkb = aacs.mkb.clone();
|
||||||
content_cert: None,
|
let uk_ro = aacs.uk_ro.clone();
|
||||||
volume_id: &volume_id,
|
let version_u8 = aacs.version;
|
||||||
providers: &provider_refs,
|
|
||||||
mkb: if mkb.is_empty() { None } else { Some(&mkb) },
|
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
||||||
|
let ctx = crate::aacs::ResolveContext {
|
||||||
|
unit_key_ro: &uk_ro,
|
||||||
|
content_cert: None,
|
||||||
|
volume_id: &volume_id,
|
||||||
|
providers: &provider_refs,
|
||||||
|
mkb: if mkb.is_empty() { None } else { Some(&mkb) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Version dispatch — V10 uses the classical resolver at 48-byte
|
||||||
|
// stride; V20/V21 share the 64-byte stride, so try the classical V20
|
||||||
|
// paths first and fall back to the 2.1 variant chain.
|
||||||
|
let resolved = match version_u8 {
|
||||||
|
1 => crate::aacs::resolve_keys_v1(&ctx),
|
||||||
|
_ => crate::aacs::resolve_keys_v2(&ctx)
|
||||||
|
.or_else(|| crate::aacs::resolve_keys_v21(&ctx)),
|
||||||
|
}
|
||||||
|
.ok_or(crate::error::Error::AacsKeyRejected)?;
|
||||||
|
|
||||||
|
if resolved.unit_keys.is_empty() {
|
||||||
|
return Err(crate::error::Error::AacsKeyRejected);
|
||||||
|
}
|
||||||
|
(resolved.unit_keys, resolved.vuk)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Version dispatch — V10 uses the classical resolver at 48-byte
|
// VALIDATE against real ciphertext. Conservative: reject only when a
|
||||||
// stride; V20/V21 share the 64-byte stride, so try the classical V20
|
// supplied sample is AACS-scrambled and NO candidate unit key can
|
||||||
// paths first and fall back to the 2.1 variant chain.
|
// de-scramble it. With no samples (or only clear ones) there is nothing
|
||||||
let resolved = match version_u8 {
|
// to disprove against, so the key is accepted as-is — keeping the
|
||||||
1 => crate::aacs::resolve_keys_v1(&ctx),
|
// sample-less paths (resume / mapfile cache) byte-for-byte unchanged.
|
||||||
_ => crate::aacs::resolve_keys_v2(&ctx).or_else(|| crate::aacs::resolve_keys_v21(&ctx)),
|
if !aligned_unit_keys_validate(&candidate_unit_keys, read_data_key.as_ref(), samples) {
|
||||||
}
|
|
||||||
.ok_or(crate::error::Error::AacsKeyRejected)?;
|
|
||||||
|
|
||||||
if resolved.unit_keys.is_empty() {
|
|
||||||
return Err(crate::error::Error::AacsKeyRejected);
|
return Err(crate::error::Error::AacsKeyRejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
let a = self.aacs.as_mut().expect("aacs present (checked above)");
|
// Commit — only now does the key touch disc state.
|
||||||
a.unit_keys = resolved.unit_keys;
|
match self.aacs.as_mut() {
|
||||||
if resolved.vuk.is_some() {
|
Some(a) => {
|
||||||
a.vuk = resolved.vuk;
|
a.unit_keys = candidate_unit_keys;
|
||||||
|
if candidate_vuk.is_some() {
|
||||||
|
a.vuk = candidate_vuk;
|
||||||
|
}
|
||||||
|
a.key_source = KeyOrigin::ExternalUk;
|
||||||
|
}
|
||||||
|
// A Unit key for an AACS disc whose scan built no state (keyless
|
||||||
|
// scan, no keydb): synthesize a minimal ExternalUk state.
|
||||||
|
None => self.inject_unit_keys(candidate_unit_keys),
|
||||||
}
|
}
|
||||||
a.key_source = KeyOrigin::ExternalUk;
|
|
||||||
// A prior scan-time resolution error (e.g. keyless scan) is now moot.
|
// A prior scan-time resolution error (e.g. keyless scan) is now moot.
|
||||||
self.aacs_error = None;
|
self.aacs_error = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -3007,7 +3077,7 @@ mod tests {
|
|||||||
disc.encrypted = true;
|
disc.encrypted = true;
|
||||||
disc.aacs = Some(aacs_with(vec![(0, [0x01; 16])]));
|
disc.aacs = Some(aacs_with(vec![(0, [0x01; 16])]));
|
||||||
let new = vec![(0u32, [0x77u8; 16]), (1, [0x88; 16])];
|
let new = vec![(0u32, [0x77u8; 16]), (1, [0x88; 16])];
|
||||||
disc.decrypt_with(Key::Unit(new.clone())).unwrap();
|
disc.decrypt_with(Key::Unit(new.clone()), &[]).unwrap();
|
||||||
match disc.decrypt_keys() {
|
match disc.decrypt_keys() {
|
||||||
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
||||||
assert_eq!(unit_keys, new, "must replace, preserving every CPS unit");
|
assert_eq!(unit_keys, new, "must replace, preserving every CPS unit");
|
||||||
@@ -3055,7 +3125,7 @@ mod tests {
|
|||||||
a.uk_ro = uk_ro_v20(&[enc0, enc1]);
|
a.uk_ro = uk_ro_v20(&[enc0, enc1]);
|
||||||
disc.aacs = Some(a);
|
disc.aacs = Some(a);
|
||||||
|
|
||||||
disc.decrypt_with(Key::Volume(vuk)).unwrap();
|
disc.decrypt_with(Key::Volume(vuk), &[]).unwrap();
|
||||||
match disc.decrypt_keys() {
|
match disc.decrypt_keys() {
|
||||||
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3081,7 +3151,8 @@ mod tests {
|
|||||||
disc.encrypted = true;
|
disc.encrypted = true;
|
||||||
disc.aacs = Some(aacs_with(Vec::new())); // uk_ro empty
|
disc.aacs = Some(aacs_with(Vec::new())); // uk_ro empty
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
disc.decrypt_with(Key::Volume([0x11u8; 16])).unwrap_err(),
|
disc.decrypt_with(Key::Volume([0x11u8; 16]), &[])
|
||||||
|
.unwrap_err(),
|
||||||
crate::error::Error::AacsNoKeys
|
crate::error::Error::AacsNoKeys
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -3090,7 +3161,7 @@ mod tests {
|
|||||||
disc2.encrypted = true;
|
disc2.encrypted = true;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
disc2
|
disc2
|
||||||
.decrypt_with(Key::Media(vec![[0x22u8; 16]]))
|
.decrypt_with(Key::Media(vec![[0x22u8; 16]]), &[])
|
||||||
.unwrap_err(),
|
.unwrap_err(),
|
||||||
crate::error::Error::AacsNoKeys
|
crate::error::Error::AacsNoKeys
|
||||||
));
|
));
|
||||||
@@ -3106,7 +3177,8 @@ mod tests {
|
|||||||
a.uk_ro = uk_ro_v20(&[]); // num_uk = 0
|
a.uk_ro = uk_ro_v20(&[]); // num_uk = 0
|
||||||
disc.aacs = Some(a);
|
disc.aacs = Some(a);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
disc.decrypt_with(Key::Volume([0x11u8; 16])).unwrap_err(),
|
disc.decrypt_with(Key::Volume([0x11u8; 16]), &[])
|
||||||
|
.unwrap_err(),
|
||||||
crate::error::Error::AacsKeyRejected
|
crate::error::Error::AacsKeyRejected
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -3119,7 +3191,7 @@ mod tests {
|
|||||||
let mut disc = make_test_disc(1000, "UHD");
|
let mut disc = make_test_disc(1000, "UHD");
|
||||||
disc.encrypted = true;
|
disc.encrypted = true;
|
||||||
let uk = vec![(0u32, [0x44u8; 16])];
|
let uk = vec![(0u32, [0x44u8; 16])];
|
||||||
disc.decrypt_with(Key::Unit(uk.clone())).unwrap();
|
disc.decrypt_with(Key::Unit(uk.clone()), &[]).unwrap();
|
||||||
match disc.decrypt_keys() {
|
match disc.decrypt_keys() {
|
||||||
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
|
||||||
assert_eq!(unit_keys, uk);
|
assert_eq!(unit_keys, uk);
|
||||||
@@ -3128,6 +3200,94 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unit_key_validation_gates_on_real_ciphertext() {
|
||||||
|
use crate::aacs::decrypt::{ALIGNED_UNIT_LEN, is_aacs_scrambled};
|
||||||
|
|
||||||
|
// No samples -> nothing to disprove against -> accept (sample-less paths
|
||||||
|
// like resume / mapfile must be unaffected).
|
||||||
|
assert!(super::aligned_unit_keys_validate(
|
||||||
|
&[(0, [0x11u8; 16])],
|
||||||
|
None,
|
||||||
|
&[]
|
||||||
|
));
|
||||||
|
|
||||||
|
// A clear unit (TS syncs intact) is not scrambled -> proves nothing ->
|
||||||
|
// accept even with an arbitrary key.
|
||||||
|
let mut clear = vec![0u8; ALIGNED_UNIT_LEN];
|
||||||
|
let mut off = 4;
|
||||||
|
while off < ALIGNED_UNIT_LEN {
|
||||||
|
clear[off] = 0x47;
|
||||||
|
off += 192;
|
||||||
|
}
|
||||||
|
assert!(!is_aacs_scrambled(&clear));
|
||||||
|
assert!(super::aligned_unit_keys_validate(
|
||||||
|
&[(0, [0x11u8; 16])],
|
||||||
|
None,
|
||||||
|
&[clear.clone()]
|
||||||
|
));
|
||||||
|
|
||||||
|
// A genuinely scrambled unit the RIGHT key restores to clear TS.
|
||||||
|
let uk = [0x5au8; 16];
|
||||||
|
let enc = encrypt_unit_for_test(&clear, &uk);
|
||||||
|
assert!(
|
||||||
|
is_aacs_scrambled(&enc),
|
||||||
|
"encrypted unit must read scrambled"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Right key -> de-scrambles -> accept (NO false reject of a good key).
|
||||||
|
assert!(super::aligned_unit_keys_validate(
|
||||||
|
&[(7, uk)],
|
||||||
|
None,
|
||||||
|
&[enc.clone()]
|
||||||
|
));
|
||||||
|
// Wrong key -> cannot de-scramble a scrambled sample -> reject.
|
||||||
|
assert!(!super::aligned_unit_keys_validate(
|
||||||
|
&[(7, [0x00u8; 16])],
|
||||||
|
None,
|
||||||
|
&[enc.clone()]
|
||||||
|
));
|
||||||
|
// Empty key set against a scrambled sample -> reject.
|
||||||
|
assert!(!super::aligned_unit_keys_validate(&[], None, &[enc]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inverse of `decrypt_unit` for one 6144-byte unit: produce on-disc
|
||||||
|
/// ciphertext that `decrypt_unit(uk)` restores to `clear`. Mirrors the AACS
|
||||||
|
/// unit algorithm — ECB-derive the per-unit key, then AES-CBC encrypt the
|
||||||
|
/// body with the fixed AACS IV.
|
||||||
|
fn encrypt_unit_for_test(clear: &[u8], uk: &[u8; 16]) -> Vec<u8> {
|
||||||
|
use crate::aacs::decrypt::{AACS_IV, ALIGNED_UNIT_LEN};
|
||||||
|
use aes::Aes128;
|
||||||
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
|
let mut unit = clear[..ALIGNED_UNIT_LEN].to_vec();
|
||||||
|
let mut header = [0u8; 16];
|
||||||
|
header.copy_from_slice(&unit[..16]);
|
||||||
|
let cipher = Aes128::new(GenericArray::from_slice(uk));
|
||||||
|
let mut blk = GenericArray::clone_from_slice(&header);
|
||||||
|
cipher.encrypt_block(&mut blk);
|
||||||
|
let mut dk = [0u8; 16];
|
||||||
|
for i in 0..16 {
|
||||||
|
dk[i] = blk[i] ^ header[i];
|
||||||
|
}
|
||||||
|
let bc = Aes128::new(GenericArray::from_slice(&dk));
|
||||||
|
let mut prev = AACS_IV;
|
||||||
|
let mut i = 16;
|
||||||
|
while i + 16 <= ALIGNED_UNIT_LEN {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
for j in 0..16 {
|
||||||
|
b[j] = unit[i + j] ^ prev[j];
|
||||||
|
}
|
||||||
|
let mut g = GenericArray::clone_from_slice(&b);
|
||||||
|
bc.encrypt_block(&mut g);
|
||||||
|
for j in 0..16 {
|
||||||
|
unit[i + j] = g[j];
|
||||||
|
}
|
||||||
|
prev.copy_from_slice(&unit[i..i + 16]);
|
||||||
|
i += 16;
|
||||||
|
}
|
||||||
|
unit
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inject_unit_keys_is_noop_without_aacs_on_unencrypted_or_css() {
|
fn inject_unit_keys_is_noop_without_aacs_on_unencrypted_or_css() {
|
||||||
// Unencrypted disc: nothing to inject into, stays None.
|
// Unencrypted disc: nothing to inject into, stays None.
|
||||||
|
|||||||
+32
-18
@@ -12,7 +12,6 @@
|
|||||||
//! stays in it.
|
//! stays in it.
|
||||||
|
|
||||||
use crate::disc::Key;
|
use crate::disc::Key;
|
||||||
use crate::error::Result;
|
|
||||||
|
|
||||||
/// The public AACS inputs a key source needs to look a disc up. Captured at
|
/// The public AACS inputs a key source needs to look a disc up. Captured at
|
||||||
/// scan; contains no secrets — only the disc identity and the on-disc AACS
|
/// scan; contains no secrets — only the disc identity and the on-disc AACS
|
||||||
@@ -38,26 +37,32 @@ pub struct DiscInputs {
|
|||||||
pub samples: Vec<Vec<u8>>,
|
pub samples: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A key source: given a disc's [`DiscInputs`], offer candidate [`Key`]s.
|
/// A key source: a stateful provider that hands a disc's candidate [`Key`]s out
|
||||||
|
/// **one at a time**, in whatever order it judges best for its backing store.
|
||||||
///
|
///
|
||||||
/// Dumb by contract — a source queries its backing store and enumerates the raw
|
/// Dumb by contract — a source queries its store and yields the raw material it
|
||||||
/// material it holds as candidate keys at whatever level it has (device /
|
/// holds at whatever level it has (device / processing / media / volume / unit).
|
||||||
/// processing / media / volume / unit). It performs NO AACS derivation and NO
|
/// It performs NO AACS derivation and NO validation: `Disc::decrypt_with`
|
||||||
/// validation; `Disc::decrypt_with` derives down, and the caller validates by
|
/// derives down AND validates against real ciphertext, returning `Err` for a key
|
||||||
/// decrypting a sample. That keeps every derivation step in one place (the
|
/// that does not decrypt this disc. That keeps every derivation step and the one
|
||||||
/// library) across AACS 1.0 / 2.0 / 2.1 / 2.x.
|
/// validation gate in the library, across AACS 1.0 / 2.0 / 2.1 / 2.x.
|
||||||
///
|
///
|
||||||
/// A source returns *multiple ordered candidates* because a single store can
|
/// The source is the one that knows how many candidates it has and in what order
|
||||||
/// hold material for several derivation paths (a keydb has a per-disc VUK *and*
|
/// to try them — a keydb holds a per-disc UK *and* VUK *and* a device-key pool,
|
||||||
/// a device-key pool *and* a media-key pool — the source can't know which
|
/// so it hands them out cheapest/most-specific first (UK ▸ VK ▸ MK ▸ DK) and
|
||||||
/// applies without the MKB walk, which is derivation). The caller tries the
|
/// reports exhaustion when its list runs out; an online key service or a mapfile
|
||||||
/// candidates in order and keeps the first that decrypts (validate-before-
|
/// cache hold exactly one. The caller drives the loop: `next_key` →
|
||||||
/// return). A source that resolves server-side (an online key service) or holds
|
/// `Disc::decrypt_with` → on `Err`, ask again → until a key decrypts or the
|
||||||
/// a cached final key (the mapfile) simply returns one candidate.
|
/// source returns `None` (a genuine "no key for this disc"). Compose several
|
||||||
|
/// sources, in the caller's chosen order, with [`crate`]'s `MultiSource`.
|
||||||
pub trait KeySource {
|
pub trait KeySource {
|
||||||
/// Candidate keys for this disc, most-specific first. Empty = this source
|
/// Hand the NEXT candidate key for this disc, or `None` once this source is
|
||||||
/// has nothing; `Err(_)` = the source itself failed (I/O, network, parse).
|
/// exhausted. Stateful: the source tracks what it already handed out this
|
||||||
fn resolve(&self, inputs: &DiscInputs) -> Result<Vec<Key>>;
|
/// session, so asking again after a rejected key yields the next candidate
|
||||||
|
/// (or `None`) — it never re-offers a key or re-hits a one-shot backend (an
|
||||||
|
/// online service is asked at most once). A source failure (I/O, network,
|
||||||
|
/// parse) surfaces as `None` — there is simply nothing more to try.
|
||||||
|
fn next_key(&mut self, inputs: &DiscInputs) -> Option<Key>;
|
||||||
|
|
||||||
/// Whether this source needs [`DiscInputs::samples`] populated (encrypted
|
/// Whether this source needs [`DiscInputs::samples`] populated (encrypted
|
||||||
/// content samples) — true for a source that validates against ciphertext
|
/// content samples) — true for a source that validates against ciphertext
|
||||||
@@ -66,4 +71,13 @@ pub trait KeySource {
|
|||||||
fn needs_samples(&self) -> bool {
|
fn needs_samples(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this source FAILED (I/O, network, parse) rather than simply
|
||||||
|
/// having no key. Checked after exhaustion so the caller can tell a genuine
|
||||||
|
/// "no key for this disc" apart from "the key service was unreachable". A
|
||||||
|
/// store that treats absence as not-an-error (a missing keydb / mapfile)
|
||||||
|
/// leaves this `false`.
|
||||||
|
fn errored(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -218,7 +218,9 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
// yields them for the stream below. Propagate a failed application
|
// yields them for the stream below. Propagate a failed application
|
||||||
// rather than silently muxing an undecryptable stream.
|
// rather than silently muxing an undecryptable stream.
|
||||||
if !opts.unit_keys.is_empty() {
|
if !opts.unit_keys.is_empty() {
|
||||||
disc.decrypt_with(crate::disc::Key::Unit(opts.unit_keys.clone()))
|
// These UKs were already resolved AND validated by the caller
|
||||||
|
// (the CLI's keydb loop), so no re-validation sample is needed.
|
||||||
|
disc.decrypt_with(crate::disc::Key::Unit(opts.unit_keys.clone()), &[])
|
||||||
.map_err(|e| -> io::Error { e.into() })?;
|
.map_err(|e| -> io::Error { e.into() })?;
|
||||||
}
|
}
|
||||||
// No-key guard: if decryption is requested (not --raw) and the disc
|
// No-key guard: if decryption is requested (not --raw) and the disc
|
||||||
|
|||||||
Reference in New Issue
Block a user