aacs: split no-key resolution reason into E7021 vs E7022
When key resolution had derivation material (device or processing keys) but no Volume ID was available to derive the unit key, surface Error::AacsVidUnavailable instead of the generic NoDiscKey. When there was no usable key material at all, keep NoDiscKey. resolve_keys_classical / resolve_keys_v21 still return a bare Option<ResolvedKeys> (all existing callers unchanged); a new resolve_keys_with_reason wrapper threads the typed ResolveFailure (VidUnavailable | NoMaterial) out. decrypt_with uses it; the ensure_decryptable_keys gate maps a captured AacsVidUnavailable reason to E7021, otherwise E7022. No decryption math, key derivation, or descramble logic changed -- only the reason reported on a resolution failure. Adds ensure_decryptable_aacs_vid_unavailable_vs_no_key proving both branches (device-keys + zero VID -> E7021; no keys -> E7022).
This commit is contained in:
@@ -1116,6 +1116,69 @@ pub struct ResolveContext<'a> {
|
|||||||
pub mkb: Option<&'a [u8]>,
|
pub mkb: Option<&'a [u8]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why a key resolution attempt produced no usable key.
|
||||||
|
///
|
||||||
|
/// Distinguishes the two no-key outcomes that an application must report
|
||||||
|
/// differently:
|
||||||
|
/// * [`ResolveFailure::VidUnavailable`] — the key source DID provide
|
||||||
|
/// derivation material (device or processing keys), but no Volume ID
|
||||||
|
/// (VID) was available to derive the Volume Unique Key. The fix is to
|
||||||
|
/// recover the VID (a drive / handshake problem), not to add keys.
|
||||||
|
/// * [`ResolveFailure::NoMaterial`] — no usable key material was found at
|
||||||
|
/// all (no DK/PK material, no disc-keyed hit). The fix is to add keys.
|
||||||
|
///
|
||||||
|
/// This carries no key bytes and is independent of the decryption math; it
|
||||||
|
/// is purely the *reason* a resolution returned no key.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ResolveFailure {
|
||||||
|
/// Derivation material was present (DKs or PKs) but no VID was available
|
||||||
|
/// to derive the unit key. Surfaced as [`crate::error::Error::AacsVidUnavailable`].
|
||||||
|
VidUnavailable,
|
||||||
|
/// No usable key material at all. Surfaced as
|
||||||
|
/// [`crate::error::Error::NoDiscKey`].
|
||||||
|
NoMaterial,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version-dispatched resolution that preserves the *reason* on failure.
|
||||||
|
///
|
||||||
|
/// Identical key derivation to the [`resolve_keys_v1`] / [`resolve_keys_v2`] /
|
||||||
|
/// [`resolve_keys_v21`] chain (it calls straight through to them); the only
|
||||||
|
/// addition is that an unresolved disc returns a typed [`ResolveFailure`]
|
||||||
|
/// instead of a bare `None`, so callers can report E7021 (material but no VID)
|
||||||
|
/// vs E7022 (no material). `version_u8` is the on-disc AACS major (1 → V10,
|
||||||
|
/// anything else → the V20/V21 chain), matching `AacsState::version`.
|
||||||
|
pub fn resolve_keys_with_reason(
|
||||||
|
ctx: &ResolveContext<'_>,
|
||||||
|
version_u8: u8,
|
||||||
|
) -> std::result::Result<ResolvedKeys, ResolveFailure> {
|
||||||
|
let resolved = match version_u8 {
|
||||||
|
1 => resolve_keys_v1(ctx),
|
||||||
|
_ => resolve_keys_v2(ctx).or_else(|| resolve_keys_v21(ctx)),
|
||||||
|
};
|
||||||
|
match resolved {
|
||||||
|
Some(r) => Ok(r),
|
||||||
|
None => Err(classify_resolve_failure(ctx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify why resolution found no key. The key source provided derivation
|
||||||
|
/// material (device or processing keys) but the VID sentinel is all-zero →
|
||||||
|
/// [`ResolveFailure::VidUnavailable`]; otherwise → [`ResolveFailure::NoMaterial`].
|
||||||
|
///
|
||||||
|
/// Reads only what the resolver already had (provider material + the VID
|
||||||
|
/// sentinel) — no key derivation, no descramble.
|
||||||
|
fn classify_resolve_failure(ctx: &ResolveContext<'_>) -> ResolveFailure {
|
||||||
|
let has_vid = *ctx.volume_id != [0u8; 16];
|
||||||
|
let providers = super::provider::Providers(ctx.providers);
|
||||||
|
let has_derivation_material =
|
||||||
|
!providers.device_keys().is_empty() || !providers.processing_keys().is_empty();
|
||||||
|
if !has_vid && has_derivation_material {
|
||||||
|
ResolveFailure::VidUnavailable
|
||||||
|
} else {
|
||||||
|
ResolveFailure::NoMaterial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// AACS 1.0 key resolution. Parses `Unit_Key_RO.inf` with 48-byte
|
/// AACS 1.0 key resolution. Parses `Unit_Key_RO.inf` with 48-byte
|
||||||
/// stride. Tries paths 1 → 4 in order.
|
/// stride. Tries paths 1 → 4 in order.
|
||||||
pub fn resolve_keys_v1(ctx: &ResolveContext<'_>) -> Option<ResolvedKeys> {
|
pub fn resolve_keys_v1(ctx: &ResolveContext<'_>) -> Option<ResolvedKeys> {
|
||||||
|
|||||||
+6
-6
@@ -32,12 +32,12 @@ pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
|||||||
pub use keys::probe;
|
pub use keys::probe;
|
||||||
pub use keys::{
|
pub use keys::{
|
||||||
AacsVersion, ContentCert, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C, MKB_TYPE_3_RECORDABLE,
|
AacsVersion, ContentCert, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C, MKB_TYPE_3_RECORDABLE,
|
||||||
MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbType, ResolveContext, ResolvedKeys,
|
MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbType, ResolveContext, ResolveFailure,
|
||||||
UnitKeyFile, decrypt_unit_key, derive_media_key_and_pk_from_dk, derive_media_key_from_dk,
|
ResolvedKeys, UnitKeyFile, decrypt_unit_key, derive_media_key_and_pk_from_dk,
|
||||||
derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, mkb_is_uhd,
|
derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex,
|
||||||
mkb_type, mkb_type_raw, mkb_version, parse_content_cert, parse_unit_key_ro,
|
mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, parse_content_cert,
|
||||||
read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21,
|
parse_unit_key_ro, read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2,
|
||||||
trim_mkb,
|
resolve_keys_v21, resolve_keys_with_reason, trim_mkb,
|
||||||
};
|
};
|
||||||
pub use provider::KeyProvider;
|
pub use provider::KeyProvider;
|
||||||
pub use variants::{
|
pub use variants::{
|
||||||
|
|||||||
+108
-7
@@ -2048,6 +2048,15 @@ impl Disc {
|
|||||||
let needs_key = matches!(keys, crate::decrypt::DecryptKeys::None);
|
let needs_key = matches!(keys, crate::decrypt::DecryptKeys::None);
|
||||||
if needs_key {
|
if needs_key {
|
||||||
if self.aacs.is_some() {
|
if self.aacs.is_some() {
|
||||||
|
// E7021 vs E7022 split: when key resolution had derivation
|
||||||
|
// material (device / processing keys) but no Volume ID to derive
|
||||||
|
// the unit key, the captured `aacs_error` is `AacsVidUnavailable`
|
||||||
|
// — report THAT (the fix is recovering the VID, not adding keys),
|
||||||
|
// not the generic `NoDiscKey`. Any other (or absent) reason →
|
||||||
|
// `NoDiscKey` naming the disc by hash, unchanged.
|
||||||
|
if matches!(self.aacs_error, Some(Error::AacsVidUnavailable)) {
|
||||||
|
return Err(Error::AacsVidUnavailable);
|
||||||
|
}
|
||||||
return Err(Error::NoDiscKey {
|
return Err(Error::NoDiscKey {
|
||||||
disc_hash: self.aacs_disc_hash(),
|
disc_hash: self.aacs_disc_hash(),
|
||||||
});
|
});
|
||||||
@@ -2267,13 +2276,12 @@ impl Disc {
|
|||||||
|
|
||||||
// Version dispatch — V10 uses the classical resolver at 48-byte
|
// Version dispatch — V10 uses the classical resolver at 48-byte
|
||||||
// stride; V20/V21 share the 64-byte stride, so try the classical V20
|
// stride; V20/V21 share the 64-byte stride, so try the classical V20
|
||||||
// paths first and fall back to the 2.1 variant chain.
|
// paths first and fall back to the 2.1 variant chain. The
|
||||||
let resolved = match version_u8 {
|
// reason-preserving wrapper threads the no-key cause out so the
|
||||||
1 => crate::aacs::resolve_keys_v1(&ctx),
|
// decrypt gate can report E7021 (had derivation material but no VID)
|
||||||
_ => crate::aacs::resolve_keys_v2(&ctx)
|
// vs E7022 (no usable material) instead of a flat AacsKeyRejected.
|
||||||
.or_else(|| crate::aacs::resolve_keys_v21(&ctx)),
|
let resolved = crate::aacs::resolve_keys_with_reason(&ctx, version_u8)
|
||||||
}
|
.map_err(|_reason| crate::error::Error::AacsKeyRejected)?;
|
||||||
.ok_or(crate::error::Error::AacsKeyRejected)?;
|
|
||||||
|
|
||||||
if resolved.unit_keys.is_empty() {
|
if resolved.unit_keys.is_empty() {
|
||||||
return Err(crate::error::Error::AacsKeyRejected);
|
return Err(crate::error::Error::AacsKeyRejected);
|
||||||
@@ -3995,6 +4003,99 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// E7021 vs E7022 split (rc.6 WS1). When key resolution HAD derivation
|
||||||
|
/// material (device / processing keys) but no Volume ID was available to
|
||||||
|
/// derive the unit key, the captured `aacs_error` is `AacsVidUnavailable`
|
||||||
|
/// — the gate must surface THAT (E7021), not the generic `NoDiscKey`
|
||||||
|
/// (E7022). When there was no usable key material at all, the reason is
|
||||||
|
/// absent and the gate keeps `NoDiscKey` (E7022). Both branches proven here.
|
||||||
|
#[test]
|
||||||
|
fn ensure_decryptable_aacs_vid_unavailable_vs_no_key() {
|
||||||
|
// Branch 1 — derivation material present, but no VID: E7021.
|
||||||
|
// The resolver classifies a device-keys-but-zero-VID context as
|
||||||
|
// `VidUnavailable`; that reason rides on `aacs_error`.
|
||||||
|
let supplied = crate::aacs::provider::SuppliedKey {
|
||||||
|
device_keys: vec![crate::aacs::DeviceKey {
|
||||||
|
key: [0x11; 16],
|
||||||
|
node: 1,
|
||||||
|
uv: 1,
|
||||||
|
u_mask_shift: 0,
|
||||||
|
}],
|
||||||
|
processing_keys: Vec::new(),
|
||||||
|
media_keys: Vec::new(),
|
||||||
|
disc_entry: None,
|
||||||
|
};
|
||||||
|
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
||||||
|
// A minimal but parseable Unit_Key_RO.inf (uk_pos=32, zero unit keys)
|
||||||
|
// so resolution proceeds to the path-try logic and fails for lack of a
|
||||||
|
// VID — not because the .inf failed to parse.
|
||||||
|
let mut uk_ro = vec![0u8; 40];
|
||||||
|
uk_ro[0..4].copy_from_slice(&32u32.to_be_bytes()); // uk_pos = 32
|
||||||
|
// num_unit_keys = 0 (BE16) at uk_pos -> parses to an empty key file.
|
||||||
|
let ctx = crate::aacs::ResolveContext {
|
||||||
|
unit_key_ro: &uk_ro,
|
||||||
|
content_cert: None,
|
||||||
|
volume_id: &[0u8; 16], // the "no VID" sentinel
|
||||||
|
providers: &provider_refs,
|
||||||
|
mkb: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
crate::aacs::resolve_keys_with_reason(&ctx, 2).err(),
|
||||||
|
Some(crate::aacs::ResolveFailure::VidUnavailable),
|
||||||
|
"device keys + zero VID must classify as VidUnavailable"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut disc_e7021 = make_test_disc(1000, "UHD");
|
||||||
|
disc_e7021.encrypted = true;
|
||||||
|
disc_e7021.aacs = Some(aacs_with(Vec::new())); // present but no unit keys
|
||||||
|
disc_e7021.aacs_error = Some(crate::error::Error::AacsVidUnavailable);
|
||||||
|
let err = disc_e7021
|
||||||
|
.ensure_decryptable(false)
|
||||||
|
.expect_err("AACS disc, material-but-no-VID, !raw must error");
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
crate::error::Error::AacsVidUnavailable.code(),
|
||||||
|
"material-but-no-VID must surface E7021 (AacsVidUnavailable), not E7022"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Branch 2 — no key material at all: classified NoMaterial, gate E7022.
|
||||||
|
let supplied_none = crate::aacs::provider::SuppliedKey {
|
||||||
|
device_keys: Vec::new(),
|
||||||
|
processing_keys: Vec::new(),
|
||||||
|
media_keys: Vec::new(),
|
||||||
|
disc_entry: None,
|
||||||
|
};
|
||||||
|
let provider_refs_none: [&dyn crate::aacs::KeyProvider; 1] = [&supplied_none];
|
||||||
|
let ctx_none = crate::aacs::ResolveContext {
|
||||||
|
unit_key_ro: &uk_ro,
|
||||||
|
content_cert: None,
|
||||||
|
volume_id: &[0u8; 16],
|
||||||
|
providers: &provider_refs_none,
|
||||||
|
mkb: None,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
crate::aacs::resolve_keys_with_reason(&ctx_none, 2).err(),
|
||||||
|
Some(crate::aacs::ResolveFailure::NoMaterial),
|
||||||
|
"no key material must classify as NoMaterial"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut disc_e7022 = make_test_disc(1000, "UHD");
|
||||||
|
disc_e7022.encrypted = true;
|
||||||
|
disc_e7022.aacs = Some(aacs_with(Vec::new()));
|
||||||
|
disc_e7022.aacs_error = None; // no reason captured → generic no-key
|
||||||
|
let err = disc_e7022
|
||||||
|
.ensure_decryptable(false)
|
||||||
|
.expect_err("AACS disc, no material, !raw must error");
|
||||||
|
assert_eq!(
|
||||||
|
err.code(),
|
||||||
|
crate::error::Error::NoDiscKey {
|
||||||
|
disc_hash: String::new()
|
||||||
|
}
|
||||||
|
.code(),
|
||||||
|
"no-material must keep E7022 (NoDiscKey)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Same AACS-no-key disc under `--raw` (raw=true) must PROCEED — the user
|
/// Same AACS-no-key disc under `--raw` (raw=true) must PROCEED — the user
|
||||||
/// asked for the encrypted image and needs no key.
|
/// asked for the encrypted image and needs no key.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user