v0.27.0: detect AACS-scrambled units by raw TS sync, not flag bits

Rename is_unit_encrypted -> is_aacs_scrambled and decide encryption from the unit's MPEG-TS sync bytes (destroyed by the encrypted body) instead of the TP_extra copy-control (byte 0) or TS scrambling-control (byte 7) flags, which discs do not set reliably. One shared predicate now backs the decrypt gate and out-of-band key validation, so callers agree on what 'encrypted' means. Decryption restores the syncs, so a decrypted unit reads as clear and there is no flag to clear.
This commit is contained in:
MattJackson
2026-06-03 07:35:50 -07:00
parent c8eb42b490
commit b518860d9c
8 changed files with 138 additions and 132 deletions
+15
View File
@@ -1,5 +1,20 @@
# Changelog # Changelog
## 0.27.0 (2026-06-03)
### Changed
- **AACS unit-encryption detection now reads the raw MPEG-TS sync bytes
instead of header flag bits.** `is_unit_encrypted` is renamed
`is_aacs_scrambled`: a unit is encrypted iff its body TS packet sync bytes
(`0x47` at offset 4 and every 192 bytes) are not intact — the encrypted
body destroys them. The previous check read the TP_extra copy-control bits
(byte 0) or TS transport_scrambling_control bits (byte 7), which discs and
players do not set reliably. A single predicate now backs both the decrypt
gate and out-of-band key validation, so all callers agree on what
"encrypted" means. Decryption restores the syncs, so a decrypted unit reads
as clear and there is no encryption flag to clear.
## 0.26.1 (2026-05-22) ## 0.26.1 (2026-05-22)
### Added ### Added
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "0.26.11" version = "0.27.0"
edition = "2024" edition = "2024"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+42 -42
View File
@@ -69,28 +69,29 @@ pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
// ── Content decryption ────────────────────────────────────────────────────── // ── Content decryption ──────────────────────────────────────────────────────
/// Check if a 6144-byte aligned unit is encrypted. /// True if a 6144-byte aligned unit is AACS-scrambled on disc.
/// ///
/// AACS encrypts at aligned-unit granularity (all-or-nothing per unit) and /// AACS encrypts the unit body, which destroys the MPEG-TS sync bytes (`0x47`)
/// signals it via the TS `transport_scrambling_control` (TSC) bits — the top /// a clear unit carries at offsets 4, 196, 388, … (one per 192-byte source
/// two bits of TS-header byte 3, which is byte 7 of the unit (4-byte /// packet). So "scrambled" = "the TS syncs are NOT intact". This is
/// TP_extra_header + sync + PID/flags). TSC `00` = clear, non-zero = scrambled. /// flag-independent: it does NOT read the TP_extra copy-control bits (byte 0)
/// Byte 7 sits inside the clear 16-byte seed, so this is readable without the /// or the TS scrambling-control bits (byte 7) — AACS sets neither reliably
/// key. /// across discs/players.
/// ///
/// NOTE: the earlier check read byte 0's TP_extra copy-control bits (`& 0xC0`), /// This is the single shared definition of "encrypted" for the whole ecosystem
/// which are copy-permission, NOT encryption status — they false-positive on /// — libfreemkv's decrypt gate, autorip's sample selection, and the online key
/// clear navigation units (PAT/PMT at a clip's start), causing a correct key to /// service's validation gate all call THIS, so they always agree on what is
/// be decrypted against clear data and wrongly rejected. /// encrypted. A correctly-decrypted (or natively-clear) unit reports `false`,
pub fn is_unit_encrypted(unit: &[u8]) -> bool { /// so the decrypt path never double-decrypts and there is no flag to clear.
unit.len() >= ALIGNED_UNIT_LEN && (unit[7] >> 6) & 0x03 != 0 pub fn is_aacs_scrambled(unit: &[u8]) -> bool {
unit.len() >= ALIGNED_UNIT_LEN && !ts_syncs_intact(unit)
} }
/// Verify decrypted unit by checking TS sync bytes at expected offsets. /// Most TS packet positions in `unit` carry the `0x47` sync byte — i.e. the
fn verify_ts(unit: &[u8]) -> bool { /// unit looks like clear MPEG-TS. Syncs sit at offset 4 and every 192 bytes
// In a 6144-byte unit, TS packets start at byte 0 with 4-byte TP_extra_header /// after (4-byte TP_extra_header + 188-byte TS packet). An encrypted body
// then 188-byte TS packet, repeating every 192 bytes. /// scrambles all but the first (which lives in the clear 16-byte seed).
// Sync byte 0x47 should appear at offset 4, 196, 388, ... fn ts_syncs_intact(unit: &[u8]) -> bool {
let mut count = 0; let mut count = 0;
let mut offset = 4; let mut offset = 4;
while offset < unit.len() { while offset < unit.len() {
@@ -99,11 +100,15 @@ fn verify_ts(unit: &[u8]) -> bool {
} }
offset += TS_PACKET_LEN; offset += TS_PACKET_LEN;
} }
// Expect at least most packets to have sync bytes
let total = (unit.len() - 4) / TS_PACKET_LEN + 1; let total = (unit.len() - 4) / TS_PACKET_LEN + 1;
count > total / 2 count > total / 2
} }
/// Verify a decrypted unit looks like clear MPEG-TS (sync bytes intact).
fn verify_ts(unit: &[u8]) -> bool {
ts_syncs_intact(unit)
}
/// Decrypt one AACS aligned unit (6144 bytes) in-place. /// Decrypt one AACS aligned unit (6144 bytes) in-place.
/// Returns true if decryption succeeded (verified by TS sync bytes). /// Returns true if decryption succeeded (verified by TS sync bytes).
/// ///
@@ -111,12 +116,14 @@ fn verify_ts(unit: &[u8]) -> bool {
/// 1. AES-128-ECB encrypt first 16 bytes with unit_key → derived /// 1. AES-128-ECB encrypt first 16 bytes with unit_key → derived
/// 2. XOR derived with original 16 bytes → unit_decrypt_key /// 2. XOR derived with original 16 bytes → unit_decrypt_key
/// 3. AES-128-CBC decrypt bytes 16..6143 with unit_decrypt_key and AACS IV /// 3. AES-128-CBC decrypt bytes 16..6143 with unit_decrypt_key and AACS IV
/// 4. Clear encryption flag bits ///
/// Decryption restores the TS sync bytes, so the unit reads as clear afterward;
/// there is no flag to clear.
pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool { pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
if unit.len() < ALIGNED_UNIT_LEN { if unit.len() < ALIGNED_UNIT_LEN {
return false; return false;
} }
if !is_unit_encrypted(unit) { if !is_aacs_scrambled(unit) {
return true; // not encrypted return true; // not encrypted
} }
@@ -136,24 +143,13 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
// Step 3: Decrypt bytes 16..6143 with AES-CBC // Step 3: Decrypt bytes 16..6143 with AES-CBC
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]); aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
// Step 4: Clear the encryption flag — the TS transport_scrambling_control // Decryption restored the TS syncs; verify the unit now looks like clear TS.
// bits (top two of TS-header byte 3) of every packet, so the output is
// valid unscrambled TS. Each 192-byte cell's TS header byte 3 sits at
// offset 7 within the cell. (The old code cleared byte 0's TP_extra
// copy-control bits, which are NOT the scrambling flag.)
let mut off = 7;
while off < ALIGNED_UNIT_LEN {
unit[off] &= 0x3F;
off += TS_PACKET_LEN;
}
// Verify
verify_ts(unit) verify_ts(unit)
} }
/// Decrypt one aligned unit trying multiple unit keys. Returns the key index that worked. /// Decrypt one aligned unit trying multiple unit keys. Returns the key index that worked.
pub fn decrypt_unit_try_keys(unit: &mut [u8], unit_keys: &[[u8; 16]]) -> Option<usize> { pub fn decrypt_unit_try_keys(unit: &mut [u8], unit_keys: &[[u8; 16]]) -> Option<usize> {
if !is_unit_encrypted(unit) { if !is_aacs_scrambled(unit) {
return Some(0); return Some(0);
} }
@@ -193,7 +189,7 @@ pub fn decrypt_unit_full(
unit_key: &[u8; 16], unit_key: &[u8; 16],
read_data_key: Option<&[u8; 16]>, read_data_key: Option<&[u8; 16]>,
) -> bool { ) -> bool {
if !is_unit_encrypted(unit) { if !is_aacs_scrambled(unit) {
return true; return true;
} }
if let Some(rdk) = read_data_key { if let Some(rdk) = read_data_key {
@@ -220,10 +216,15 @@ mod tests {
#[test] #[test]
fn test_decrypt_unit_unencrypted() { fn test_decrypt_unit_unencrypted() {
// Unit with 0xC0 bits clear should pass through unchanged // A clear unit (TS syncs intact) is not scrambled → passes through.
let mut unit = vec![0u8; ALIGNED_UNIT_LEN]; let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
unit[0] = 0x00; // not encrypted let mut off = 4;
while off < ALIGNED_UNIT_LEN {
unit[off] = TS_SYNC;
off += TS_PACKET_LEN;
}
let key = [0u8; 16]; let key = [0u8; 16];
assert!(!is_aacs_scrambled(&unit));
assert!(decrypt_unit(&mut unit, &key)); assert!(decrypt_unit(&mut unit, &key));
} }
@@ -273,9 +274,8 @@ mod tests {
plain[offset] = TS_SYNC; plain[offset] = TS_SYNC;
offset += TS_PACKET_LEN; offset += TS_PACKET_LEN;
} }
// Set the encryption flag: TS transport_scrambling_control (top two // No flag set: CBC-encrypting the body below scrambles packets 1..31's
// bits of byte 7), inside the clear seed. // TS syncs, which is exactly what `is_aacs_scrambled` (raw-sync) detects.
plain[7] |= 0x80;
// Now encrypt bytes 16..6143 using the AACS algorithm (reverse of decrypt) // Now encrypt bytes 16..6143 using the AACS algorithm (reverse of decrypt)
let header: [u8; 16] = plain[..16].try_into().unwrap(); let header: [u8; 16] = plain[..16].try_into().unwrap();
@@ -302,9 +302,9 @@ mod tests {
// Now plain contains encrypted data. Decrypt it. // Now plain contains encrypted data. Decrypt it.
let mut unit = plain; let mut unit = plain;
assert!(is_unit_encrypted(&unit)); assert!(is_aacs_scrambled(&unit));
assert!(decrypt_unit(&mut unit, &unit_key)); assert!(decrypt_unit(&mut unit, &unit_key));
assert!(!is_unit_encrypted(&unit)); // flag should be cleared assert!(!is_aacs_scrambled(&unit)); // decrypted: TS syncs restored
// Verify TS sync bytes // Verify TS sync bytes
let mut count = 0; let mut count = 0;
+1 -1
View File
@@ -1292,7 +1292,7 @@ mod tests {
let original = std::fs::read(unit_path).unwrap(); let original = std::fs::read(unit_path).unwrap();
assert_eq!(original.len(), ALIGNED_UNIT_LEN); assert_eq!(original.len(), ALIGNED_UNIT_LEN);
assert!( assert!(
super::super::decrypt::is_unit_encrypted(&original), super::super::decrypt::is_aacs_scrambled(&original),
"Unit should be encrypted" "Unit should be encrypted"
); );
+1 -1
View File
@@ -24,7 +24,7 @@ pub mod variants;
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs. // AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
pub use decrypt::{ pub use decrypt::{
ALIGNED_UNIT_LEN, decrypt_bus, decrypt_unit, decrypt_unit_full, decrypt_unit_try_keys, ALIGNED_UNIT_LEN, decrypt_bus, decrypt_unit, decrypt_unit_full, decrypt_unit_try_keys,
is_unit_encrypted, is_aacs_scrambled,
}; };
pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb}; pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
pub use keys::probe; pub use keys::probe;
+11 -10
View File
@@ -185,12 +185,13 @@ pub fn decrypt_sectors(
let chunks: Vec<&mut [u8]> = buf.chunks_mut(unit_len).collect(); let chunks: Vec<&mut [u8]> = buf.chunks_mut(unit_len).collect();
let nunits = chunks.len(); let nunits = chunks.len();
// Per-unit decrypt closure. The is_unit_encrypted check is // Per-unit decrypt closure. The is_aacs_scrambled check reads the
// a byte-0 heuristic; on a misfire we snapshot+restore via // raw TS syncs; a non-m2ts unit (e.g. MPLS/CLPI nav file) can look
// the original bytes so non-m2ts (e.g. MPLS/CLPI nav files) // scrambled and trigger a decrypt attempt, so on a verify miss we
// survive. See test `nav_file_unit_survives_decrypt_attempt`. // snapshot+restore the original bytes so it survives. See test
// `nav_file_unit_survives_decrypt_attempt`.
let decrypt_one = |chunk: &mut [u8]| { let decrypt_one = |chunk: &mut [u8]| {
if chunk.len() == unit_len && aacs::is_unit_encrypted(chunk) { if chunk.len() == unit_len && aacs::is_aacs_scrambled(chunk) {
let original: Vec<u8> = chunk.to_vec(); let original: Vec<u8> = chunk.to_vec();
if !aacs::decrypt_unit_full(chunk, &uk, rdk.as_ref()) { if !aacs::decrypt_unit_full(chunk, &uk, rdk.as_ref()) {
chunk.copy_from_slice(&original); chunk.copy_from_slice(&original);
@@ -232,11 +233,11 @@ pub fn decrypt_sectors(
mod tests { mod tests {
use super::*; use super::*;
/// Regression for the 0.18.1 nav-file scramble bug. A non-m2ts unit whose /// Regression for the 0.18.1 nav-file scramble bug. A non-m2ts unit (here
/// first byte has the top 2 bits set (here: the ASCII letter 'M' that /// an MPLS file: starts "MPLS", carries no TS syncs) reads as scrambled
/// MPLS files start with, 0x4D = 0b01001101) trips `is_unit_encrypted`, /// under `is_aacs_scrambled`, gets AES-decrypted with the unit key, fails
/// gets AES-decrypted with the unit key, fails the TS-sync verification, /// the TS-sync verification, and must be restored to its original bytes —
/// and must be restored to its original bytes — 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::ALIGNED_UNIT_LEN];
+52 -61
View File
@@ -89,9 +89,8 @@ fn aacs_decrypt_unit_roundtrip() {
plain[offset] = 0x47; // TS sync byte plain[offset] = 0x47; // TS sync byte
offset += 192; offset += 192;
} }
// Set encryption flag: TS transport_scrambling_control (top two bits of // No flag set: CBC-encrypting the body below scrambles packets 1..31's TS
// byte 7), inside the clear seed. // syncs, which is exactly what `is_aacs_scrambled` (raw-sync) detects.
plain[7] |= 0x80;
// Save original plaintext for comparison // Save original plaintext for comparison
let expected = plain.clone(); let expected = plain.clone();
@@ -127,8 +126,8 @@ fn aacs_decrypt_unit_roundtrip() {
prev.copy_from_slice(&plain[off..off + 16]); prev.copy_from_slice(&plain[off..off + 16]);
} }
// Verify it looks encrypted // Verify it looks encrypted (body TS syncs scrambled)
assert!(aacs::is_unit_encrypted(&plain)); assert!(aacs::is_aacs_scrambled(&plain));
// Now decrypt // Now decrypt
let result = aacs::decrypt_unit(&mut plain, &unit_key); let result = aacs::decrypt_unit(&mut plain, &unit_key);
@@ -137,8 +136,8 @@ fn aacs_decrypt_unit_roundtrip() {
"decrypt_unit should return true on valid encrypted unit" "decrypt_unit should return true on valid encrypted unit"
); );
assert!( assert!(
!aacs::is_unit_encrypted(&plain), !aacs::is_aacs_scrambled(&plain),
"encryption flag should be cleared" "decrypted unit should read as clear (TS syncs restored)"
); );
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt) // Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
@@ -157,14 +156,11 @@ fn aacs_decrypt_unit_roundtrip() {
sync_count, expected_syncs sync_count, expected_syncs
); );
// decrypt clears the TSC bits (byte 7, top two) — the only change from the // Decryption clears no flag, so the unit round-trips byte-for-byte.
// original plaintext. Everything else round-trips exactly.
assert_eq!(plain[7], expected[7] & 0x3F, "TSC bits should be cleared");
assert_eq!(&plain[..7], &expected[..7], "bytes 0..7 mismatch");
assert_eq!( assert_eq!(
&plain[8..aacs::ALIGNED_UNIT_LEN], &plain[..],
&expected[8..aacs::ALIGNED_UNIT_LEN], &expected[..],
"decrypted unit body does not match original" "decrypted unit does not match original plaintext"
); );
} }
@@ -258,56 +254,65 @@ fn aacs_vuk_derivation_roundtrip() {
assert_eq!(vuk, vuk2, "derive_vuk not deterministic"); assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
} }
/// Test: aacs_is_unit_encrypted detects encryption flags correctly. /// Test: aacs_is_aacs_scrambled detects scrambled units via the raw TS syncs.
#[test] #[test]
fn aacs_is_unit_encrypted_detection() { fn aacs_is_aacs_scrambled_detection() {
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN]; // A clear unit: TS sync (0x47) intact at every 192-byte packet → not
// scrambled. (Flag bits play no role.)
let mut clear = vec![0u8; aacs::ALIGNED_UNIT_LEN];
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
clear[off] = 0x47;
off += 192;
}
assert!( assert!(
!aacs::is_unit_encrypted(&unit), !aacs::is_aacs_scrambled(&clear),
"zero unit should not be encrypted" "clear unit (syncs intact) must not be scrambled"
); );
// The encryption flag is the TS transport_scrambling_control (top two bits // Flag bits do NOT make a synced unit look scrambled.
// of byte 7). Any non-zero TSC = encrypted. let mut flagged = clear.clone();
unit[7] = 0x40; // TSC = 01 flagged[0] = 0xC0; // copy-control bits
assert!(aacs::is_unit_encrypted(&unit)); flagged[7] = 0xC0; // TSC bits
unit[7] = 0x80; // TSC = 10
assert!(aacs::is_unit_encrypted(&unit));
unit[7] = 0xC0; // TSC = 11
assert!(aacs::is_unit_encrypted(&unit));
unit[7] = 0x3F; // top two bits clear
assert!(!aacs::is_unit_encrypted(&unit));
// Byte 0's TP_extra copy-control bits are NOT the encryption flag.
unit[7] = 0x00;
unit[0] = 0xC0;
assert!( assert!(
!aacs::is_unit_encrypted(&unit), !aacs::is_aacs_scrambled(&flagged),
"byte-0 copy-control bits must not be read as encryption" "flag bits must not be read as encryption"
);
// A scrambled body (syncs destroyed) → scrambled.
let scrambled = vec![0x99u8; aacs::ALIGNED_UNIT_LEN];
assert!(
aacs::is_aacs_scrambled(&scrambled),
"unit with no intact TS syncs must read as scrambled"
); );
// Too short // Too short
let short = vec![0xFFu8; 100]; let short = vec![0xFFu8; 100];
assert!( assert!(
!aacs::is_unit_encrypted(&short), !aacs::is_aacs_scrambled(&short),
"short buffer should not be detected" "short buffer should not be detected"
); );
} }
/// Test: aacs_decrypt_unit_unencrypted_passthrough /// Test: aacs_decrypt_unit_unencrypted_passthrough
/// ///
/// A unit without encryption flags should pass through decrypt_unit unchanged. /// A clear unit (TS syncs intact) should pass through decrypt_unit unchanged.
#[test] #[test]
fn aacs_decrypt_unit_unencrypted_passthrough() { fn aacs_decrypt_unit_unencrypted_passthrough() {
let mut unit = vec![0x42u8; aacs::ALIGNED_UNIT_LEN]; let mut unit = vec![0x42u8; aacs::ALIGNED_UNIT_LEN];
unit[7] &= 0x3F; // TSC = 0 → clear/unencrypted unit // Intact TS syncs every 192 bytes → not scrambled → passthrough.
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
unit[off] = 0x47;
off += 192;
}
let original = unit.clone(); let original = unit.clone();
let key = [0xAA; 16]; let key = [0xAA; 16];
assert!(!aacs::is_aacs_scrambled(&unit));
let result = aacs::decrypt_unit(&mut unit, &key); let result = aacs::decrypt_unit(&mut unit, &key);
assert!(result, "unencrypted unit should return true"); assert!(result, "clear unit should return true");
assert_eq!(unit, original, "unencrypted unit should be unchanged"); assert_eq!(unit, original, "clear unit should be unchanged");
} }
// ── AACS cross-validation with independent AES implementation ────────────── // ── AACS cross-validation with independent AES implementation ──────────────
@@ -374,8 +379,7 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
plaintext[i] = (i % 251) as u8; plaintext[i] = (i % 251) as u8;
} }
} }
// Set encryption flag: TSC bits of packet 0 (byte 7). // No flag set: the CBC-encrypted body scrambles the packet syncs.
plaintext[7] |= 0x80;
let expected = plaintext.clone(); let expected = plaintext.clone();
@@ -406,19 +410,11 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
ok, ok,
"decrypt_unit returned false (TS sync verification failed)" "decrypt_unit returned false (TS sync verification failed)"
); );
assert_eq!(plaintext[7] >> 6, 0, "TSC bits not cleared");
// decrypt clears the TSC bits of every packet (byte 7 of each 192-byte // Decryption clears no flag, so the unit round-trips byte-for-byte.
// cell). Clear the same positions in the expected copy before comparing.
let mut expected_cleared = expected.clone();
let mut o = 7;
while o < aacs::ALIGNED_UNIT_LEN {
expected_cleared[o] &= 0x3F;
o += 192;
}
assert_eq!( assert_eq!(
&plaintext[..], &plaintext[..],
&expected_cleared[..], &expected[..],
"decrypted unit does not match original plaintext" "decrypted unit does not match original plaintext"
); );
} }
@@ -438,7 +434,7 @@ fn aacs_cross_validation_alternate_key() {
plaintext[off] = 0x47; plaintext[off] = 0x47;
off += 192; off += 192;
} }
plaintext[7] |= 0x80; // TSC encryption flag // No flag set: the CBC-encrypted body scrambles the packet syncs.
let expected = plaintext.clone(); let expected = plaintext.clone();
let mut header = [0u8; 16]; let mut header = [0u8; 16];
@@ -456,13 +452,8 @@ fn aacs_cross_validation_alternate_key() {
assert!(aacs::decrypt_unit(&mut plaintext, &unit_key)); assert!(aacs::decrypt_unit(&mut plaintext, &unit_key));
let mut expected_cleared = expected; // Decryption clears no flag, so the unit round-trips byte-for-byte.
let mut o = 7; assert_eq!(&plaintext[..], &expected[..]);
while o < aacs::ALIGNED_UNIT_LEN {
expected_cleared[o] &= 0x3F;
o += 192;
}
assert_eq!(&plaintext[..], &expected_cleared[..]);
} }
/// Verify that `decrypt_bus` correctly reverses AES-CBC encryption applied /// Verify that `decrypt_bus` correctly reverses AES-CBC encryption applied
+15 -16
View File
@@ -82,25 +82,24 @@ fn decrypt_sectors_with_css_keys_works() {
/// Test: AACS unit encryption detection works. /// Test: AACS unit encryption detection works.
#[test] #[test]
fn aacs_encryption_flag_detection() { fn aacs_encryption_flag_detection() {
// A clear unit: TS syncs (0x47) intact at every 192-byte packet.
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN]; let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
let mut off = 4;
while off < aacs::ALIGNED_UNIT_LEN {
unit[off] = 0x47;
off += 192;
}
// Encryption is the scrambled body (TS syncs destroyed), NOT a flag bit.
assert!(!aacs::is_aacs_scrambled(&unit));
// The encryption flag is the TS transport_scrambling_control (top two bits // Flag bits on a synced unit do not make it look encrypted.
// of byte 7), not byte 0's copy-control bits.
assert!(!aacs::is_unit_encrypted(&unit));
unit[7] = 0x40; // TSC = 01
assert!(aacs::is_unit_encrypted(&unit));
unit[7] = 0x80; // TSC = 10
assert!(aacs::is_unit_encrypted(&unit));
unit[7] = 0xC0; // TSC = 11
assert!(aacs::is_unit_encrypted(&unit));
// Byte 0 copy-control bits must NOT count as encryption.
unit[7] = 0x00;
unit[0] = 0xC0; unit[0] = 0xC0;
assert!(!aacs::is_unit_encrypted(&unit)); unit[7] = 0xC0;
assert!(!aacs::is_aacs_scrambled(&unit));
// Scrambled body (syncs gone) → encrypted.
let scrambled = vec![0x99u8; aacs::ALIGNED_UNIT_LEN];
assert!(aacs::is_aacs_scrambled(&scrambled));
} }
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state. /// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.