mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
keyproof: answer only for a provable encrypted BD-TS unit; differential tests
unit_key_opens is false for other formats, units that are short, clear, or carry fewer than KEY_PROOF_PACKETS content packets (before, an empty slice or an all-zero unit opened under any key). The fast path returns packet counts, and a seeded differential pins them to decrypt_unit's over padding, zero-sync and corrupted packets; it fails if either skip branch is removed. The compile_fail door names E0425.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## [1.8.2] — Unreleased
|
||||
|
||||
- New non-default `keyproof` feature: `aacs::content::unit_key_opens(unit, key, format)` answers whether a key opens an encrypted BD-TS aligned unit (one bit, never plaintext). It is `false` for anything that proves nothing (other formats, short or clear units, fewer than four content packets); a wrong key passes about once in 10^5 units, so confirm a match on several. `test_util::encrypt_unit` builds ciphertext fixtures. The default build is unchanged.
|
||||
- Key lookup trusts the disc's declared CPS units: samples from each unit instead of 32 reads from every stream file (a 46-file UHD over USB: ~95 s → ~1.5 s). Each source is asked once; a disc no key opens is refused at once. FMTS discs included.
|
||||
- Key evidence reads the filesystem in batches, not one command per sector (~1 s less).
|
||||
- The live scan prefetches only the files it parses (~22 MB less on a UHD) and reads the MKB from a 4 MiB prefix, not 16 MiB; the MKB bytes are unchanged.
|
||||
|
||||
+34
-20
@@ -156,6 +156,12 @@ pub fn is_clean(unit: &[u8], format: crate::disc::ContentFormat) -> bool {
|
||||
// Structural "does this unit carry enough valid MPEG-TS to prove a key opened it?" (the mux's
|
||||
// key-selection/verify signal): synced >= min(E, KEY_PROOF_PACKETS) over non-padding packets.
|
||||
fn is_clean_ts(unit: &[u8]) -> bool {
|
||||
let (content, synced) = ts_packet_counts(unit);
|
||||
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
|
||||
}
|
||||
|
||||
// `(content, synced)`: non-padding packets after packet 0, and those whose sync byte is 0x47.
|
||||
fn ts_packet_counts(unit: &[u8]) -> (usize, usize) {
|
||||
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
|
||||
let limit = ALIGNED_UNIT_LEN.min(unit.len());
|
||||
let mut content = 0usize;
|
||||
@@ -173,13 +179,14 @@ fn is_clean_ts(unit: &[u8]) -> bool {
|
||||
}
|
||||
off += PKT;
|
||||
}
|
||||
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
|
||||
(content, synced)
|
||||
}
|
||||
|
||||
/// Does `unit_key` open this encrypted AACS aligned `unit`? One bit, never plaintext.
|
||||
/// BD-TS 6144-byte units take the decrypt-less TS-sync check (`ts_unit_key_opens`, only
|
||||
/// the proof blocks are touched); Program-Stream and other lengths fall back to the full
|
||||
/// [`decrypt_unit`] + [`is_clean`].
|
||||
/// Does `unit_key` open this encrypted BD-TS aligned `unit`? One bit, never plaintext.
|
||||
/// `true` only for a whole 6144-byte unit flagged encrypted whose plaintext would carry
|
||||
/// at least `KEY_PROOF_PACKETS` content packets, all synced: anything else proves nothing
|
||||
/// and is `false` (other formats, short or clear units, too little content). A wrong key
|
||||
/// passes about once in 10^5 units, so confirm a match on several units.
|
||||
///
|
||||
/// Behind the `keyproof` feature so the default public API still cannot decrypt AACS: a
|
||||
/// key oracle, not a decryptor (KU §2.2; see `keys::public_api_cannot_decrypt_aacs`).
|
||||
@@ -190,24 +197,31 @@ pub fn unit_key_opens(
|
||||
format: crate::disc::ContentFormat,
|
||||
) -> bool {
|
||||
use crate::disc::ContentFormat;
|
||||
if matches!(format, ContentFormat::BdTs) && unit.len() == ALIGNED_UNIT_LEN {
|
||||
return ts_unit_key_opens(unit, unit_key);
|
||||
if !matches!(format, ContentFormat::BdTs)
|
||||
|| unit.len() != ALIGNED_UNIT_LEN
|
||||
|| !aacs_unit_encrypted(unit, format)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// PS / non-standard length: full decrypt into a scratch copy, then structural check.
|
||||
let mut scratch = unit.to_vec();
|
||||
decrypt_unit(&mut scratch, unit_key);
|
||||
is_clean(&scratch, format)
|
||||
let (content, synced) = ts_unit_key_proof(unit, unit_key);
|
||||
content >= KEY_PROOF_PACKETS && synced >= KEY_PROOF_PACKETS
|
||||
}
|
||||
|
||||
/// BD-TS fast path of `unit_key_opens`: the same verdict as `decrypt_unit` +
|
||||
/// `is_clean(BdTs)` from one AES block per source packet. CBC decrypts any block alone
|
||||
/// [`is_clean_ts`]'s verdict for `decrypt_unit(unit, unit_key)`, from the packet counts of
|
||||
/// `ts_unit_key_proof`.
|
||||
#[cfg(test)]
|
||||
fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool {
|
||||
let (content, synced) = ts_unit_key_proof(unit, unit_key);
|
||||
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
|
||||
}
|
||||
|
||||
/// The `(content, synced)` counts `ts_packet_counts` would see after `decrypt_unit`, from
|
||||
/// one AES block per source packet. CBC decrypts any block alone
|
||||
/// (`P[i] = AES_dec(C[i]) ⊕ C[i-1]`), and packet heads sit at block-aligned offsets ≥ 192.
|
||||
/// Packet 0 is skipped (its sync byte is in the clear seed), as are all-zero padding
|
||||
/// packets, which `decrypt_unit` zeroes. Verdict mirrors [`is_clean_ts`]:
|
||||
/// `synced >= min(content, KEY_PROOF_PACKETS)`. `dead_code` is allowed so the
|
||||
/// equivalence test covers it without `keyproof`.
|
||||
#[allow(dead_code)]
|
||||
fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool {
|
||||
/// packets, which `decrypt_unit` zeroes.
|
||||
#[cfg(any(test, feature = "keyproof"))]
|
||||
fn ts_unit_key_proof(unit: &[u8], unit_key: &[u8; 16]) -> (usize, usize) {
|
||||
use aes::cipher::{Array, BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
|
||||
use aes::{Aes128, Block};
|
||||
const PKT: usize = BD_SOURCE_PACKET_BYTES; // 192
|
||||
@@ -223,7 +237,7 @@ fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool {
|
||||
*b ^= s;
|
||||
}
|
||||
let cipher = Aes128::new(&bk);
|
||||
// Plaintext of the 16-byte block at `o` (o ≥ 32, so C[i-1] is ciphertext).
|
||||
// Plaintext of the 16-byte block at `o` (o ≥ 192, so C[i-1] is ciphertext).
|
||||
let plain = |o: usize| {
|
||||
let mut b = block_at(o);
|
||||
cipher.decrypt_block(&mut b);
|
||||
@@ -268,7 +282,7 @@ fn ts_unit_key_opens(unit: &[u8], unit_key: &[u8; 16]) -> bool {
|
||||
_ => content += 1,
|
||||
}
|
||||
}
|
||||
content == 0 || synced >= content.min(KEY_PROOF_PACKETS)
|
||||
(content, synced)
|
||||
}
|
||||
|
||||
/// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride
|
||||
|
||||
@@ -1301,3 +1301,109 @@ fn ts_unit_key_opens_matches_decrypt_unit_then_is_clean() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn xorshift(state: &mut u64) -> u64 {
|
||||
*state ^= *state << 13;
|
||||
*state ^= *state >> 7;
|
||||
*state ^= *state << 17;
|
||||
*state
|
||||
}
|
||||
|
||||
/// A flagged, encrypted BD-TS unit whose packets mix synced content, header-only and
|
||||
/// zero-sync packets, zero-ciphertext padding and stray corruption, as seeded by `state`.
|
||||
fn mixed_encrypted_unit(state: &mut u64, key: &[u8; 16]) -> Vec<u8> {
|
||||
const PKT: usize = BD_SOURCE_PACKET_BYTES;
|
||||
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
|
||||
let mut kinds = Vec::new();
|
||||
for p in 0..ALIGNED_UNIT_LEN / PKT {
|
||||
let off = p * PKT;
|
||||
let kind = xorshift(state) % 5;
|
||||
kinds.push(kind);
|
||||
match kind {
|
||||
0 | 1 => {
|
||||
unit[off + 4] = TS_SYNC;
|
||||
for b in &mut unit[off + 5..off + PKT] {
|
||||
*b = xorshift(state) as u8;
|
||||
}
|
||||
}
|
||||
2 => unit[off + 4] = TS_SYNC,
|
||||
3 => unit[off + 4 + 1 + (xorshift(state) as usize % (PKT - 5))] = 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
aacs_encrypt_unit(&mut unit, key);
|
||||
for (p, &kind) in kinds.iter().enumerate().skip(1) {
|
||||
if kind == 4 && xorshift(state).is_multiple_of(2) {
|
||||
unit[p * PKT..(p + 1) * PKT].fill(0);
|
||||
}
|
||||
}
|
||||
if xorshift(state).is_multiple_of(4) {
|
||||
let i = 16 + xorshift(state) as usize % (ALIGNED_UNIT_LEN - 16);
|
||||
unit[i] ^= 1 + xorshift(state) as u8 % 255;
|
||||
}
|
||||
unit
|
||||
}
|
||||
|
||||
// Seeded differential: the fast path's packet counts and verdict match `decrypt_unit`'s over
|
||||
// padding, header-only and zero-sync packets and corruption, for right and wrong keys.
|
||||
#[test]
|
||||
fn ts_unit_key_opens_matches_the_full_path_on_mixed_units() {
|
||||
let fmt = crate::disc::ContentFormat::BdTs;
|
||||
let mut state = 0x9E37_79B9_7F4A_7C15u64;
|
||||
for _ in 0..400 {
|
||||
let mut right = [0u8; 16];
|
||||
right
|
||||
.iter_mut()
|
||||
.for_each(|b| *b = xorshift(&mut state) as u8);
|
||||
let enc = mixed_encrypted_unit(&mut state, &right);
|
||||
let mut wrong = right;
|
||||
wrong[xorshift(&mut state) as usize % 16] ^= 0x80;
|
||||
for key in [right, wrong] {
|
||||
let mut full = enc.clone();
|
||||
decrypt_unit(&mut full, &key);
|
||||
assert_eq!(ts_unit_key_proof(&enc, &key), ts_packet_counts(&full));
|
||||
assert_eq!(ts_unit_key_opens(&enc, &key), is_clean(&full, fmt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyproof")]
|
||||
#[test]
|
||||
fn unit_key_opens_answers_only_for_a_provable_encrypted_bd_ts_unit() {
|
||||
use crate::disc::ContentFormat::{BdTs, MpegPs};
|
||||
let right = [0x5Au8; 16];
|
||||
let mut enc = clear_unit();
|
||||
for (i, b) in enc.iter_mut().enumerate() {
|
||||
if i % BD_SOURCE_PACKET_BYTES > 4 {
|
||||
*b = (i as u8).wrapping_mul(7) | 1;
|
||||
}
|
||||
}
|
||||
aacs_encrypt_unit(&mut enc, &right);
|
||||
assert!(unit_key_opens(&enc, &right, BdTs));
|
||||
for n in 0..64u8 {
|
||||
assert!(!unit_key_opens(&enc, &[n; 16], BdTs), "wrong key {n}");
|
||||
}
|
||||
assert!(
|
||||
!unit_key_opens(&enc, &right, MpegPs),
|
||||
"only BD-TS is answered"
|
||||
);
|
||||
let mut clear_flag = enc.clone();
|
||||
clear_flag[0] &= 0x3F;
|
||||
assert!(!unit_key_opens(&clear_flag, &right, BdTs), "a clear unit");
|
||||
for len in [0, 383, ALIGNED_UNIT_LEN - 1] {
|
||||
assert!(!unit_key_opens(&enc[..len], &right, BdTs), "{len} bytes");
|
||||
}
|
||||
let mut zero = vec![0u8; ALIGNED_UNIT_LEN];
|
||||
zero[0] = 0xC0;
|
||||
assert!(
|
||||
!unit_key_opens(&zero, &right, BdTs),
|
||||
"no content proves nothing"
|
||||
);
|
||||
// Three content packets, the rest zero-ciphertext padding: too little to prove a key.
|
||||
let mut thin = enc.clone();
|
||||
thin[4 * BD_SOURCE_PACKET_BYTES..].fill(0);
|
||||
assert!(
|
||||
!unit_key_opens(&thin, &right, BdTs),
|
||||
"below the proof floor"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
//! `--features keyproof`, where the symbol is intentionally present).
|
||||
|
||||
/// With `keyproof` OFF, `aacs::content::unit_key_opens` must not exist:
|
||||
/// ```compile_fail
|
||||
/// ```compile_fail,E0425
|
||||
/// libfreemkv::aacs::content::unit_key_opens(
|
||||
/// &[0u8; 6144], &[0u8; 16], libfreemkv::disc::ContentFormat::BdTs);
|
||||
/// ```
|
||||
|
||||
Reference in New Issue
Block a user