aacs: remove the mod.rs facade — module paths are the public API
Delete the pub-use re-export facade; consumers now import from the owning module (aacs::content::decrypt_unit, aacs::mkb::MkbType, aacs::derive::derive_vuk, aacs::boil::mk_from_dk, aacs::resolve::resolve_keys_v2, ...). Internal callers repointed accordingly. Path-only change; logic hash identical (95fb9924); 2210 tests green.
This commit is contained in:
@@ -10,8 +10,8 @@
|
|||||||
pub fn collect_host_certs(
|
pub fn collect_host_certs(
|
||||||
opts: &crate::disc::ScanOptions,
|
opts: &crate::disc::ScanOptions,
|
||||||
mkb: Option<u32>,
|
mkb: Option<u32>,
|
||||||
) -> Vec<crate::aacs::HostCert> {
|
) -> Vec<crate::aacs::types::HostCert> {
|
||||||
let mut host_certs: Vec<crate::aacs::HostCert> = Vec::new();
|
let mut host_certs: Vec<crate::aacs::types::HostCert> = Vec::new();
|
||||||
if let Some(c) = &opts.credentials {
|
if let Some(c) = &opts.credentials {
|
||||||
host_certs.extend(c.host_certs.iter().cloned());
|
host_certs.extend(c.host_certs.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-57
@@ -50,62 +50,21 @@ pub const PATH_MKB_RW: &str = "/AACS/MKB_RW.inf";
|
|||||||
pub const PATH_CONTENT_CERT: &str = "/AACS/Content000.cer";
|
pub const PATH_CONTENT_CERT: &str = "/AACS/Content000.cer";
|
||||||
pub const PATH_CONTENT_CERT_ALT: &str = "/AACS/Content001.cer";
|
pub const PATH_CONTENT_CERT_ALT: &str = "/AACS/Content001.cer";
|
||||||
|
|
||||||
// Boil-down derivation primitives (thin newtypes + wrappers over the crypto).
|
// No facade: the module structure IS the public API. Consumers import from the
|
||||||
pub use boil::{
|
// owning module directly — e.g. `aacs::content::decrypt_unit`, `aacs::mkb::MkbType`,
|
||||||
KeyCandidate, MediaKey, ProcessingKey, ResolvedChain, UnitKey, Vid, Vuk, mk_from_dk,
|
// `aacs::derive::derive_vuk`, `aacs::boil::mk_from_dk`, `aacs::resolve::resolve_keys_v2`.
|
||||||
mk_from_pk, resolve_candidate, uk_from_vuk, vuk_from_mk,
|
// The `derive::probe` reproduction harness stays reachable via its module path.
|
||||||
};
|
|
||||||
// Structured, English-free resolution trace.
|
|
||||||
pub use trace::{KeyNode, KeyOutcome, KeyStep, ResolutionTrace, UnlockOutcome, UnlockStep};
|
|
||||||
|
|
||||||
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
|
||||||
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
|
||||||
pub use content::{
|
|
||||||
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
|
||||||
aacs_unit_needs_decrypt, aacs_unit_still_ciphertext, decrypt_bus, decrypt_unit,
|
|
||||||
decrypt_unit_checked, decrypt_unit_full, decrypt_unit_try_keys, fill_null_ts_unit,
|
|
||||||
is_unit_aligned, ts_packet_total, ts_sync_count, ts_sync_destroyed, unit_is_clean_ps,
|
|
||||||
unit_is_clean_ts, unit_key_validates,
|
|
||||||
};
|
|
||||||
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
|
||||||
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
|
||||||
// don't commit semver stability to test primitives.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use derive::probe;
|
|
||||||
pub use derive::{decrypt_unit_key, derive_vuk};
|
|
||||||
pub use derive::{
|
|
||||||
derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk,
|
|
||||||
recover_dk_position,
|
|
||||||
};
|
|
||||||
pub use inf::{
|
|
||||||
ContentCert, UnitKeyFile, disc_hash, disc_hash_hex, parse_content_cert, parse_unit_key_ro,
|
|
||||||
read_mkb_from_drive,
|
|
||||||
};
|
|
||||||
pub use mkb::{
|
|
||||||
AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C,
|
|
||||||
MKB_TYPE_3_RECORDABLE, MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbRecord, MkbType,
|
|
||||||
mkb_content_len, mkb_is_uhd, mkb_type, mkb_type_raw, mkb_version, trim_mkb, walk_mkb,
|
|
||||||
};
|
|
||||||
pub use provider::KeyProvider;
|
|
||||||
pub use resolve::{
|
|
||||||
ResolveContext, ResolveFailure, ResolvedKeys, resolve_keys_v1, resolve_keys_v2,
|
|
||||||
resolve_keys_v21, resolve_keys_with_reason,
|
|
||||||
};
|
|
||||||
pub use types::{DeviceKey, DiscEntry, HostCert};
|
|
||||||
pub use variant::{
|
|
||||||
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, ProcessingKeyMatch,
|
|
||||||
derive_media_key_variant, is_variant_mkb, variant_nonce, walk_processing_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
//! Re-export surface guards. The module's public API is the set of
|
//! Surface guards. The public API is the module tree itself (no facade).
|
||||||
//! `pub use` items above. A regression that drops or renames an export
|
//! Touching one representative item per module keeps these as a
|
||||||
//! (the class of bug that shipped in 0.31.0 by silently changing a
|
//! compile-time contract that the module paths stay stable.
|
||||||
//! surface) breaks compilation of these references, so they act as a
|
|
||||||
//! compile-time contract for the crate's AACS surface.
|
|
||||||
|
|
||||||
use super::*;
|
use super::content::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
||||||
|
use super::inf::{disc_hash, disc_hash_hex};
|
||||||
|
use super::mkb::{AacsVersion, mkb_content_len, walk_mkb};
|
||||||
|
use super::variant::{KEY_CORRECTION_DATA_PLACEHOLDER, is_variant_mkb};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aligned_unit_len_is_three_2048_byte_sectors() {
|
fn aligned_unit_len_is_three_2048_byte_sectors() {
|
||||||
@@ -134,14 +93,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn public_helpers_are_callable_through_the_facade() {
|
fn public_helpers_are_callable_by_module_path() {
|
||||||
// Touch a representative function from each re-export group so a
|
// Touch a representative function from each module so a dropped/renamed
|
||||||
// dropped/renamed export fails to compile. These are smoke calls, not
|
// item fails to compile. Smoke calls, not behavioural assertions.
|
||||||
// behavioural assertions (behaviour is covered in each module).
|
|
||||||
let _ = ts_sync_destroyed(&[0u8; ALIGNED_UNIT_LEN]);
|
let _ = ts_sync_destroyed(&[0u8; ALIGNED_UNIT_LEN]);
|
||||||
let _ = mkb_content_len(&[]);
|
let _ = mkb_content_len(&[]);
|
||||||
let _ = is_variant_mkb(&walk_mkb(&[]));
|
let _ = is_variant_mkb(&walk_mkb(&[]));
|
||||||
let _ = disc_hash_hex(&disc_hash(b"x"));
|
let _ = disc_hash_hex(&disc_hash(b"x"));
|
||||||
let _ = mk_from_pk(&[[0u8; 16]], &[]);
|
let _ = super::boil::mk_from_pk(&[[0u8; 16]], &[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-15
@@ -1068,7 +1068,7 @@ mod tests {
|
|||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1111,7 +1111,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let vid = [0u8; 16];
|
let vid = [0u8; 16];
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1153,7 +1153,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let vid = [0u8; 16];
|
let vid = [0u8; 16];
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1193,7 +1193,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let vid = [0u8; 16];
|
let vid = [0u8; 16];
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1240,7 +1240,7 @@ mod tests {
|
|||||||
media_keys: vec![km],
|
media_keys: vec![km],
|
||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1627,7 +1627,7 @@ mod tests {
|
|||||||
mkb.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
mkb.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
||||||
mkb.extend_from_slice(&[0x55; 16]);
|
mkb.extend_from_slice(&[0x55; 16]);
|
||||||
|
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1663,7 +1663,7 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
|
let mkb = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 1];
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1697,7 +1697,7 @@ mod tests {
|
|||||||
let mut cc = vec![0u8; 20];
|
let mut cc = vec![0u8; 20];
|
||||||
cc[0] = 0x10;
|
cc[0] = 0x10;
|
||||||
cc[1] = 0x80;
|
cc[1] = 0x80;
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: Some(&cc),
|
content_cert: Some(&cc),
|
||||||
@@ -1731,7 +1731,7 @@ mod tests {
|
|||||||
unit_keys: Vec::new(),
|
unit_keys: Vec::new(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &data,
|
unit_key_ro: &data,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1769,7 +1769,7 @@ mod tests {
|
|||||||
unit_keys: Vec::new(),
|
unit_keys: Vec::new(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1786,7 +1786,7 @@ mod tests {
|
|||||||
fn resolve_keys_returns_none_when_no_provider_has_anything() {
|
fn resolve_keys_returns_none_when_no_provider_has_anything() {
|
||||||
// Empty provider array + VID present + no MKB → all paths miss → None.
|
// Empty provider array + VID present + no MKB → all paths miss → None.
|
||||||
let uk_ro = minimal_unit_key_ro();
|
let uk_ro = minimal_unit_key_ro();
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
@@ -1969,7 +1969,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_processing_keys_zero_vid_is_vid_unavailable() {
|
fn classify_processing_keys_zero_vid_is_vid_unavailable() {
|
||||||
let prov = material_provider(Vec::new(), vec![[0u8; 16]]);
|
let prov = material_provider(Vec::new(), vec![[0u8; 16]]);
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&prov];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&prov];
|
||||||
let uk_ro = minimal_unit_key_ro();
|
let uk_ro = minimal_unit_key_ro();
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
@@ -1994,7 +1994,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_vid_present_with_material_is_no_material_not_vid() {
|
fn classify_vid_present_with_material_is_no_material_not_vid() {
|
||||||
let prov = material_provider(vec![one_device_key()], vec![[0u8; 16]]);
|
let prov = material_provider(vec![one_device_key()], vec![[0u8; 16]]);
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&prov];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&prov];
|
||||||
let uk_ro = minimal_unit_key_ro();
|
let uk_ro = minimal_unit_key_ro();
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
@@ -2017,7 +2017,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn classify_vid_present_no_material_is_no_material() {
|
fn classify_vid_present_no_material_is_no_material() {
|
||||||
let prov = material_provider(Vec::new(), Vec::new());
|
let prov = material_provider(Vec::new(), Vec::new());
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&prov];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&prov];
|
||||||
let uk_ro = minimal_unit_key_ro();
|
let uk_ro = minimal_unit_key_ro();
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
@@ -2055,7 +2055,7 @@ mod tests {
|
|||||||
unit_keys: Vec::new(),
|
unit_keys: Vec::new(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let providers: &[&dyn super::super::KeyProvider] = &[&keydb];
|
let providers: &[&dyn super::super::provider::KeyProvider] = &[&keydb];
|
||||||
let ctx = ResolveContext {
|
let ctx = ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
|
|||||||
+48
-44
@@ -244,7 +244,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Strip CPS-unit IDs — the decrypt primitives only want the raw key bytes.
|
// Strip CPS-unit IDs — the decrypt primitives only want the raw key bytes.
|
||||||
let raw_keys: Vec<[u8; 16]> = unit_keys.iter().map(|(_, k)| *k).collect();
|
let raw_keys: Vec<[u8; 16]> = unit_keys.iter().map(|(_, k)| *k).collect();
|
||||||
let rdk: Option<[u8; 16]> = *read_data_key;
|
let rdk: Option<[u8; 16]> = *read_data_key;
|
||||||
let unit_len = aacs::ALIGNED_UNIT_LEN;
|
let unit_len = aacs::content::ALIGNED_UNIT_LEN;
|
||||||
// AACS decrypts whole 6144-byte aligned units. The live mux path
|
// AACS decrypts whole 6144-byte aligned units. The live mux path
|
||||||
// (mux/disc.rs::fill_extents) issues 1- or 2-sector reads at every
|
// (mux/disc.rs::fill_extents) issues 1- or 2-sector reads at every
|
||||||
// extent tail, so a buffer is commonly NOT a multiple of the unit
|
// extent tail, so a buffer is commonly NOT a multiple of the unit
|
||||||
@@ -288,8 +288,8 @@ fn decrypt_sectors_impl(
|
|||||||
};
|
};
|
||||||
if partial_in_content {
|
if partial_in_content {
|
||||||
let partial = &buf[buf.len() - partial_len..];
|
let partial = &buf[buf.len() - partial_len..];
|
||||||
let packets = aacs::ts_packet_total(partial);
|
let packets = aacs::content::ts_packet_total(partial);
|
||||||
if packets > 0 && aacs::ts_sync_count(partial) <= packets / 2 {
|
if packets > 0 && aacs::content::ts_sync_count(partial) <= packets / 2 {
|
||||||
return Err(crate::error::Error::DecryptFailed);
|
return Err(crate::error::Error::DecryptFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +326,7 @@ fn decrypt_sectors_impl(
|
|||||||
// must happen first — it's a shared layer on top that is key-independent
|
// must happen first — it's a shared layer on top that is key-independent
|
||||||
// across all CPS units on the disc.
|
// across all CPS units on the disc.
|
||||||
let decrypt_one = |chunk: &mut [u8]| {
|
let decrypt_one = |chunk: &mut [u8]| {
|
||||||
if chunk.len() != unit_len || !aacs::aacs_unit_needs_decrypt(chunk) {
|
if chunk.len() != unit_len || !aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Save original bytes so we can restore if no key validates.
|
// Save original bytes so we can restore if no key validates.
|
||||||
@@ -335,7 +335,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Build a bus-decrypted copy to try unit keys against, or work
|
// Build a bus-decrypted copy to try unit keys against, or work
|
||||||
// in-place when there is no bus layer.
|
// in-place when there is no bus layer.
|
||||||
if let Some(ref rdk_key) = rdk {
|
if let Some(ref rdk_key) = rdk {
|
||||||
aacs::decrypt_bus(chunk, rdk_key);
|
aacs::content::decrypt_bus(chunk, rdk_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reorder the key iterator: try the cached hint first, then fall
|
// Reorder the key iterator: try the cached hint first, then fall
|
||||||
@@ -349,7 +349,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Work on a per-key copy so a failing attempt doesn't
|
// Work on a per-key copy so a failing attempt doesn't
|
||||||
// clobber the bus-decrypted base we'll retry on.
|
// clobber the bus-decrypted base we'll retry on.
|
||||||
let mut attempt: Vec<u8> = chunk.to_vec();
|
let mut attempt: Vec<u8> = chunk.to_vec();
|
||||||
if aacs::decrypt_unit(&mut attempt, key) {
|
if aacs::content::decrypt_unit(&mut attempt, key) {
|
||||||
chunk.copy_from_slice(&attempt);
|
chunk.copy_from_slice(&attempt);
|
||||||
last_key_idx.store(idx, Ordering::Relaxed);
|
last_key_idx.store(idx, Ordering::Relaxed);
|
||||||
return;
|
return;
|
||||||
@@ -478,7 +478,7 @@ mod tests {
|
|||||||
/// not left scrambled.
|
/// not left scrambled.
|
||||||
#[test]
|
#[test]
|
||||||
fn nav_file_unit_survives_decrypt_attempt() {
|
fn nav_file_unit_survives_decrypt_attempt() {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
unit[0] = b'M';
|
unit[0] = b'M';
|
||||||
unit[1] = b'P';
|
unit[1] = b'P';
|
||||||
unit[2] = b'L';
|
unit[2] = b'L';
|
||||||
@@ -528,7 +528,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < len {
|
while u < len {
|
||||||
v[u] |= 0xC0;
|
v[u] |= 0xC0;
|
||||||
u += aacs::ALIGNED_UNIT_LEN;
|
u += aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
@@ -561,7 +561,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
|
|
||||||
// base_lba 0, content = [(100,10)] ⇒ the unit at LBA 0 is OUTSIDE content.
|
// base_lba 0, content = [(100,10)] ⇒ the unit at LBA 0 is OUTSIDE content.
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
@@ -581,7 +581,7 @@ mod tests {
|
|||||||
decrypt_sectors_in_content(&mut buf2, &mut keys, 0, 100, &[(100, 10)]).unwrap();
|
decrypt_sectors_in_content(&mut buf2, &mut keys, 0, 100, &[(100, 10)]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped2,
|
dropped2,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"an undecryptable CONTENT unit IS counted as loss"
|
"an undecryptable CONTENT unit IS counted as loss"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -594,12 +594,12 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = scrambled_region(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = scrambled_region(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
// unit0 @ LBA 0 (clear/skip), unit1 @ LBA 3 (content). Content = [(3,3)].
|
// unit0 @ LBA 0 (clear/skip), unit1 @ LBA 3 (content). Content = [(3,3)].
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(3, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(3, 3)]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"only the in-content unit (unit1) is checked; clear unit0 is skipped"
|
"only the in-content unit (unit1) is checked; clear unit0 is skipped"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut keys_u = keys_g.clone();
|
let mut keys_u = keys_g.clone();
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut g = original.clone();
|
let mut g = original.clone();
|
||||||
let mut u = original.clone();
|
let mut u = original.clone();
|
||||||
let gated = decrypt_sectors_in_content(&mut g, &mut keys_g, 0, 0, &[(0, 3)]).unwrap();
|
let gated = decrypt_sectors_in_content(&mut g, &mut keys_g, 0, 0, &[(0, 3)]).unwrap();
|
||||||
@@ -662,7 +662,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -680,7 +680,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let original = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, 0, "a clear in-content unit is not ciphertext");
|
assert_eq!(dropped, 0, "a clear in-content unit is not ciphertext");
|
||||||
@@ -691,7 +691,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn content_gate_none_keys_is_noop() {
|
fn content_gate_none_keys_is_noop() {
|
||||||
let mut keys = DecryptKeys::None;
|
let mut keys = DecryptKeys::None;
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, 0);
|
assert_eq!(dropped, 0);
|
||||||
@@ -721,7 +721,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let u = aacs::ALIGNED_UNIT_LEN;
|
let u = aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let mut buf = vec![0u8; 3 * u];
|
let mut buf = vec![0u8; 3 * u];
|
||||||
buf[..u].copy_from_slice(&scrambled_region(u)); // unit0 @ LBA0 scrambled
|
buf[..u].copy_from_slice(&scrambled_region(u)); // unit0 @ LBA0 scrambled
|
||||||
buf[u..2 * u].copy_from_slice(&clear_ts_region(u)); // unit1 @ LBA3 clear
|
buf[u..2 * u].copy_from_slice(&clear_ts_region(u)); // unit1 @ LBA3 clear
|
||||||
@@ -738,10 +738,14 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = scrambled_region(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = scrambled_region(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
// unit0 @ LBA0 content, unit1 @ LBA3 out. Content = [(0,3)].
|
// unit0 @ LBA0 content, unit1 @ LBA3 out. Content = [(0,3)].
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, aacs::ALIGNED_UNIT_LEN, "only unit0 counts");
|
assert_eq!(
|
||||||
|
dropped,
|
||||||
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
|
"only unit0 counts"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The trailing-partial reject is ALSO content-gated: a scrambled partial
|
/// The trailing-partial reject is ALSO content-gated: a scrambled partial
|
||||||
@@ -755,7 +759,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// One full clear unit + a scrambled single-sector partial, all OUTSIDE
|
// One full clear unit + a scrambled single-sector partial, all OUTSIDE
|
||||||
// content → the partial must be tolerated (Ok), not DecryptFailed.
|
// content → the partial must be tolerated (Ok), not DecryptFailed.
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
buf.extend_from_slice(&scrambled_region(2048));
|
buf.extend_from_slice(&scrambled_region(2048));
|
||||||
// content far away → both the full unit and the partial are non-content.
|
// content far away → both the full unit and the partial are non-content.
|
||||||
let res = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(1000, 3)]);
|
let res = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(1000, 3)]);
|
||||||
@@ -776,7 +780,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
// One full scrambled unit + a 2048-byte (single-sector) CLEAR tail.
|
// One full scrambled unit + a 2048-byte (single-sector) CLEAR tail.
|
||||||
let unit = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let unit = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let tail = clear_ts_region(2048);
|
let tail = clear_ts_region(2048);
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
@@ -784,7 +788,7 @@ mod tests {
|
|||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("clear trailing partial is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("clear trailing partial is Ok");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[aacs::ALIGNED_UNIT_LEN..],
|
&buf[aacs::content::ALIGNED_UNIT_LEN..],
|
||||||
&tail[..],
|
&tail[..],
|
||||||
"clear trailing partial unit must be left unchanged"
|
"clear trailing partial unit must be left unchanged"
|
||||||
);
|
);
|
||||||
@@ -801,7 +805,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
// One full unit + a 4096-byte (two-sector) SCRAMBLED tail.
|
// One full unit + a 4096-byte (two-sector) SCRAMBLED tail.
|
||||||
let unit = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let unit = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let tail = scrambled_region(4096);
|
let tail = scrambled_region(4096);
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
@@ -835,7 +839,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN * 2);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN * 2);
|
||||||
let snapshot = buf.clone();
|
let snapshot = buf.clone();
|
||||||
|
|
||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("exact-multiple buffer is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("exact-multiple buffer is Ok");
|
||||||
@@ -1092,7 +1096,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
||||||
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1112,7 +1116,7 @@ mod tests {
|
|||||||
unit_keys: vec![],
|
unit_keys: vec![],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
||||||
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1124,7 @@ mod tests {
|
|||||||
// ── Multi-CPS-unit key selection ──────────────────────────────────────
|
// ── Multi-CPS-unit key selection ──────────────────────────────────────
|
||||||
|
|
||||||
/// Encrypt an aligned unit with the AACS algorithm run in reverse so that
|
/// Encrypt an aligned unit with the AACS algorithm run in reverse so that
|
||||||
/// `aacs::decrypt_unit` with the same key recovers the plaintext. Mirrors
|
/// `aacs::content::decrypt_unit` with the same key recovers the plaintext. Mirrors
|
||||||
/// the `aacs_encrypt_unit` helper in `aacs::content::tests`.
|
/// the `aacs_encrypt_unit` helper in `aacs::content::tests`.
|
||||||
fn aacs_encrypt_unit_for_test(unit: &mut [u8], unit_key: &[u8; 16]) {
|
fn aacs_encrypt_unit_for_test(unit: &mut [u8], unit_key: &[u8; 16]) {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
@@ -1136,7 +1140,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::crypto::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let num_blocks = (aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let num_blocks = (aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..num_blocks {
|
for i in 0..num_blocks {
|
||||||
let off = 16 + i * 16;
|
let off = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1153,9 +1157,9 @@ mod tests {
|
|||||||
/// (offset 4 + k*192) so `ts_sync_destroyed` reports false and
|
/// (offset 4 + k*192) so `ts_sync_destroyed` reports false and
|
||||||
/// `decrypt_unit` verifies it as clear after decryption.
|
/// `decrypt_unit` verifies it as clear after decryption.
|
||||||
fn clear_ts_unit() -> Vec<u8> {
|
fn clear_ts_unit() -> Vec<u8> {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
@@ -1171,7 +1175,7 @@ mod tests {
|
|||||||
/// garbage for content under key ≥ 1. The fix tries every key and
|
/// garbage for content under key ≥ 1. The fix tries every key and
|
||||||
/// accepts the one whose output passes the TS-sync verify.
|
/// accepts the one whose output passes the TS-sync verify.
|
||||||
///
|
///
|
||||||
/// Grounding: `for idx in try_order { … if aacs::decrypt_unit(&mut attempt, key) { … } }`
|
/// Grounding: `for idx in try_order { … if aacs::content::decrypt_unit(&mut attempt, key) { … } }`
|
||||||
/// Mutation: revert to the pre-fix `decrypt_unit_full(chunk, &uk, …)` where
|
/// Mutation: revert to the pre-fix `decrypt_unit_full(chunk, &uk, …)` where
|
||||||
/// `uk = raw_keys[unit_key_idx]` (always key 0) → the unit comes out as
|
/// `uk = raw_keys[unit_key_idx]` (always key 0) → the unit comes out as
|
||||||
/// garbled bytes that still look scrambled, failing the `!ts_sync_destroyed`
|
/// garbled bytes that still look scrambled, failing the `!ts_sync_destroyed`
|
||||||
@@ -1185,7 +1189,7 @@ mod tests {
|
|||||||
let mut unit = clear_ts_unit();
|
let mut unit = clear_ts_unit();
|
||||||
aacs_encrypt_unit_for_test(&mut unit, &key1);
|
aacs_encrypt_unit_for_test(&mut unit, &key1);
|
||||||
assert!(
|
assert!(
|
||||||
aacs::ts_sync_destroyed(&unit),
|
aacs::content::ts_sync_destroyed(&unit),
|
||||||
"encrypted unit must look scrambled before decrypt"
|
"encrypted unit must look scrambled before decrypt"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1199,13 +1203,13 @@ mod tests {
|
|||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("multi-CPS decrypt must succeed");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("multi-CPS decrypt must succeed");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf),
|
!aacs::content::ts_sync_destroyed(&buf),
|
||||||
"unit encrypted under key1 must be fully decrypted (TS syncs restored)"
|
"unit encrypted under key1 must be fully decrypted (TS syncs restored)"
|
||||||
);
|
);
|
||||||
// Every sync position must carry 0x47.
|
// Every sync position must carry 0x47.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
aacs::ts_sync_count(&buf),
|
aacs::content::ts_sync_count(&buf),
|
||||||
aacs::ts_packet_total(&buf),
|
aacs::content::ts_packet_total(&buf),
|
||||||
"all TS sync bytes must be restored after decrypting under key1"
|
"all TS sync bytes must be restored after decrypting under key1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1230,12 +1234,12 @@ mod tests {
|
|||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("single-key disc must decrypt");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("single-key disc must decrypt");
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf),
|
!aacs::content::ts_sync_destroyed(&buf),
|
||||||
"single-key disc: TS syncs must be restored"
|
"single-key disc: TS syncs must be restored"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
aacs::ts_sync_count(&buf),
|
aacs::content::ts_sync_count(&buf),
|
||||||
aacs::ts_packet_total(&buf),
|
aacs::content::ts_packet_total(&buf),
|
||||||
"all TS sync bytes must be restored for single-key disc"
|
"all TS sync bytes must be restored for single-key disc"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1267,7 @@ mod tests {
|
|||||||
aacs_encrypt_unit_for_test(&mut unit, &real_key);
|
aacs_encrypt_unit_for_test(&mut unit, &real_key);
|
||||||
let ciphertext = unit.clone();
|
let ciphertext = unit.clone();
|
||||||
assert!(
|
assert!(
|
||||||
aacs::ts_sync_destroyed(&unit),
|
aacs::content::ts_sync_destroyed(&unit),
|
||||||
"encrypted unit must look scrambled going in"
|
"encrypted unit must look scrambled going in"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1277,7 +1281,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"the whole scrambled unit must be reported as dropped when no key validates"
|
"the whole scrambled unit must be reported as dropped when no key validates"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1306,7 +1310,7 @@ mod tests {
|
|||||||
aacs_encrypt_unit_for_test(&mut unit_b, &wrong);
|
aacs_encrypt_unit_for_test(&mut unit_b, &wrong);
|
||||||
let unit_b_ciphertext = unit_b.clone();
|
let unit_b_ciphertext = unit_b.clone();
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = Vec::with_capacity(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
buf.extend_from_slice(&unit_a);
|
buf.extend_from_slice(&unit_a);
|
||||||
buf.extend_from_slice(&unit_b);
|
buf.extend_from_slice(&unit_b);
|
||||||
|
|
||||||
@@ -1318,15 +1322,15 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"exactly one unit's worth of bytes must be reported dropped"
|
"exactly one unit's worth of bytes must be reported dropped"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf[..aacs::ALIGNED_UNIT_LEN]),
|
!aacs::content::ts_sync_destroyed(&buf[..aacs::content::ALIGNED_UNIT_LEN]),
|
||||||
"the decryptable unit must come out clear"
|
"the decryptable unit must come out clear"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[aacs::ALIGNED_UNIT_LEN..],
|
&buf[aacs::content::ALIGNED_UNIT_LEN..],
|
||||||
&unit_b_ciphertext[..],
|
&unit_b_ciphertext[..],
|
||||||
"the undecryptable unit must be restored to ciphertext"
|
"the undecryptable unit must be restored to ciphertext"
|
||||||
);
|
);
|
||||||
|
|||||||
+26
-23
@@ -86,9 +86,9 @@ impl AacsCertUnlocker<'_> {
|
|||||||
// MKB generation (best-effort) — forwarded to each source's
|
// MKB generation (best-effort) — forwarded to each source's
|
||||||
// `host_certs(mkb)` so a source MAY select a generation-appropriate cert
|
// `host_certs(mkb)` so a source MAY select a generation-appropriate cert
|
||||||
// (the default impl ignores it). A read failure leaves it `None`.
|
// (the default impl ignores it). A read failure leaves it `None`.
|
||||||
let mkb_gen = aacs::read_mkb_from_drive(session.scsi_mut())
|
let mkb_gen = aacs::inf::read_mkb_from_drive(session.scsi_mut())
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|m| aacs::mkb_version(&m));
|
.and_then(|m| aacs::mkb::mkb_version(&m));
|
||||||
|
|
||||||
// Host certs are keysource-served, never compiled in — unioned from the
|
// Host certs are keysource-served, never compiled in — unioned from the
|
||||||
// explicit `DriveCredentials` and the key-source layer. With ZERO certs
|
// explicit `DriveCredentials` and the key-source layer. With ZERO certs
|
||||||
@@ -152,10 +152,10 @@ fn unlock_error_to_error(e: &CertUnlockFailure) -> Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a [`CertUnlockFailure`] to a structured [`crate::aacs::UnlockOutcome`]
|
/// Map a [`CertUnlockFailure`] to a structured [`crate::aacs::trace::UnlockOutcome`]
|
||||||
/// for the resolution trace (English-free).
|
/// for the resolution trace (English-free).
|
||||||
fn cert_unlock_outcome(e: &CertUnlockFailure) -> crate::aacs::UnlockOutcome {
|
fn cert_unlock_outcome(e: &CertUnlockFailure) -> crate::aacs::trace::UnlockOutcome {
|
||||||
use crate::aacs::UnlockOutcome;
|
use crate::aacs::trace::UnlockOutcome;
|
||||||
use freemkv_unlock::UnlockError;
|
use freemkv_unlock::UnlockError;
|
||||||
match e {
|
match e {
|
||||||
CertUnlockFailure::NoHostCert { mkb } => UnlockOutcome::NoUsableHostCert { mkb: *mkb },
|
CertUnlockFailure::NoHostCert { mkb } => UnlockOutcome::NoUsableHostCert { mkb: *mkb },
|
||||||
@@ -230,7 +230,10 @@ impl Disc {
|
|||||||
/// `mkb` is the disc's MKB generation when known, forwarded to each source's
|
/// `mkb` is the disc's MKB generation when known, forwarded to each source's
|
||||||
/// [`crate::KeySource::host_certs`] so a source MAY return only
|
/// [`crate::KeySource::host_certs`] so a source MAY return only
|
||||||
/// generation-appropriate certs (the default ignores it).
|
/// generation-appropriate certs (the default ignores it).
|
||||||
fn collect_host_certs(opts: &ScanOptions, mkb: Option<u32>) -> Vec<crate::aacs::HostCert> {
|
fn collect_host_certs(
|
||||||
|
opts: &ScanOptions,
|
||||||
|
mkb: Option<u32>,
|
||||||
|
) -> Vec<crate::aacs::types::HostCert> {
|
||||||
// Delegates to the shared cert primitive (the external freemkv-unlock-aacs
|
// Delegates to the shared cert primitive (the external freemkv-unlock-aacs
|
||||||
// plugin uses the same one). Kept as a thin Disc method so the existing
|
// plugin uses the same one). Kept as a thin Disc method so the existing
|
||||||
// collect_host_certs_* unit tests and call sites are unchanged.
|
// collect_host_certs_* unit tests and call sites are unchanged.
|
||||||
@@ -312,14 +315,14 @@ impl Disc {
|
|||||||
.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO)
|
.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO)
|
||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO_DUPLICATE))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO_DUPLICATE))
|
||||||
.map_err(|_| Error::AacsNoKeys)?;
|
.map_err(|_| Error::AacsNoKeys)?;
|
||||||
let dh = aacs::disc_hash(&uk_ro_data);
|
let dh = aacs::inf::disc_hash(&uk_ro_data);
|
||||||
|
|
||||||
let cc = udf_fs
|
let cc = udf_fs
|
||||||
.read_file(reader, crate::aacs::PATH_CONTENT_CERT)
|
.read_file(reader, crate::aacs::PATH_CONTENT_CERT)
|
||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
||||||
.ok()
|
.ok()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(aacs::parse_content_cert);
|
.and_then(aacs::inf::parse_content_cert);
|
||||||
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
|
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
|
||||||
// No-cert default = UHD (V20 stride), matching `read_aacs_version` so the
|
// No-cert default = UHD (V20 stride), matching `read_aacs_version` so the
|
||||||
// scanned `AacsState.version` and the out-of-band fetch agree. A wrong
|
// scanned `AacsState.version` and the out-of-band fetch agree. A wrong
|
||||||
@@ -328,7 +331,7 @@ impl Disc {
|
|||||||
let version = cc
|
let version = cc
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.version.major())
|
.map(|c| c.version.major())
|
||||||
.unwrap_or(aacs::AACS_MAJOR_UHD);
|
.unwrap_or(aacs::mkb::AACS_MAJOR_UHD);
|
||||||
|
|
||||||
// Bus-encryption gate (wrong-keys guard). A bus-encrypted disc (Content
|
// Bus-encryption gate (wrong-keys guard). A bus-encrypted disc (Content
|
||||||
// Certificate bus-encryption bit set) carries bus encryption on its
|
// Certificate bus-encryption bit set) carries bus encryption on its
|
||||||
@@ -394,12 +397,12 @@ impl Disc {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mkb_ver = aacs::mkb_version(&mkb_bytes);
|
let mkb_ver = aacs::mkb::mkb_version(&mkb_bytes);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "scan_aacs_vid_only",
|
phase = "scan_aacs_vid_only",
|
||||||
disc_hash = %aacs::disc_hash_hex(&dh),
|
disc_hash = %aacs::inf::disc_hash_hex(&dh),
|
||||||
version,
|
version,
|
||||||
bus_encryption,
|
bus_encryption,
|
||||||
has_vid = handshake.is_some(),
|
has_vid = handshake.is_some(),
|
||||||
@@ -410,7 +413,7 @@ impl Disc {
|
|||||||
version,
|
version,
|
||||||
bus_encryption,
|
bus_encryption,
|
||||||
mkb_version: mkb_ver,
|
mkb_version: mkb_ver,
|
||||||
disc_hash: aacs::disc_hash_hex(&dh),
|
disc_hash: aacs::inf::disc_hash_hex(&dh),
|
||||||
key_source: KeyOrigin::ExternalUk,
|
key_source: KeyOrigin::ExternalUk,
|
||||||
vuk: None,
|
vuk: None,
|
||||||
unit_keys: vec![],
|
unit_keys: vec![],
|
||||||
@@ -687,14 +690,14 @@ mod tests {
|
|||||||
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.version,
|
st.version,
|
||||||
aacs::AACS_MAJOR_UHD,
|
aacs::mkb::AACS_MAJOR_UHD,
|
||||||
"no cert → default UHD (major 2)"
|
"no cert → default UHD (major 2)"
|
||||||
);
|
);
|
||||||
assert!(!st.bus_encryption);
|
assert!(!st.bus_encryption);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// disc_hash is SHA1 of the Unit_Key_RO.inf bytes, hex with 0x prefix
|
/// disc_hash is SHA1 of the Unit_Key_RO.inf bytes, hex with 0x prefix
|
||||||
/// and uppercase (aacs::disc_hash + disc_hash_hex). The state's
|
/// and uppercase (aacs::inf::disc_hash + disc_hash_hex). The state's
|
||||||
/// disc_hash must match independently computing it over the same bytes.
|
/// disc_hash must match independently computing it over the same bytes.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_vid_only_disc_hash_is_sha1_of_unit_key_ro() {
|
fn resolve_vid_only_disc_hash_is_sha1_of_unit_key_ro() {
|
||||||
@@ -710,7 +713,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
||||||
let expected = aacs::disc_hash_hex(&aacs::disc_hash(&uk));
|
let expected = aacs::inf::disc_hash_hex(&aacs::inf::disc_hash(&uk));
|
||||||
assert_eq!(st.disc_hash, expected);
|
assert_eq!(st.disc_hash, expected);
|
||||||
assert!(st.disc_hash.starts_with("0x"));
|
assert!(st.disc_hash.starts_with("0x"));
|
||||||
// uk_ro must be stashed verbatim for the external resolver.
|
// uk_ro must be stashed verbatim for the external resolver.
|
||||||
@@ -746,7 +749,7 @@ mod tests {
|
|||||||
// Real record stream is the single 16-byte type-0x10 record.
|
// Real record stream is the single 16-byte type-0x10 record.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.mkb.len(),
|
st.mkb.len(),
|
||||||
aacs::mkb_content_len(&mkb),
|
aacs::mkb::mkb_content_len(&mkb),
|
||||||
"MKB must be trimmed to record-stream length, not the zero-pad"
|
"MKB must be trimmed to record-stream length, not the zero-pad"
|
||||||
);
|
);
|
||||||
assert_eq!(st.mkb.len(), 16);
|
assert_eq!(st.mkb.len(), 16);
|
||||||
@@ -943,7 +946,7 @@ mod tests {
|
|||||||
// disc_hash must be computed over the DUPLICATE bytes.
|
// disc_hash must be computed over the DUPLICATE bytes.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.disc_hash,
|
st.disc_hash,
|
||||||
aacs::disc_hash_hex(&aacs::disc_hash(&uk)),
|
aacs::inf::disc_hash_hex(&aacs::inf::disc_hash(&uk)),
|
||||||
"fallback must hash the DUPLICATE Unit_Key_RO.inf"
|
"fallback must hash the DUPLICATE Unit_Key_RO.inf"
|
||||||
);
|
);
|
||||||
assert_eq!(st.uk_ro, uk);
|
assert_eq!(st.uk_ro, uk);
|
||||||
@@ -965,8 +968,8 @@ mod tests {
|
|||||||
// the route fails gracefully (AacsNoHostCert), never panics.
|
// the route fails gracefully (AacsNoHostCert), never panics.
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
fn fake_cert(tag: u8) -> aacs::HostCert {
|
fn fake_cert(tag: u8) -> aacs::types::HostCert {
|
||||||
aacs::HostCert {
|
aacs::types::HostCert {
|
||||||
private_key: [tag; 20],
|
private_key: [tag; 20],
|
||||||
certificate: vec![tag; 92],
|
certificate: vec![tag; 92],
|
||||||
private_key_v2: None,
|
private_key_v2: None,
|
||||||
@@ -975,15 +978,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal in-test KeySource that yields no keys but a fixed cert list.
|
/// A minimal in-test KeySource that yields no keys but a fixed cert list.
|
||||||
struct CertSource(Vec<aacs::HostCert>);
|
struct CertSource(Vec<aacs::types::HostCert>);
|
||||||
impl crate::KeySource for CertSource {
|
impl crate::KeySource for CertSource {
|
||||||
fn get_uk(
|
fn get_uk(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &dyn crate::keysource::ResolveCtx,
|
_ctx: &dyn crate::keysource::ResolveCtx,
|
||||||
) -> Result<Vec<crate::aacs::UnitKey>> {
|
) -> Result<Vec<crate::aacs::boil::UnitKey>> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
fn host_certs(&self, _mkb: Option<u32>) -> Vec<aacs::HostCert> {
|
fn host_certs(&self, _mkb: Option<u32>) -> Vec<aacs::types::HostCert> {
|
||||||
self.0.clone()
|
self.0.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,7 +1080,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cert_unlock_outcome_maps_to_structured_trace_step() {
|
fn cert_unlock_outcome_maps_to_structured_trace_step() {
|
||||||
use crate::aacs::UnlockOutcome;
|
use crate::aacs::trace::UnlockOutcome;
|
||||||
use freemkv_unlock::UnlockError;
|
use freemkv_unlock::UnlockError;
|
||||||
// The libfreemkv-side no-cert case carries the MKB generation.
|
// The libfreemkv-side no-cert case carries the MKB generation.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+6
-6
@@ -1083,13 +1083,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a clear 6144-byte AACS unit (TS syncs at the 192-byte BD-TS
|
/// Build a clear 6144-byte AACS unit (TS syncs at the 192-byte BD-TS
|
||||||
/// stride) then encrypt it under `unit_key` so `aacs::decrypt_unit`
|
/// stride) then encrypt it under `unit_key` so `aacs::content::decrypt_unit`
|
||||||
/// recovers it cleanly (zero decrypt loss). Mirrors the encrypt helper in
|
/// recovers it cleanly (zero decrypt loss). Mirrors the encrypt helper in
|
||||||
/// `sector/decrypting.rs` tests. `tag` distinguishes two units' payloads.
|
/// `sector/decrypting.rs` tests. `tag` distinguishes two units' payloads.
|
||||||
fn encrypt_aacs_unit(unit_key: &[u8; 16], tag: u8) -> Vec<u8> {
|
fn encrypt_aacs_unit(unit_key: &[u8; 16], tag: u8) -> Vec<u8> {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47; // TS sync
|
unit[off] = 0x47; // TS sync
|
||||||
@@ -1108,7 +1108,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::crypto::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let blocks = (crate::aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..blocks {
|
for i in 0..blocks {
|
||||||
let o = 16 + i * 16;
|
let o = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1124,7 +1124,7 @@ mod tests {
|
|||||||
|
|
||||||
/// The plaintext that `encrypt_aacs_unit(_, tag)` decrypts back to.
|
/// The plaintext that `encrypt_aacs_unit(_, tag)` decrypts back to.
|
||||||
fn clear_aacs_unit(tag: u8) -> Vec<u8> {
|
fn clear_aacs_unit(tag: u8) -> Vec<u8> {
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
@@ -1600,10 +1600,10 @@ mod tests {
|
|||||||
/// own batch starts are always unit-aligned; anchoring a later extent
|
/// own batch starts are always unit-aligned; anchoring a later extent
|
||||||
/// against the FIRST extent's base mis-aligns whenever the extents' starts
|
/// against the FIRST extent's base mis-aligns whenever the extents' starts
|
||||||
/// differ by a non-multiple of 3 sectors. This is the exact arithmetic the
|
/// differ by a non-multiple of 3 sectors. This is the exact arithmetic the
|
||||||
/// decrypt-on-read gate (`aacs::is_unit_aligned`) performs.
|
/// decrypt-on-read gate (`aacs::content::is_unit_aligned`) performs.
|
||||||
#[test]
|
#[test]
|
||||||
fn per_extent_base_is_aligned_first_extent_base_is_not() {
|
fn per_extent_base_is_aligned_first_extent_base_is_not() {
|
||||||
use crate::aacs::is_unit_aligned;
|
use crate::aacs::content::is_unit_aligned;
|
||||||
let ext_a_start = 7000u32; // first extent abs LBA
|
let ext_a_start = 7000u32; // first extent abs LBA
|
||||||
let ext_b_start = 7004u32; // second extent abs LBA (Δ4 — not mult of 3)
|
let ext_b_start = 7004u32; // second extent abs LBA (Δ4 — not mult of 3)
|
||||||
|
|
||||||
|
|||||||
+30
-30
@@ -1413,7 +1413,7 @@ impl KeyOrigin {
|
|||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct DriveCredentials {
|
pub struct DriveCredentials {
|
||||||
/// Host certificate(s) + private key(s) for the SCSI AACS handshake.
|
/// Host certificate(s) + private key(s) for the SCSI AACS handshake.
|
||||||
pub host_certs: Vec<crate::aacs::HostCert>,
|
pub host_certs: Vec<crate::aacs::types::HostCert>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for disc scanning.
|
/// Options for disc scanning.
|
||||||
@@ -1781,8 +1781,8 @@ impl Disc {
|
|||||||
Ok((inf, mkb, version))
|
Ok((inf, mkb, version))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AACS major version ([`crate::aacs::AACS_MAJOR_BD`] /
|
/// AACS major version ([`crate::aacs::mkb::AACS_MAJOR_BD`] /
|
||||||
/// [`crate::aacs::AACS_MAJOR_UHD`]) from the content certificate. Drives the
|
/// [`crate::aacs::mkb::AACS_MAJOR_UHD`]) from the content certificate. Drives the
|
||||||
/// `Unit_Key_RO.inf` parse stride (48-byte V10 vs 64-byte V20/V21), so the
|
/// `Unit_Key_RO.inf` parse stride (48-byte V10 vs 64-byte V20/V21), so the
|
||||||
/// out-of-band key-fetch path parses `enc_title_keys` at the right stride (a
|
/// out-of-band key-fetch path parses `enc_title_keys` at the right stride (a
|
||||||
/// server VUK then derives the correct unit keys).
|
/// server VUK then derives the correct unit keys).
|
||||||
@@ -1798,7 +1798,7 @@ impl Disc {
|
|||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
||||||
.ok()
|
.ok()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(crate::aacs::parse_content_cert)
|
.and_then(crate::aacs::inf::parse_content_cert)
|
||||||
{
|
{
|
||||||
Some(c) => c.version.major(),
|
Some(c) => c.version.major(),
|
||||||
None => {
|
None => {
|
||||||
@@ -1808,7 +1808,7 @@ impl Disc {
|
|||||||
"no readable AACS content certificate; defaulting to the V20/UHD \
|
"no readable AACS content certificate; defaulting to the V20/UHD \
|
||||||
Unit_Key_RO stride (a VUK-from-server path would otherwise mis-stride)"
|
Unit_Key_RO stride (a VUK-from-server path would otherwise mis-stride)"
|
||||||
);
|
);
|
||||||
crate::aacs::AACS_MAJOR_UHD
|
crate::aacs::mkb::AACS_MAJOR_UHD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1817,7 +1817,7 @@ impl Disc {
|
|||||||
///
|
///
|
||||||
/// `MKB_RO.inf` / `MKB_RW.inf` are allocated to a fixed ~128 MiB and
|
/// `MKB_RO.inf` / `MKB_RW.inf` are allocated to a fixed ~128 MiB and
|
||||||
/// zero-padded; the actual record stream is a few MiB. We read a bounded
|
/// zero-padded; the actual record stream is a few MiB. We read a bounded
|
||||||
/// prefix, find the record-stream length via [`crate::aacs::mkb_content_len`]
|
/// prefix, find the record-stream length via [`crate::aacs::mkb::mkb_content_len`]
|
||||||
/// and return exactly that, growing the prefix if the records run past it.
|
/// and return exactly that, growing the prefix if the records run past it.
|
||||||
/// This avoids reading 100+ MiB of padding on every scan AND avoids the
|
/// This avoids reading 100+ MiB of padding on every scan AND avoids the
|
||||||
/// `read_file` `MAX_FILE_BYTES` cap that (since 0.31.0) rejected the padded
|
/// `read_file` `MAX_FILE_BYTES` cap that (since 0.31.0) rejected the padded
|
||||||
@@ -1833,13 +1833,13 @@ impl Disc {
|
|||||||
.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, want)
|
.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, want)
|
||||||
.or_else(|_| udf_fs.read_file_prefix(reader, crate::aacs::PATH_MKB_RW, want))
|
.or_else(|_| udf_fs.read_file_prefix(reader, crate::aacs::PATH_MKB_RW, want))
|
||||||
.map_err(|_| Error::AacsNoKeys)?;
|
.map_err(|_| Error::AacsNoKeys)?;
|
||||||
let n = crate::aacs::mkb_content_len(&buf);
|
let n = crate::aacs::mkb::mkb_content_len(&buf);
|
||||||
// `n` strictly inside `buf` => the record walk reached the padding
|
// `n` strictly inside `buf` => the record walk reached the padding
|
||||||
// boundary (full content captured). `buf` shorter than `want` =>
|
// boundary (full content captured). `buf` shorter than `want` =>
|
||||||
// the whole file is already read. Otherwise the records may run
|
// the whole file is already read. Otherwise the records may run
|
||||||
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
||||||
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
||||||
return Ok(crate::aacs::trim_mkb(buf));
|
return Ok(crate::aacs::mkb::trim_mkb(buf));
|
||||||
}
|
}
|
||||||
want = (want * 2).min(MAX_BYTES);
|
want = (want * 2).min(MAX_BYTES);
|
||||||
}
|
}
|
||||||
@@ -2274,7 +2274,7 @@ impl Disc {
|
|||||||
/// else (UDF filesystem, BDMV nav, PLAYLIST/CLIPINF) is always clear.
|
/// else (UDF filesystem, BDMV nav, PLAYLIST/CLIPINF) is always clear.
|
||||||
///
|
///
|
||||||
/// The in-read decrypt-verify gate (`DecryptingSectorSource`) uses this so it
|
/// The in-read decrypt-verify gate (`DecryptingSectorSource`) uses this so it
|
||||||
/// never consults [`ts_sync_destroyed`](crate::aacs::ts_sync_destroyed) about
|
/// never consults [`ts_sync_destroyed`](crate::aacs::content::ts_sync_destroyed) about
|
||||||
/// non-content bytes — filesystem data has no TS sync and would otherwise be
|
/// non-content bytes — filesystem data has no TS sync and would otherwise be
|
||||||
/// mistaken for ciphertext (the first-2-GB false-positive this fixes).
|
/// mistaken for ciphertext (the first-2-GB false-positive this fixes).
|
||||||
///
|
///
|
||||||
@@ -2567,9 +2567,9 @@ impl Disc {
|
|||||||
} else if self.encrypted && self.css.is_none() {
|
} else if self.encrypted && self.css.is_none() {
|
||||||
self.aacs = Some(AacsState {
|
self.aacs = Some(AacsState {
|
||||||
version: if self.format == DiscFormat::Uhd {
|
version: if self.format == DiscFormat::Uhd {
|
||||||
crate::aacs::AACS_MAJOR_UHD
|
crate::aacs::mkb::AACS_MAJOR_UHD
|
||||||
} else {
|
} else {
|
||||||
crate::aacs::AACS_MAJOR_BD
|
crate::aacs::mkb::AACS_MAJOR_BD
|
||||||
},
|
},
|
||||||
bus_encryption: self.format == DiscFormat::Uhd,
|
bus_encryption: self.format == DiscFormat::Uhd,
|
||||||
mkb_version: None,
|
mkb_version: None,
|
||||||
@@ -2667,7 +2667,7 @@ impl Disc {
|
|||||||
Key::Processing(pks) => supplied.processing_keys = pks,
|
Key::Processing(pks) => supplied.processing_keys = pks,
|
||||||
Key::Media(mks) => supplied.media_keys = mks,
|
Key::Media(mks) => supplied.media_keys = mks,
|
||||||
Key::Volume(vuk) => {
|
Key::Volume(vuk) => {
|
||||||
supplied.disc_entry = Some(crate::aacs::DiscEntry {
|
supplied.disc_entry = Some(crate::aacs::types::DiscEntry {
|
||||||
disc_hash: aacs.disc_hash.clone(),
|
disc_hash: aacs.disc_hash.clone(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
media_key: None,
|
media_key: None,
|
||||||
@@ -2685,8 +2685,8 @@ impl Disc {
|
|||||||
let uk_ro = aacs.uk_ro.clone();
|
let uk_ro = aacs.uk_ro.clone();
|
||||||
let version_u8 = aacs.version;
|
let version_u8 = aacs.version;
|
||||||
|
|
||||||
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
let provider_refs: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied];
|
||||||
let ctx = crate::aacs::ResolveContext {
|
let ctx = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &volume_id,
|
volume_id: &volume_id,
|
||||||
@@ -2700,7 +2700,7 @@ impl Disc {
|
|||||||
// reason-preserving wrapper threads the no-key cause out so the
|
// reason-preserving wrapper threads the no-key cause out so the
|
||||||
// decrypt gate can report E7017 (had derivation material but no VID)
|
// decrypt gate can report E7017 (had derivation material but no VID)
|
||||||
// vs E7022 (no usable material) instead of a flat AacsKeyRejected.
|
// vs E7022 (no usable material) instead of a flat AacsKeyRejected.
|
||||||
let resolved = crate::aacs::resolve_keys_with_reason(&ctx, version_u8)
|
let resolved = crate::aacs::resolve::resolve_keys_with_reason(&ctx, version_u8)
|
||||||
.map_err(|_reason| crate::error::Error::AacsKeyRejected)?;
|
.map_err(|_reason| crate::error::Error::AacsKeyRejected)?;
|
||||||
|
|
||||||
if resolved.unit_keys.is_empty() {
|
if resolved.unit_keys.is_empty() {
|
||||||
@@ -3203,7 +3203,7 @@ impl Disc {
|
|||||||
// decrypts and is AACS-keyed. Region read-starts are aligned DOWN to a
|
// decrypts and is AACS-keyed. Region read-starts are aligned DOWN to a
|
||||||
// unit boundary in the loop below; a fresh sweep starts at LBA 0 (already
|
// unit boundary in the loop below; a fresh sweep starts at LBA 0 (already
|
||||||
// aligned), so alignment only bites on resume NonTried regions.
|
// aligned), so alignment only bites on resume NonTried regions.
|
||||||
const UNIT_SECTORS: u16 = (crate::aacs::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
const UNIT_SECTORS: u16 = (crate::aacs::content::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
||||||
if decrypt_is_aacs && batch % UNIT_SECTORS != 0 {
|
if decrypt_is_aacs && batch % UNIT_SECTORS != 0 {
|
||||||
batch = batch.saturating_add(UNIT_SECTORS - (batch % UNIT_SECTORS));
|
batch = batch.saturating_add(UNIT_SECTORS - (batch % UNIT_SECTORS));
|
||||||
}
|
}
|
||||||
@@ -3285,7 +3285,7 @@ impl Disc {
|
|||||||
// sweep's NonTried region starts at 0, already unit-aligned; this only
|
// sweep's NonTried region starts at 0, already unit-aligned; this only
|
||||||
// shifts resume regions that begin mid-unit.
|
// shifts resume regions that begin mid-unit.
|
||||||
let mut pos = if decrypt_is_aacs {
|
let mut pos = if decrypt_is_aacs {
|
||||||
let unit_bytes = crate::aacs::ALIGNED_UNIT_LEN as u64;
|
let unit_bytes = crate::aacs::content::ALIGNED_UNIT_LEN as u64;
|
||||||
region_pos - (region_pos % unit_bytes)
|
region_pos - (region_pos % unit_bytes)
|
||||||
} else {
|
} else {
|
||||||
region_pos
|
region_pos
|
||||||
@@ -4240,8 +4240,8 @@ mod tests {
|
|||||||
/// `sector::decrypting::tests::aacs_unaligned_start_lba_rejected`.
|
/// `sector::decrypting::tests::aacs_unaligned_start_lba_rejected`.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_sweep_batch_and_region_are_unit_aligned() {
|
fn aacs_sweep_batch_and_region_are_unit_aligned() {
|
||||||
const UNIT_SECTORS: u16 = (crate::aacs::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
const UNIT_SECTORS: u16 = (crate::aacs::content::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
||||||
let unit_bytes = crate::aacs::ALIGNED_UNIT_LEN as u64; // 6144
|
let unit_bytes = crate::aacs::content::ALIGNED_UNIT_LEN as u64; // 6144
|
||||||
|
|
||||||
// (a) Batch rounding: ecc_sectors() for UHD/BD is 32, not a multiple of 3.
|
// (a) Batch rounding: ecc_sectors() for UHD/BD is 32, not a multiple of 3.
|
||||||
// The decrypting-AACS path rounds it up to the next multiple of 3 (33).
|
// The decrypting-AACS path rounds it up to the next multiple of 3 (33).
|
||||||
@@ -4700,7 +4700,7 @@ mod tests {
|
|||||||
// The resolver classifies a device-keys-but-zero-VID context as
|
// The resolver classifies a device-keys-but-zero-VID context as
|
||||||
// `VidUnavailable`; that reason rides on `aacs_error`.
|
// `VidUnavailable`; that reason rides on `aacs_error`.
|
||||||
let supplied = crate::aacs::provider::SuppliedKey {
|
let supplied = crate::aacs::provider::SuppliedKey {
|
||||||
device_keys: vec![crate::aacs::DeviceKey {
|
device_keys: vec![crate::aacs::types::DeviceKey {
|
||||||
key: [0x11; 16],
|
key: [0x11; 16],
|
||||||
node: 1,
|
node: 1,
|
||||||
uv: 1,
|
uv: 1,
|
||||||
@@ -4710,14 +4710,14 @@ mod tests {
|
|||||||
media_keys: Vec::new(),
|
media_keys: Vec::new(),
|
||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
let provider_refs: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied];
|
||||||
// A minimal but parseable Unit_Key_RO.inf (uk_pos=32, zero unit keys)
|
// 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
|
// so resolution proceeds to the path-try logic and fails for lack of a
|
||||||
// VID — not because the .inf failed to parse.
|
// VID — not because the .inf failed to parse.
|
||||||
let mut uk_ro = vec![0u8; 40];
|
let mut uk_ro = vec![0u8; 40];
|
||||||
uk_ro[0..4].copy_from_slice(&32u32.to_be_bytes()); // uk_pos = 32
|
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.
|
// num_unit_keys = 0 (BE16) at uk_pos -> parses to an empty key file.
|
||||||
let ctx = crate::aacs::ResolveContext {
|
let ctx = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &[0u8; 16], // the "no VID" sentinel
|
volume_id: &[0u8; 16], // the "no VID" sentinel
|
||||||
@@ -4725,8 +4725,8 @@ mod tests {
|
|||||||
mkb: None,
|
mkb: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crate::aacs::resolve_keys_with_reason(&ctx, 2).err(),
|
crate::aacs::resolve::resolve_keys_with_reason(&ctx, 2).err(),
|
||||||
Some(crate::aacs::ResolveFailure::VidUnavailable),
|
Some(crate::aacs::resolve::ResolveFailure::VidUnavailable),
|
||||||
"device keys + zero VID must classify as VidUnavailable"
|
"device keys + zero VID must classify as VidUnavailable"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4750,8 +4750,8 @@ mod tests {
|
|||||||
media_keys: Vec::new(),
|
media_keys: Vec::new(),
|
||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
let provider_refs_none: [&dyn crate::aacs::KeyProvider; 1] = [&supplied_none];
|
let provider_refs_none: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied_none];
|
||||||
let ctx_none = crate::aacs::ResolveContext {
|
let ctx_none = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &[0u8; 16],
|
volume_id: &[0u8; 16],
|
||||||
@@ -4759,8 +4759,8 @@ mod tests {
|
|||||||
mkb: None,
|
mkb: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crate::aacs::resolve_keys_with_reason(&ctx_none, 2).err(),
|
crate::aacs::resolve::resolve_keys_with_reason(&ctx_none, 2).err(),
|
||||||
Some(crate::aacs::ResolveFailure::NoMaterial),
|
Some(crate::aacs::resolve::ResolveFailure::NoMaterial),
|
||||||
"no key material must classify as NoMaterial"
|
"no key material must classify as NoMaterial"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5030,8 +5030,8 @@ mod tests {
|
|||||||
let vuk = [0x5au8; 16];
|
let vuk = [0x5au8; 16];
|
||||||
let enc0 = [0x12u8; 16];
|
let enc0 = [0x12u8; 16];
|
||||||
let enc1 = [0x34u8; 16];
|
let enc1 = [0x34u8; 16];
|
||||||
let exp0 = crate::aacs::decrypt_unit_key(&vuk, &enc0);
|
let exp0 = crate::aacs::derive::decrypt_unit_key(&vuk, &enc0);
|
||||||
let exp1 = crate::aacs::decrypt_unit_key(&vuk, &enc1);
|
let exp1 = crate::aacs::derive::decrypt_unit_key(&vuk, &enc1);
|
||||||
|
|
||||||
let mut disc = make_test_disc(1000, "UHD");
|
let mut disc = make_test_disc(1000, "UHD");
|
||||||
disc.encrypted = true;
|
disc.encrypted = true;
|
||||||
|
|||||||
+10
-9
@@ -7,7 +7,7 @@
|
|||||||
//! grid (clips can start off the 6144 grid and fragment across UDF extents). So
|
//! grid (clips can start off the 6144 grid and fragment across UDF extents). So
|
||||||
//! this gate BUFFERS the disc-absolute read stream and re-ALIGNS it into
|
//! this gate BUFFERS the disc-absolute read stream and re-ALIGNS it into
|
||||||
//! clip-file units, then applies the standards-correct
|
//! clip-file units, then applies the standards-correct
|
||||||
//! [`crate::aacs::unit_is_clean_ts`] gate (libaacs `_verify_ts`, all-32 syncs).
|
//! [`crate::aacs::content::unit_is_clean_ts`] gate (libaacs `_verify_ts`, all-32 syncs).
|
||||||
//!
|
//!
|
||||||
//! FAIL-SAFE CONTRACT (this sits in the middle of every read, so it must never
|
//! FAIL-SAFE CONTRACT (this sits in the middle of every read, so it must never
|
||||||
//! break a good read): the gate can ONLY downgrade a unit it is *confident* is
|
//! break a good read): the gate can ONLY downgrade a unit it is *confident* is
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use crate::aacs::{self, ALIGNED_UNIT_LEN};
|
use crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
|
use crate::aacs::{self};
|
||||||
use crate::consts::SECTOR_BYTES_U64;
|
use crate::consts::SECTOR_BYTES_U64;
|
||||||
use crate::decrypt::DecryptKeys;
|
use crate::decrypt::DecryptKeys;
|
||||||
use crate::sector::KeyFetch;
|
use crate::sector::KeyFetch;
|
||||||
@@ -57,7 +58,7 @@ pub enum ContainerKind {
|
|||||||
Ts,
|
Ts,
|
||||||
/// HD-DVD `.evo` — MPEG-2 program stream (pack-start `00 00 01 BA`).
|
/// HD-DVD `.evo` — MPEG-2 program stream (pack-start `00 00 01 BA`).
|
||||||
/// NOT yet enabled by enumeration; present so adding HD-DVD is a one-mapping
|
/// NOT yet enabled by enumeration; present so adding HD-DVD is a one-mapping
|
||||||
/// change. See [`crate::aacs::unit_is_clean_ps`] for the (unvalidated) check.
|
/// change. See [`crate::aacs::content::unit_is_clean_ps`] for the (unvalidated) check.
|
||||||
Ps,
|
Ps,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +188,8 @@ impl UnitVerifier {
|
|||||||
/// The post-decrypt structural check for a clip's container.
|
/// The post-decrypt structural check for a clip's container.
|
||||||
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
|
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
|
||||||
match self.containers[clip as usize] {
|
match self.containers[clip as usize] {
|
||||||
ContainerKind::Ts => aacs::unit_is_clean_ts,
|
ContainerKind::Ts => aacs::content::unit_is_clean_ts,
|
||||||
ContainerKind::Ps => aacs::unit_is_clean_ps,
|
ContainerKind::Ps => aacs::content::unit_is_clean_ps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +302,8 @@ impl UnitVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Can this fully-assembled unit be decrypted + verified? `accept` is the
|
/// Can this fully-assembled unit be decrypted + verified? `accept` is the
|
||||||
/// container's strict structural check ([`aacs::unit_is_clean_ts`] for TS,
|
/// container's strict structural check ([`aacs::content::unit_is_clean_ts`] for TS,
|
||||||
/// [`aacs::unit_is_clean_ps`] for PS) — the only format-specific part; the
|
/// [`aacs::content::unit_is_clean_ps`] for PS) — the only format-specific part; the
|
||||||
/// AACS crypto is container-agnostic. Returns the 3-state [`Decryptability`].
|
/// AACS crypto is container-agnostic. Returns the 3-state [`Decryptability`].
|
||||||
fn decryptability(
|
fn decryptability(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -318,7 +319,7 @@ impl UnitVerifier {
|
|||||||
// ENCRYPTED units that no key opens are ever flagged. (A real bad READ of
|
// ENCRYPTED units that no key opens are ever flagged. (A real bad READ of
|
||||||
// clear content is still caught by the normal SCSI read-error path; this
|
// clear content is still caught by the normal SCSI read-error path; this
|
||||||
// gate just won't false-flag it.)
|
// gate just won't false-flag it.)
|
||||||
if !aacs::aacs_unit_encrypted(raw) {
|
if !aacs::content::aacs_unit_encrypted(raw) {
|
||||||
return if accept(raw) {
|
return if accept(raw) {
|
||||||
Decryptability::Decryptable
|
Decryptability::Decryptable
|
||||||
} else {
|
} else {
|
||||||
@@ -363,7 +364,7 @@ impl UnitVerifier {
|
|||||||
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
|
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
|
||||||
for k in &self.keys {
|
for k in &self.keys {
|
||||||
let mut scratch = *raw;
|
let mut scratch = *raw;
|
||||||
if aacs::decrypt_unit_checked(&mut scratch, k, accept) {
|
if aacs::content::decrypt_unit_checked(&mut scratch, k, accept) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-23
@@ -3,12 +3,12 @@
|
|||||||
//! libfreemkv performs NO key lookup. An application resolves a disc's keys
|
//! libfreemkv performs NO key lookup. An application resolves a disc's keys
|
||||||
//! through one or more [`KeySource`]s, each an adapter over a backing store (a
|
//! through one or more [`KeySource`]s, each an adapter over a backing store (a
|
||||||
//! keydb file, a key server, the mapfile cache). A source's job is to return the
|
//! keydb file, a key server, the mapfile cache). A source's job is to return the
|
||||||
//! disc's terminal **Unit Keys** ([`crate::aacs::UnitKey`]). It knows what
|
//! disc's terminal **Unit Keys** ([`crate::aacs::boil::UnitKey`]). It knows what
|
||||||
//! material it holds (a DK / MK / VUK / pre-decrypted UK) and what it must fetch
|
//! material it holds (a DK / MK / VUK / pre-decrypted UK) and what it must fetch
|
||||||
//! from the disc (VID, MKB, encrypted title keys, content samples) to get there;
|
//! from the disc (VID, MKB, encrypted title keys, content samples) to get there;
|
||||||
//! it orchestrates the derivation by calling libfreemkv's own boil-down crypto
|
//! it orchestrates the derivation by calling libfreemkv's own boil-down crypto
|
||||||
//! primitives ([`crate::aacs::mk_from_dk`] / [`crate::aacs::vuk_from_mk`] /
|
//! primitives ([`crate::aacs::boil::mk_from_dk`] / [`crate::aacs::boil::vuk_from_mk`] /
|
||||||
//! [`crate::aacs::uk_from_vuk`]) through the [`ResolveCtx`] handed to it.
|
//! [`crate::aacs::boil::uk_from_vuk`]) through the [`ResolveCtx`] handed to it.
|
||||||
//!
|
//!
|
||||||
//! libfreemkv still OWNS the crypto: the boil-down primitives and the AES live
|
//! libfreemkv still OWNS the crypto: the boil-down primitives and the AES live
|
||||||
//! here. A source owns only PATH ORCHESTRATION — deciding which primitive to
|
//! here. A source owns only PATH ORCHESTRATION — deciding which primitive to
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
//! keeping key *policy* (which store, which order, online vs local) out of the
|
//! keeping key *policy* (which store, which order, online vs local) out of the
|
||||||
//! library.
|
//! library.
|
||||||
|
|
||||||
use crate::aacs::{HostCert, UnitKey, Vid};
|
use crate::aacs::boil::{UnitKey, Vid};
|
||||||
|
use crate::aacs::types::HostCert;
|
||||||
use crate::disc::Key;
|
use crate::disc::Key;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
@@ -74,8 +75,8 @@ pub trait ResolveCtx {
|
|||||||
/// Raw MKB bytes (may be empty when not captured).
|
/// Raw MKB bytes (may be empty when not captured).
|
||||||
fn mkb(&self) -> Result<&[u8], Error>;
|
fn mkb(&self) -> Result<&[u8], Error>;
|
||||||
/// The disc's encrypted title keys, parsed from `Unit_Key_RO.inf` the same
|
/// The disc's encrypted title keys, parsed from `Unit_Key_RO.inf` the same
|
||||||
/// way the library's resolver parses them ([`crate::aacs::parse_unit_key_ro`]),
|
/// way the library's resolver parses them ([`crate::aacs::inf::parse_unit_key_ro`]),
|
||||||
/// in on-disc order. Feed straight into [`crate::aacs::uk_from_vuk`].
|
/// in on-disc order. Feed straight into [`crate::aacs::boil::uk_from_vuk`].
|
||||||
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error>;
|
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error>;
|
||||||
/// Up to `n` encrypted on-disc content sample units, for a source that
|
/// Up to `n` encrypted on-disc content sample units, for a source that
|
||||||
/// validates a candidate server-side against real ciphertext.
|
/// validates a candidate server-side against real ciphertext.
|
||||||
@@ -113,7 +114,8 @@ impl<'a> DiscInputsCtx<'a> {
|
|||||||
/// title keys — the parse failure is swallowed here, not surfaced as an
|
/// title keys — the parse failure is swallowed here, not surfaced as an
|
||||||
/// error.
|
/// error.
|
||||||
pub fn new(inputs: &'a DiscInputs) -> Self {
|
pub fn new(inputs: &'a DiscInputs) -> Self {
|
||||||
use crate::aacs::{AacsVersion, parse_unit_key_ro};
|
use crate::aacs::inf::parse_unit_key_ro;
|
||||||
|
use crate::aacs::mkb::AacsVersion;
|
||||||
let enc_keys = if inputs.unit_key_ro.is_empty() {
|
let enc_keys = if inputs.unit_key_ro.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -164,8 +166,8 @@ impl ResolveCtx for DiscInputsCtx<'_> {
|
|||||||
/// holds, orchestrates the derivation down to Unit Keys using the library's
|
/// holds, orchestrates the derivation down to Unit Keys using the library's
|
||||||
/// boil-down crypto primitives — never re-implementing AES. A source that holds
|
/// boil-down crypto primitives — never re-implementing AES. A source that holds
|
||||||
/// pre-decrypted Unit Keys returns them directly; one that holds a VUK calls
|
/// pre-decrypted Unit Keys returns them directly; one that holds a VUK calls
|
||||||
/// [`crate::aacs::uk_from_vuk`]; one that holds device keys calls
|
/// [`crate::aacs::boil::uk_from_vuk`]; one that holds device keys calls
|
||||||
/// [`crate::aacs::mk_from_dk`] → [`crate::aacs::vuk_from_mk`] → `uk_from_vuk`.
|
/// [`crate::aacs::boil::mk_from_dk`] → [`crate::aacs::boil::vuk_from_mk`] → `uk_from_vuk`.
|
||||||
///
|
///
|
||||||
/// Returning an empty `Vec` means "no key for this disc from this source"; an
|
/// Returning an empty `Vec` means "no key for this disc from this source"; an
|
||||||
/// `Err` means the source itself failed (I/O, parse, network). The caller
|
/// `Err` means the source itself failed (I/O, parse, network). The caller
|
||||||
@@ -209,7 +211,7 @@ pub fn resolve_and_apply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`resolve_and_apply`] but also returns a structured
|
/// Like [`resolve_and_apply`] but also returns a structured
|
||||||
/// [`crate::aacs::ResolutionTrace`] recording, per source, what happened — for
|
/// [`crate::aacs::trace::ResolutionTrace`] recording, per source, what happened — for
|
||||||
/// applications to render. ZERO English; the trace is typed enums only.
|
/// applications to render. ZERO English; the trace is typed enums only.
|
||||||
///
|
///
|
||||||
/// One-shot per source: each source's [`KeySource::get_uk`] is called exactly
|
/// One-shot per source: each source's [`KeySource::get_uk`] is called exactly
|
||||||
@@ -219,8 +221,8 @@ pub fn resolve_and_apply(
|
|||||||
/// success — so a wrong/partial key set is rejected and the loop continues.
|
/// success — so a wrong/partial key set is rejected and the loop continues.
|
||||||
///
|
///
|
||||||
/// CPS-unit numbering: a source returns Unit Keys carrying the POSITIONAL index
|
/// CPS-unit numbering: a source returns Unit Keys carrying the POSITIONAL index
|
||||||
/// from [`crate::aacs::uk_from_vuk`]; the library's canonical CPS-unit number is
|
/// from [`crate::aacs::boil::uk_from_vuk`]; the library's canonical CPS-unit number is
|
||||||
/// `position + 1` (matching [`crate::aacs::parse_unit_key_ro`]'s `(i + 1)`), so
|
/// `position + 1` (matching [`crate::aacs::inf::parse_unit_key_ro`]'s `(i + 1)`), so
|
||||||
/// the committed `AacsState.unit_keys` is byte-identical to the library-resolved
|
/// the committed `AacsState.unit_keys` is byte-identical to the library-resolved
|
||||||
/// path. The number is cosmetic for descramble (the decrypt path strips it and
|
/// path. The number is cosmetic for descramble (the decrypt path strips it and
|
||||||
/// tries every key) but is kept faithful to the resolver's convention.
|
/// tries every key) but is kept faithful to the resolver's convention.
|
||||||
@@ -228,10 +230,10 @@ pub fn resolve_and_apply_traced(
|
|||||||
sources: &[Box<dyn KeySource>],
|
sources: &[Box<dyn KeySource>],
|
||||||
inputs: &DiscInputs,
|
inputs: &DiscInputs,
|
||||||
disc: &mut crate::Disc,
|
disc: &mut crate::Disc,
|
||||||
) -> (bool, crate::aacs::ResolutionTrace) {
|
) -> (bool, crate::aacs::trace::ResolutionTrace) {
|
||||||
use crate::aacs::trace::{KeyNode, KeyOutcome, KeyStep};
|
use crate::aacs::trace::{KeyNode, KeyOutcome, KeyStep};
|
||||||
|
|
||||||
let mut trace = crate::aacs::ResolutionTrace::new();
|
let mut trace = crate::aacs::trace::ResolutionTrace::new();
|
||||||
|
|
||||||
// The ctx parses Unit_Key_RO.inf at the stride for `inputs.version` (the
|
// The ctx parses Unit_Key_RO.inf at the stride for `inputs.version` (the
|
||||||
// disc's own AACS major), so the stride is the disc's single source of truth.
|
// disc's own AACS major), so the stride is the disc's single source of truth.
|
||||||
@@ -341,7 +343,7 @@ pub fn key_fetch(
|
|||||||
/// `start_lba`), which the library owns. A key source is *handed* these bytes
|
/// `start_lba`), which the library owns. A key source is *handed* these bytes
|
||||||
/// via `DiscInputs.samples`; it never reads the disc itself.
|
/// via `DiscInputs.samples`; it never reads the disc itself.
|
||||||
///
|
///
|
||||||
/// "Encrypted" is decided by [`crate::aacs::ts_sync_destroyed`] — the SAME
|
/// "Encrypted" is decided by [`crate::aacs::content::ts_sync_destroyed`] — the SAME
|
||||||
/// predicate the decrypt gate uses — so all sides agree. A clip opens with clear
|
/// predicate the decrypt gate uses — so all sides agree. A clip opens with clear
|
||||||
/// navigation units (PAT/PMT, menus); only the feature body is scrambled, and a
|
/// navigation units (PAT/PMT, menus); only the feature body is scrambled, and a
|
||||||
/// clear unit proves nothing, so this collects only scrambled ones — probing
|
/// clear unit proves nothing, so this collects only scrambled ones — probing
|
||||||
@@ -352,7 +354,7 @@ pub fn read_encrypted_units(
|
|||||||
title: &crate::disc::DiscTitle,
|
title: &crate::disc::DiscTitle,
|
||||||
n: usize,
|
n: usize,
|
||||||
) -> Vec<Vec<u8>> {
|
) -> Vec<Vec<u8>> {
|
||||||
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
||||||
const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap
|
const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap
|
||||||
// Probe several evenly-spaced points across EACH extent rather than only the
|
// Probe several evenly-spaced points across EACH extent rather than only the
|
||||||
// midpoint-and-forward: a title whose encrypted feature starts late, or whose
|
// midpoint-and-forward: a title whose encrypted feature starts late, or whose
|
||||||
@@ -413,7 +415,7 @@ pub fn read_encrypted_units(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::aacs::UnitKey;
|
use crate::aacs::boil::UnitKey;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
// ── KeySource default-method behaviour ────────────────────────────────────
|
// ── KeySource default-method behaviour ────────────────────────────────────
|
||||||
@@ -454,7 +456,7 @@ mod tests {
|
|||||||
let inputs = DiscInputs {
|
let inputs = DiscInputs {
|
||||||
disc_hash: "0xABC".into(),
|
disc_hash: "0xABC".into(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_BD,
|
version: crate::aacs::mkb::AACS_MAJOR_BD,
|
||||||
mkb: vec![1, 2, 3],
|
mkb: vec![1, 2, 3],
|
||||||
unit_key_ro: uk_ro,
|
unit_key_ro: uk_ro,
|
||||||
samples: vec![vec![9u8; 4], vec![8u8; 4], vec![7u8; 4]],
|
samples: vec![vec![9u8; 4], vec![8u8; 4], vec![7u8; 4]],
|
||||||
@@ -510,7 +512,7 @@ mod tests {
|
|||||||
let inputs = DiscInputs {
|
let inputs = DiscInputs {
|
||||||
disc_hash: "0x00".into(),
|
disc_hash: "0x00".into(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_UHD,
|
version: crate::aacs::mkb::AACS_MAJOR_UHD,
|
||||||
mkb: Vec::new(),
|
mkb: Vec::new(),
|
||||||
unit_key_ro: Vec::new(),
|
unit_key_ro: Vec::new(),
|
||||||
samples: Vec::new(),
|
samples: Vec::new(),
|
||||||
@@ -531,7 +533,7 @@ mod tests {
|
|||||||
DiscInputs {
|
DiscInputs {
|
||||||
disc_hash: String::new(),
|
disc_hash: String::new(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_UHD,
|
version: crate::aacs::mkb::AACS_MAJOR_UHD,
|
||||||
mkb: Vec::new(),
|
mkb: Vec::new(),
|
||||||
unit_key_ro: Vec::new(),
|
unit_key_ro: Vec::new(),
|
||||||
samples: Vec::new(),
|
samples: Vec::new(),
|
||||||
@@ -621,7 +623,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cb = key_fetch(empty_inputs(), make);
|
let cb = key_fetch(empty_inputs(), make);
|
||||||
let samples = vec![vec![0xEEu8; crate::aacs::ALIGNED_UNIT_LEN]];
|
let samples = vec![vec![0xEEu8; crate::aacs::content::ALIGNED_UNIT_LEN]];
|
||||||
let got = cb(&samples);
|
let got = cb(&samples);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got,
|
got,
|
||||||
@@ -643,7 +645,7 @@ mod tests {
|
|||||||
/// finds the early scrambled band.
|
/// finds the early scrambled band.
|
||||||
#[test]
|
#[test]
|
||||||
fn read_encrypted_units_finds_scrambled_content_off_the_midpoint() {
|
fn read_encrypted_units_finds_scrambled_content_off_the_midpoint() {
|
||||||
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
|
|
||||||
@@ -725,7 +727,7 @@ mod tests {
|
|||||||
/// prior single-key fixtures passed regardless of stride.
|
/// prior single-key fixtures passed regardless of stride.
|
||||||
#[test]
|
#[test]
|
||||||
fn disc_inputs_ctx_parses_unit_keys_at_the_version_stride() {
|
fn disc_inputs_ctx_parses_unit_keys_at_the_version_stride() {
|
||||||
use crate::aacs::{AACS_MAJOR_BD, AACS_MAJOR_UHD};
|
use crate::aacs::mkb::{AACS_MAJOR_BD, AACS_MAJOR_UHD};
|
||||||
const UK_POS: usize = 64;
|
const UK_POS: usize = 64;
|
||||||
let mut inf = vec![0u8; 200];
|
let mut inf = vec![0u8; 200];
|
||||||
inf[0..4].copy_from_slice(&(UK_POS as u32).to_be_bytes()); // uk_pos
|
inf[0..4].copy_from_slice(&(UK_POS as u32).to_be_bytes()); // uk_pos
|
||||||
|
|||||||
+1
-1
@@ -366,7 +366,7 @@ impl TsDemuxer {
|
|||||||
|
|
||||||
// P3/B1 CONCEALMENT MARKER. The decrypt layer fills an undecryptable
|
// P3/B1 CONCEALMENT MARKER. The decrypt layer fills an undecryptable
|
||||||
// aligned unit with NULL-TS packets (PID 0x1FFF) that carry an
|
// aligned unit with NULL-TS packets (PID 0x1FFF) that carry an
|
||||||
// adaptation-field discontinuity_indicator (see `aacs::fill_null_ts_unit`).
|
// adaptation-field discontinuity_indicator (see `aacs::content::fill_null_ts_unit`).
|
||||||
// This is the authoritative loss signal — unlike a tracked PID's 4-bit
|
// This is the authoritative loss signal — unlike a tracked PID's 4-bit
|
||||||
// continuity_counter it is CC-INDEPENDENT, so it survives a loss that is
|
// continuity_counter it is CC-INDEPENDENT, so it survives a loss that is
|
||||||
// an exact multiple of 16 packets and a loss at the very start of a PID
|
// an exact multiple of 16 packets and a loss at the very start of a PID
|
||||||
|
|||||||
+44
-39
@@ -146,7 +146,7 @@ pub struct DecryptingSectorSource<S: SectorSource> {
|
|||||||
scratch: Vec<u8>,
|
scratch: Vec<u8>,
|
||||||
/// MUX loss-concealment switch (P3 / Edit-2). When `true`, a content unit
|
/// MUX loss-concealment switch (P3 / Edit-2). When `true`, a content unit
|
||||||
/// that genuinely won't decrypt is NOT a read failure: it is overwritten with
|
/// that genuinely won't decrypt is NOT a read failure: it is overwritten with
|
||||||
/// valid NULL TS packets ([`crate::aacs::fill_null_ts_unit`]), tallied into
|
/// valid NULL TS packets ([`crate::aacs::content::fill_null_ts_unit`]), tallied into
|
||||||
/// [`decrypt_dropped`](Self::decrypt_dropped), logged loud with its LBA, and
|
/// [`decrypt_dropped`](Self::decrypt_dropped), logged loud with its LBA, and
|
||||||
/// the read returns `Ok` so the mux KEEPS GOING (it can never abort over an
|
/// the read returns `Ok` so the mux KEEPS GOING (it can never abort over an
|
||||||
/// undecryptable unit). This is the spec's "decrypt-verify is a RIP gate, not
|
/// undecryptable unit). This is the spec's "decrypt-verify is a RIP gate, not
|
||||||
@@ -197,7 +197,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
/// (sorted/merged `(start_lba, sector_count)` — see
|
/// (sorted/merged `(start_lba, sector_count)` — see
|
||||||
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
||||||
/// Units outside content (UDF filesystem / BDMV nav) pass through untouched,
|
/// Units outside content (UDF filesystem / BDMV nav) pass through untouched,
|
||||||
/// so [`ts_sync_destroyed`](crate::aacs::ts_sync_destroyed) is never consulted
|
/// so [`ts_sync_destroyed`](crate::aacs::content::ts_sync_destroyed) is never consulted
|
||||||
/// about non-content bytes. Whole-disc readers (sweep / patch) set this; the
|
/// about non-content bytes. Whole-disc readers (sweep / patch) set this; the
|
||||||
/// mux leaves it unset because it only ever reads title extents.
|
/// mux leaves it unset because it only ever reads title extents.
|
||||||
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
|
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
|
||||||
@@ -301,14 +301,14 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
content: Option<&[(u32, u32)]>,
|
content: Option<&[(u32, u32)]>,
|
||||||
prev_dropped: usize,
|
prev_dropped: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
// Gather up to MAX_FETCH_SAMPLES still-scrambled aligned units — the
|
// Gather up to MAX_FETCH_SAMPLES still-scrambled aligned units — the
|
||||||
// exact on-disc ciphertext no held key could open. A trailing partial
|
// exact on-disc ciphertext no held key could open. A trailing partial
|
||||||
// unit (chunks_exact remainder) can't be a whole scrambled unit, so
|
// unit (chunks_exact remainder) can't be a whole scrambled unit, so
|
||||||
// skipping it is correct.
|
// skipping it is correct.
|
||||||
let mut samples: Vec<Vec<u8>> = Vec::new();
|
let mut samples: Vec<Vec<u8>> = Vec::new();
|
||||||
for chunk in buf.chunks_exact(unit_len) {
|
for chunk in buf.chunks_exact(unit_len) {
|
||||||
if crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if crate::aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
samples.push(chunk.to_vec());
|
samples.push(chunk.to_vec());
|
||||||
if samples.len() >= MAX_FETCH_SAMPLES {
|
if samples.len() >= MAX_FETCH_SAMPLES {
|
||||||
break;
|
break;
|
||||||
@@ -391,7 +391,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
content: Option<&[(u32, u32)]>,
|
content: Option<&[(u32, u32)]>,
|
||||||
keys: &DecryptKeys,
|
keys: &DecryptKeys,
|
||||||
) {
|
) {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let unit_sectors = (unit_len / 2048) as u32;
|
let unit_sectors = (unit_len / 2048) as u32;
|
||||||
// Only AACS produces decrypt-verify failures; None / CSS never reach here
|
// Only AACS produces decrypt-verify failures; None / CSS never reach here
|
||||||
// with a non-zero dropped count.
|
// with a non-zero dropped count.
|
||||||
@@ -415,12 +415,12 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
// A unit that decrypted is no longer sync-destroyed; a CPI-clear or
|
// A unit that decrypted is no longer sync-destroyed; a CPI-clear or
|
||||||
// non-content unit is gated out. Only undecryptable in-content units
|
// non-content unit is gated out. Only undecryptable in-content units
|
||||||
// that are flagged encrypted carry signal.
|
// that are flagged encrypted carry signal.
|
||||||
if !in_content || !crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if !in_content || !crate::aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let all_zero = chunk.iter().all(|&b| b == 0);
|
let all_zero = chunk.iter().all(|&b| b == 0);
|
||||||
let ts_sync = crate::aacs::ts_sync_count(chunk);
|
let ts_sync = crate::aacs::content::ts_sync_count(chunk);
|
||||||
let ts_total = crate::aacs::ts_packet_total(chunk);
|
let ts_total = crate::aacs::content::ts_packet_total(chunk);
|
||||||
let mut seen = [false; 256];
|
let mut seen = [false; 256];
|
||||||
for &b in chunk {
|
for &b in chunk {
|
||||||
seen[b as usize] = true;
|
seen[b as usize] = true;
|
||||||
@@ -431,10 +431,10 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
for (_, k) in unit_keys.iter() {
|
for (_, k) in unit_keys.iter() {
|
||||||
let mut attempt = chunk.to_vec();
|
let mut attempt = chunk.to_vec();
|
||||||
if let Some(ref rdk_key) = rdk {
|
if let Some(ref rdk_key) = rdk {
|
||||||
crate::aacs::decrypt_bus(&mut attempt, rdk_key);
|
crate::aacs::content::decrypt_bus(&mut attempt, rdk_key);
|
||||||
}
|
}
|
||||||
crate::aacs::decrypt_unit(&mut attempt, k);
|
crate::aacs::content::decrypt_unit(&mut attempt, k);
|
||||||
let s = crate::aacs::ts_sync_count(&attempt);
|
let s = crate::aacs::content::ts_sync_count(&attempt);
|
||||||
if s > best_sync {
|
if s > best_sync {
|
||||||
best_sync = s;
|
best_sync = s;
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// units (else its readable units are wrongly rejected → "Decryption
|
// units (else its readable units are wrongly rejected → "Decryption
|
||||||
// failed" on exactly those titles).
|
// failed" on exactly those titles).
|
||||||
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
||||||
&& !crate::aacs::is_unit_aligned(lba, self.unit_base)
|
&& !crate::aacs::content::is_unit_aligned(lba, self.unit_base)
|
||||||
{
|
{
|
||||||
return Err(crate::error::Error::DecryptFailed);
|
return Err(crate::error::Error::DecryptFailed);
|
||||||
}
|
}
|
||||||
@@ -585,7 +585,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// rip stays fail-loud. Ciphertext is never passed downstream: it is
|
// rip stays fail-loud. Ciphertext is never passed downstream: it is
|
||||||
// replaced by null packets, not emitted.
|
// replaced by null packets, not emitted.
|
||||||
if self.tolerate_decrypt_loss && !self.verify_only {
|
if self.tolerate_decrypt_loss && !self.verify_only {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let mut concealed = 0usize;
|
let mut concealed = 0usize;
|
||||||
let mut first_lba = lba;
|
let mut first_lba = lba;
|
||||||
for (i, chunk) in buf[..n].chunks_mut(unit_len).enumerate() {
|
for (i, chunk) in buf[..n].chunks_mut(unit_len).enumerate() {
|
||||||
@@ -604,11 +604,12 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// un-restored ciphertext. In-content gating already happened in
|
// un-restored ciphertext. In-content gating already happened in
|
||||||
// `decrypt_buf`, which restored only failed units to ciphertext;
|
// `decrypt_buf`, which restored only failed units to ciphertext;
|
||||||
// clear nav and decrypted tails pass through clean.
|
// clear nav and decrypted tails pass through clean.
|
||||||
if crate::aacs::aacs_unit_still_ciphertext(chunk) {
|
if crate::aacs::content::aacs_unit_still_ciphertext(chunk) {
|
||||||
if concealed == 0 {
|
if concealed == 0 {
|
||||||
first_lba = lba + (i as u32) * crate::aacs::ALIGNED_UNIT_SECTORS;
|
first_lba =
|
||||||
|
lba + (i as u32) * crate::aacs::content::ALIGNED_UNIT_SECTORS;
|
||||||
}
|
}
|
||||||
crate::aacs::fill_null_ts_unit(chunk);
|
crate::aacs::content::fill_null_ts_unit(chunk);
|
||||||
concealed += 1;
|
concealed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,8 +632,10 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// flags, loudly. Cryptographically unreachable in practice.
|
// flags, loudly. Cryptographically unreachable in practice.
|
||||||
let mut forced = 0usize;
|
let mut forced = 0usize;
|
||||||
for chunk in buf[..n].chunks_mut(unit_len) {
|
for chunk in buf[..n].chunks_mut(unit_len) {
|
||||||
if chunk.len() == unit_len && crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if chunk.len() == unit_len
|
||||||
crate::aacs::fill_null_ts_unit(chunk);
|
&& crate::aacs::content::aacs_unit_needs_decrypt(chunk)
|
||||||
|
{
|
||||||
|
crate::aacs::content::fill_null_ts_unit(chunk);
|
||||||
forced += 1;
|
forced += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1253,12 +1256,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
||||||
/// encrypt it under `unit_key` so `aacs::decrypt_unit` recovers it. Mirrors
|
/// encrypt it under `unit_key` so `aacs::content::decrypt_unit` recovers it. Mirrors
|
||||||
/// the encrypt helper in `crate::decrypt`'s tests.
|
/// the encrypt helper in `crate::decrypt`'s tests.
|
||||||
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
@@ -1274,7 +1277,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::crypto::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let blocks = (crate::aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..blocks {
|
for i in 0..blocks {
|
||||||
let o = 16 + i * 16;
|
let o = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1347,7 +1350,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"the undecryptable unit is tallied as loss before the read errors"
|
"the undecryptable unit is tallied as loss before the read errors"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1358,7 +1361,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
2 * crate::aacs::ALIGNED_UNIT_LEN as u64,
|
2 * crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"loss must accumulate across reads"
|
"loss must accumulate across reads"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1392,7 +1395,7 @@ mod tests {
|
|||||||
// One unit encrypted under real_key, plus one trailing CLEAR (TS-sync)
|
// One unit encrypted under real_key, plus one trailing CLEAR (TS-sync)
|
||||||
// unit so we can confirm conceal touches ONLY the undecryptable unit.
|
// unit so we can confirm conceal touches ONLY the undecryptable unit.
|
||||||
let enc = encrypt_aacs_unit(&real_key);
|
let enc = encrypt_aacs_unit(&real_key);
|
||||||
let mut clear = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut clear = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut o = 4;
|
let mut o = 4;
|
||||||
while o < clear.len() {
|
while o < clear.len() {
|
||||||
clear[o] = 0x47;
|
clear[o] = 0x47;
|
||||||
@@ -1441,13 +1444,13 @@ mod tests {
|
|||||||
// The undecryptable unit is tallied as loss.
|
// The undecryptable unit is tallied as loss.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"the concealed unit is still counted as loss"
|
"the concealed unit is still counted as loss"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit 0 is now valid NULL TS packets — sync 0x47 at every 192-byte
|
// Unit 0 is now valid NULL TS packets — sync 0x47 at every 192-byte
|
||||||
// stride (offset 4), PID 0x1FFF — and carries no ciphertext.
|
// stride (offset 4), PID 0x1FFF — and carries no ciphertext.
|
||||||
let unit0 = &buf[..crate::aacs::ALIGNED_UNIT_LEN];
|
let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
while off + 192 <= unit0.len() {
|
while off + 192 <= unit0.len() {
|
||||||
assert_eq!(unit0[off + 4], 0x47, "null packet sync at {off}");
|
assert_eq!(unit0[off + 4], 0x47, "null packet sync at {off}");
|
||||||
@@ -1456,12 +1459,13 @@ mod tests {
|
|||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
!crate::aacs::ts_sync_destroyed(unit0),
|
!crate::aacs::content::ts_sync_destroyed(unit0),
|
||||||
"concealed unit reads as well-formed TS, not scrambled"
|
"concealed unit reads as well-formed TS, not scrambled"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit 1 (clear) passed through untouched.
|
// Unit 1 (clear) passed through untouched.
|
||||||
let unit1 = &buf[crate::aacs::ALIGNED_UNIT_LEN..2 * crate::aacs::ALIGNED_UNIT_LEN];
|
let unit1 = &buf
|
||||||
|
[crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read");
|
assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1496,7 +1500,7 @@ mod tests {
|
|||||||
// The byte-exact expected post-decrypt form of unit B (independent decrypt).
|
// The byte-exact expected post-decrypt form of unit B (independent decrypt).
|
||||||
let mut expected_tail = good_tail.clone();
|
let mut expected_tail = good_tail.clone();
|
||||||
assert!(
|
assert!(
|
||||||
crate::aacs::decrypt_unit(&mut expected_tail, &good_key),
|
crate::aacs::content::decrypt_unit(&mut expected_tail, &good_key),
|
||||||
"padding-tail must decrypt under good_key"
|
"padding-tail must decrypt under good_key"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1542,12 +1546,12 @@ mod tests {
|
|||||||
// ONLY the genuinely-undecryptable unit A is tallied / concealed.
|
// ONLY the genuinely-undecryptable unit A is tallied / concealed.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"exactly one unit (the undecryptable one) is counted as loss"
|
"exactly one unit (the undecryptable one) is counted as loss"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit A → NULL TS (concealed).
|
// Unit A → NULL TS (concealed).
|
||||||
let unit0 = &buf[..crate::aacs::ALIGNED_UNIT_LEN];
|
let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
while off + 192 <= unit0.len() {
|
while off + 192 <= unit0.len() {
|
||||||
assert_eq!(unit0[off + 4], 0x47, "unit A null packet sync at {off}");
|
assert_eq!(unit0[off + 4], 0x47, "unit A null packet sync at {off}");
|
||||||
@@ -1562,7 +1566,8 @@ mod tests {
|
|||||||
// Unit B → the GOOD decrypted padding tail, byte-for-byte intact (NOT
|
// Unit B → the GOOD decrypted padding tail, byte-for-byte intact (NOT
|
||||||
// overwritten with NULL TS). This is the silent-data-loss the old
|
// overwritten with NULL TS). This is the silent-data-loss the old
|
||||||
// majority-vote predicate caused.
|
// majority-vote predicate caused.
|
||||||
let unit1 = &buf[crate::aacs::ALIGNED_UNIT_LEN..2 * crate::aacs::ALIGNED_UNIT_LEN];
|
let unit1 = &buf
|
||||||
|
[crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unit1,
|
unit1,
|
||||||
&expected_tail[..],
|
&expected_tail[..],
|
||||||
@@ -1587,8 +1592,8 @@ mod tests {
|
|||||||
/// `mux::ts` reads as a concealed gap.
|
/// `mux::ts` reads as a concealed gap.
|
||||||
#[test]
|
#[test]
|
||||||
fn null_ts_fill_is_well_formed_and_invisible_to_real_pids() {
|
fn null_ts_fill_is_well_formed_and_invisible_to_real_pids() {
|
||||||
let mut unit = vec![0xAAu8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0xAAu8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
crate::aacs::fill_null_ts_unit(&mut unit);
|
crate::aacs::content::fill_null_ts_unit(&mut unit);
|
||||||
// 32 packets, each: sync 0x47, PID 0x1FFF, adaptation-only (0b10) with a
|
// 32 packets, each: sync 0x47, PID 0x1FFF, adaptation-only (0b10) with a
|
||||||
// discontinuity_indicator in the adaptation field.
|
// discontinuity_indicator in the adaptation field.
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
@@ -1679,7 +1684,7 @@ mod tests {
|
|||||||
"callback must be invoked once with the failing unit"
|
"callback must be invoked once with the failing unit"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
crate::aacs::ts_sync_destroyed(&got[0]),
|
crate::aacs::content::ts_sync_destroyed(&got[0]),
|
||||||
"the sample handed to the callback is the still-scrambled ciphertext"
|
"the sample handed to the callback is the still-scrambled ciphertext"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1703,7 +1708,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
nocb_loss.load(Ordering::Relaxed),
|
nocb_loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"without a fetch callback the undecryptable unit is loss"
|
"without a fetch callback the undecryptable unit is loss"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1784,7 +1789,7 @@ mod tests {
|
|||||||
"fetch fired for BOTH units — the dry result for A did not latch off B"
|
"fetch fired for BOTH units — the dry result for A did not latch off B"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!crate::aacs::ts_sync_destroyed(&buf2),
|
!crate::aacs::content::ts_sync_destroyed(&buf2),
|
||||||
"unit B is decrypted after its on-demand fetch"
|
"unit B is decrypted after its on-demand fetch"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1899,7 +1904,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < bytes {
|
while u < bytes {
|
||||||
buf[u] |= 0xC0;
|
buf[u] |= 0xC0;
|
||||||
u += crate::aacs::ALIGNED_UNIT_LEN;
|
u += crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
@@ -1998,7 +2003,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < b {
|
while u < b {
|
||||||
buf[u] |= 0xC0; // CPI bits → reads as encrypted
|
buf[u] |= 0xC0; // CPI bits → reads as encrypted
|
||||||
u += crate::aacs::ALIGNED_UNIT_LEN;
|
u += crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
Ok(b)
|
Ok(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl fu::scsi::ScsiTransport for ScsiAdapter<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Map libfreemkv's host certs (keysource-collected) to the unlock contract's.
|
/// Map libfreemkv's host certs (keysource-collected) to the unlock contract's.
|
||||||
pub(crate) fn map_host_certs(certs: &[crate::aacs::HostCert]) -> Vec<fu::HostCert> {
|
pub(crate) fn map_host_certs(certs: &[crate::aacs::types::HostCert]) -> Vec<fu::HostCert> {
|
||||||
certs
|
certs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| fu::HostCert {
|
.map(|c| fu::HostCert {
|
||||||
|
|||||||
Reference in New Issue
Block a user