v0.26.11: detect AACS unit encryption via TS scrambling-control bits
is_unit_encrypted read the TP_extra copy-control bits (byte 0), which are a copy-permission flag, not an encryption flag. On discs whose sampled units are clear navigation packets (PAT/PMT) those bits can be set while the unit is not scrambled, so a correct Unit Key was used to 'decrypt' already-plaintext data, produced garbage, and the key was wrongly treated as failing. Read the actual flag instead: the TS transport_scrambling_control bits (top two of TS-header byte 3 = byte 7 of the aligned unit, inside the clear seed). AACS encrypts whole aligned units, so this one packet's TSC reflects the unit. decrypt_unit now clears the TSC bits of every packet on the way out so the result is valid unscrambled TS. Tests updated to the TSC flag.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.26.8"
|
version = "0.26.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
+27
-6
@@ -69,9 +69,21 @@ 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 (copy_permission_indicator bits).
|
/// Check if a 6144-byte aligned unit is encrypted.
|
||||||
|
///
|
||||||
|
/// AACS encrypts at aligned-unit granularity (all-or-nothing per unit) and
|
||||||
|
/// signals it via the TS `transport_scrambling_control` (TSC) bits — the top
|
||||||
|
/// two bits of TS-header byte 3, which is byte 7 of the unit (4-byte
|
||||||
|
/// TP_extra_header + sync + PID/flags). TSC `00` = clear, non-zero = scrambled.
|
||||||
|
/// Byte 7 sits inside the clear 16-byte seed, so this is readable without the
|
||||||
|
/// key.
|
||||||
|
///
|
||||||
|
/// NOTE: the earlier check read byte 0's TP_extra copy-control bits (`& 0xC0`),
|
||||||
|
/// which are copy-permission, NOT encryption status — they false-positive on
|
||||||
|
/// clear navigation units (PAT/PMT at a clip's start), causing a correct key to
|
||||||
|
/// be decrypted against clear data and wrongly rejected.
|
||||||
pub fn is_unit_encrypted(unit: &[u8]) -> bool {
|
pub fn is_unit_encrypted(unit: &[u8]) -> bool {
|
||||||
unit.len() >= ALIGNED_UNIT_LEN && (unit[0] & 0xC0) != 0
|
unit.len() >= ALIGNED_UNIT_LEN && (unit[7] >> 6) & 0x03 != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify decrypted unit by checking TS sync bytes at expected offsets.
|
/// Verify decrypted unit by checking TS sync bytes at expected offsets.
|
||||||
@@ -124,8 +136,16 @@ 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 encryption flag
|
// Step 4: Clear the encryption flag — the TS transport_scrambling_control
|
||||||
unit[0] &= !0xC0;
|
// 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
|
||||||
verify_ts(unit)
|
verify_ts(unit)
|
||||||
@@ -253,8 +273,9 @@ mod tests {
|
|||||||
plain[offset] = TS_SYNC;
|
plain[offset] = TS_SYNC;
|
||||||
offset += TS_PACKET_LEN;
|
offset += TS_PACKET_LEN;
|
||||||
}
|
}
|
||||||
// Set encryption flag
|
// Set the encryption flag: TS transport_scrambling_control (top two
|
||||||
plain[0] |= 0xC0;
|
// bits of byte 7), inside the clear seed.
|
||||||
|
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();
|
||||||
|
|||||||
+44
-33
@@ -89,8 +89,9 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
plain[offset] = 0x47; // TS sync byte
|
plain[offset] = 0x47; // TS sync byte
|
||||||
offset += 192;
|
offset += 192;
|
||||||
}
|
}
|
||||||
// Set encryption flag (bits 6-7 of byte 0)
|
// Set encryption flag: TS transport_scrambling_control (top two bits of
|
||||||
plain[0] |= 0xC0;
|
// byte 7), inside the clear seed.
|
||||||
|
plain[7] |= 0x80;
|
||||||
|
|
||||||
// Save original plaintext for comparison
|
// Save original plaintext for comparison
|
||||||
let expected = plain.clone();
|
let expected = plain.clone();
|
||||||
@@ -156,18 +157,15 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
sync_count, expected_syncs
|
sync_count, expected_syncs
|
||||||
);
|
);
|
||||||
|
|
||||||
// Compare all bytes except byte 0 (encryption flag cleared)
|
// decrypt clears the TSC bits (byte 7, top two) — the only change from the
|
||||||
|
// 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[1..aacs::ALIGNED_UNIT_LEN],
|
&plain[8..aacs::ALIGNED_UNIT_LEN],
|
||||||
&expected[1..aacs::ALIGNED_UNIT_LEN],
|
&expected[8..aacs::ALIGNED_UNIT_LEN],
|
||||||
"decrypted unit body does not match original"
|
"decrypted unit body does not match original"
|
||||||
);
|
);
|
||||||
// Byte 0: original had 0xC0 set, decrypted has it cleared
|
|
||||||
assert_eq!(
|
|
||||||
plain[0] & !0xC0,
|
|
||||||
expected[0] & !0xC0,
|
|
||||||
"byte 0 mismatch ignoring flag"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test 7: aacs_disc_hash_deterministic
|
/// Test 7: aacs_disc_hash_deterministic
|
||||||
@@ -270,20 +268,27 @@ fn aacs_is_unit_encrypted_detection() {
|
|||||||
"zero unit should not be encrypted"
|
"zero unit should not be encrypted"
|
||||||
);
|
);
|
||||||
|
|
||||||
unit[0] = 0x40; // bit 6 set
|
// The encryption flag is the TS transport_scrambling_control (top two bits
|
||||||
|
// of byte 7). Any non-zero TSC = encrypted.
|
||||||
|
unit[7] = 0x40; // TSC = 01
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
|
unit[7] = 0x80; // TSC = 10
|
||||||
unit[0] = 0x80; // bit 7 set
|
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
|
unit[7] = 0xC0; // TSC = 11
|
||||||
unit[0] = 0xC0; // both bits set
|
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
|
unit[7] = 0x3F; // top two bits clear
|
||||||
unit[0] = 0x3F; // bits 6-7 clear
|
|
||||||
assert!(!aacs::is_unit_encrypted(&unit));
|
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!(
|
||||||
|
!aacs::is_unit_encrypted(&unit),
|
||||||
|
"byte-0 copy-control bits must not be read as encryption"
|
||||||
|
);
|
||||||
|
|
||||||
// Too short
|
// Too short
|
||||||
let short = vec![0xC0u8; 100];
|
let short = vec![0xFFu8; 100];
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::is_unit_encrypted(&short),
|
!aacs::is_unit_encrypted(&short),
|
||||||
"short buffer should not be detected"
|
"short buffer should not be detected"
|
||||||
@@ -296,7 +301,7 @@ fn aacs_is_unit_encrypted_detection() {
|
|||||||
#[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[0] = 0x00; // no encryption flag
|
unit[7] &= 0x3F; // TSC = 0 → clear/unencrypted unit
|
||||||
let original = unit.clone();
|
let original = unit.clone();
|
||||||
let key = [0xAA; 16];
|
let key = [0xAA; 16];
|
||||||
|
|
||||||
@@ -369,8 +374,8 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
|||||||
plaintext[i] = (i % 251) as u8;
|
plaintext[i] = (i % 251) as u8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set encryption flag
|
// Set encryption flag: TSC bits of packet 0 (byte 7).
|
||||||
plaintext[0] = 0xC0;
|
plaintext[7] |= 0x80;
|
||||||
|
|
||||||
let expected = plaintext.clone();
|
let expected = plaintext.clone();
|
||||||
|
|
||||||
@@ -401,14 +406,19 @@ 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[0] & 0xC0, 0x00, "encryption flag not cleared");
|
assert_eq!(plaintext[7] >> 6, 0, "TSC bits not cleared");
|
||||||
|
|
||||||
// Compare (byte 0 flag was cleared)
|
// decrypt clears the TSC bits of every packet (byte 7 of each 192-byte
|
||||||
|
// cell). Clear the same positions in the expected copy before comparing.
|
||||||
let mut expected_cleared = expected.clone();
|
let mut expected_cleared = expected.clone();
|
||||||
expected_cleared[0] &= !0xC0;
|
let mut o = 7;
|
||||||
|
while o < aacs::ALIGNED_UNIT_LEN {
|
||||||
|
expected_cleared[o] &= 0x3F;
|
||||||
|
o += 192;
|
||||||
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
|
&plaintext[..],
|
||||||
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
|
&expected_cleared[..],
|
||||||
"decrypted unit does not match original plaintext"
|
"decrypted unit does not match original plaintext"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -428,7 +438,7 @@ fn aacs_cross_validation_alternate_key() {
|
|||||||
plaintext[off] = 0x47;
|
plaintext[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
plaintext[0] = 0xC0;
|
plaintext[7] |= 0x80; // TSC encryption flag
|
||||||
let expected = plaintext.clone();
|
let expected = plaintext.clone();
|
||||||
|
|
||||||
let mut header = [0u8; 16];
|
let mut header = [0u8; 16];
|
||||||
@@ -447,11 +457,12 @@ 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;
|
let mut expected_cleared = expected;
|
||||||
expected_cleared[0] &= !0xC0;
|
let mut o = 7;
|
||||||
assert_eq!(
|
while o < aacs::ALIGNED_UNIT_LEN {
|
||||||
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
|
expected_cleared[o] &= 0x3F;
|
||||||
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
|
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
|
||||||
|
|||||||
@@ -84,20 +84,23 @@ fn decrypt_sectors_with_css_keys_works() {
|
|||||||
fn aacs_encryption_flag_detection() {
|
fn aacs_encryption_flag_detection() {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||||
|
|
||||||
// No encryption flag
|
// The encryption flag is the TS transport_scrambling_control (top two bits
|
||||||
|
// of byte 7), not byte 0's copy-control bits.
|
||||||
assert!(!aacs::is_unit_encrypted(&unit));
|
assert!(!aacs::is_unit_encrypted(&unit));
|
||||||
|
|
||||||
// Set bit 6
|
unit[7] = 0x40; // TSC = 01
|
||||||
unit[0] |= 0x40;
|
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
|
|
||||||
// Set bit 7
|
unit[7] = 0x80; // TSC = 10
|
||||||
unit[0] = 0x80;
|
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
|
|
||||||
// Both bits set
|
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));
|
assert!(!aacs::is_unit_encrypted(&unit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.
|
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.
|
||||||
|
|||||||
Reference in New Issue
Block a user