Promote AACS unit encryption to real API, and assert decrypt byte-exactly
encrypt_unit becomes public library API rather than a #[cfg(test)] helper.
Authoring an encrypted disc image is a legitimate use of this crate, and
the capability was already written four times over: a pub(crate) test-only
copy in aacs/content.rs plus three hand-rolled duplicates in decrypt.rs,
sector/decrypting.rs and disc/extract.rs. All four now call one function,
removing ~110 lines of duplicated cipher code that could drift from
decrypt_unit independently.
It mirrors decrypt_unit's purity contract: crypto only, no encrypted-flag
handling, because where that flag lives is container-specific (CPI bits in
byte 0 for BD-TS, elsewhere for HD-DVD-PS). Callers set the flag BEFORE
encrypting — bytes 0..16 are the key seed left in plaintext, so touching a
header byte afterwards changes the key a decryptor derives. That footgun is
documented at the function and at every call site.
Two tests pin it: an exact round trip through both directions, and the one
place the pair is deliberately asymmetric — decrypt_unit restores
all-zero-on-disc packets to zero, and the test proves an all-zero plaintext
packet enciphers to non-zero bytes so it is never mistaken for padding.
That asymmetry was previously only prose.
Two decrypt tests were also weaker than their own names:
* aacs_clear_trailing_partial_passes_through asserted only is_ok(), so a
mutant corrupting the clear partial while returning Ok passed. It now
snapshots the buffer and asserts byte equality, matching the
none_keys_is_noop pattern already in the file.
* aacs_decorator_decrypts_encrypted_unit_via_map checked only that 0x47
reappeared at the 192-byte stride, leaving corruption in the other 6112
bytes undetected. The plaintext is fully known, so it now asserts
byte-exact recovery against it.
Both were verified red first by mutating the production path.
This commit is contained in:
+23
-31
@@ -738,12 +738,13 @@ mod tests {
|
||||
assert_eq!(n, 2048, "CSS reads must not be unit-alignment gated");
|
||||
}
|
||||
|
||||
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
||||
/// encrypt it under `unit_key` so `aacs::content::decrypt_unit` recovers it. Mirrors
|
||||
/// the encrypt helper in `crate::decrypt`'s tests.
|
||||
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
/// The clear 6144-byte AACS unit that [`encrypt_aacs_unit`] encrypts: all
|
||||
/// zeroes except a TS sync `0x47` at the BD-TS stride (offset 4, then every
|
||||
/// 192 bytes) and the CPI bits on byte 0.
|
||||
///
|
||||
/// Exposed separately so a decrypt test can assert byte-exact recovery of the
|
||||
/// known plaintext instead of only spot-checking the sync bytes.
|
||||
fn clear_aacs_unit() -> Vec<u8> {
|
||||
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let mut off = 4;
|
||||
while off < unit.len() {
|
||||
@@ -752,25 +753,14 @@ mod tests {
|
||||
}
|
||||
// CPI bits on byte 0 so it reads as encrypted; set before key derivation.
|
||||
unit[0] |= 0xC0;
|
||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
||||
let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header);
|
||||
let mut k = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
k[i] = derived[i] ^ header[i];
|
||||
}
|
||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||
let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||
for i in 0..blocks {
|
||||
let o = 16 + i * 16;
|
||||
for j in 0..16 {
|
||||
unit[o + j] ^= prev[j];
|
||||
}
|
||||
let mut blk = GenericArray::clone_from_slice(&unit[o..o + 16]);
|
||||
cipher.encrypt_block(&mut blk);
|
||||
unit[o..o + 16].copy_from_slice(&blk);
|
||||
prev.copy_from_slice(&unit[o..o + 16]);
|
||||
}
|
||||
unit
|
||||
}
|
||||
|
||||
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
||||
/// encrypt it under `unit_key` so `aacs::content::decrypt_unit` recovers it.
|
||||
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
||||
let mut unit = clear_aacs_unit();
|
||||
crate::aacs::content::encrypt_unit(&mut unit, unit_key);
|
||||
unit
|
||||
}
|
||||
|
||||
@@ -827,12 +817,14 @@ mod tests {
|
||||
let mut buf = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||
let n = dec.read_sectors(0, 3, &mut buf, false).unwrap();
|
||||
assert_eq!(n, crate::aacs::content::ALIGNED_UNIT_LEN);
|
||||
// Decrypted: the TS sync 0x47 reappears at the BD-TS stride (offset 4, then
|
||||
// every 192 bytes). If the map/keys were wrong the bytes would stay
|
||||
// ciphertext and these syncs would be absent.
|
||||
for off in (4..crate::aacs::content::ALIGNED_UNIT_LEN).step_by(192) {
|
||||
assert_eq!(buf[off], 0x47, "TS sync recovered at offset {off}");
|
||||
}
|
||||
// The plaintext is fully known, so assert byte-exact recovery rather than
|
||||
// spot-checking the TS syncs: checking only 0x47 at the 192-byte stride let
|
||||
// corruption anywhere in the other 6112 bytes pass undetected.
|
||||
assert_eq!(
|
||||
buf,
|
||||
clear_aacs_unit(),
|
||||
"the decrypted unit must equal the known plaintext byte-for-byte"
|
||||
);
|
||||
}
|
||||
|
||||
/// An AACS decorator built WITHOUT a key map must fail loud on the first unit —
|
||||
|
||||
Reference in New Issue
Block a user