FMTS: resolve the index key map from one forensic keyserver query

The keyserver protocol now returns all 32 index keys as an array for a
forensic content sample (and a single-element array for plain content).
resolve_fmts_key_map sends one forensic batch and maps array element i to
segment index i+1, replacing the per-index blind-probe collection loop
that repeatedly hit the key service. Segment/index parsing and the
aligned-unit content classification are reworked to support this:

- rename variant_select -> index_select (per-index, not per-variant)
- content classification moves to is_clean(buf, ContentFormat) so the
  unit selector emits only units the key service accepts
- segment.rs: parse IndividualSegment.tbl index tags + SPN ranges,
  build contiguous LBA key ranges from the resolved 32-key array
- decrypt/decorator plumbing for the resolved per-index keys

Fail loud (FmtsKeyMissing) when the forensic query returns < 32 keys or
any segment index stays unresolved.
This commit is contained in:
Matthew Jackson
2026-07-16 19:41:44 -07:00
parent ccb7cafc68
commit edc60582ec
14 changed files with 1087 additions and 289 deletions
+15 -9
View File
@@ -131,12 +131,15 @@ fn aacs_decrypt_unit_roundtrip() {
}
// Verify it looks encrypted (body TS syncs scrambled)
assert!(aacs::content::ts_sync_destroyed(&plain));
assert!(!aacs::content::is_clean(
&plain,
libfreemkv::disc::ContentFormat::BdTs
));
// Now decrypt
aacs::content::decrypt_unit(&mut plain, &unit_key);
assert!(
!aacs::content::ts_sync_destroyed(&plain),
aacs::content::is_clean(&plain, libfreemkv::disc::ContentFormat::BdTs),
"decrypted unit should read as clear (TS syncs restored)"
);
@@ -254,9 +257,9 @@ fn aacs_vuk_derivation_roundtrip() {
assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
}
/// Test: aacs_ts_sync_destroyed detects scrambled units via the raw TS syncs.
/// Test: `is_clean` distinguishes clean vs scrambled units via the TS proof floor.
#[test]
fn aacs_ts_sync_destroyed_detection() {
fn aacs_is_clean_detection() {
// 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::content::ALIGNED_UNIT_LEN];
@@ -266,7 +269,7 @@ fn aacs_ts_sync_destroyed_detection() {
off += 192;
}
assert!(
!aacs::content::ts_sync_destroyed(&clear),
aacs::content::is_clean(&clear, libfreemkv::disc::ContentFormat::BdTs),
"clear unit (syncs intact) must not be scrambled"
);
@@ -275,21 +278,21 @@ fn aacs_ts_sync_destroyed_detection() {
flagged[0] = 0xC0; // copy-control bits
flagged[7] = 0xC0; // TSC bits
assert!(
!aacs::content::ts_sync_destroyed(&flagged),
aacs::content::is_clean(&flagged, libfreemkv::disc::ContentFormat::BdTs),
"flag bits must not be read as encryption"
);
// A scrambled body (syncs destroyed) → scrambled.
let scrambled = vec![0x99u8; aacs::content::ALIGNED_UNIT_LEN];
assert!(
aacs::content::ts_sync_destroyed(&scrambled),
!aacs::content::is_clean(&scrambled, libfreemkv::disc::ContentFormat::BdTs),
"unit with no intact TS syncs must read as scrambled"
);
// Too short
let short = vec![0xFFu8; 100];
assert!(
!aacs::content::ts_sync_destroyed(&short),
aacs::content::is_clean(&short, libfreemkv::disc::ContentFormat::BdTs),
"short buffer should not be detected"
);
}
@@ -312,7 +315,10 @@ fn aacs_clear_unit_reports_not_encrypted() {
// CPI bits (byte 0) CLEAR → the authoritative gate reads this as plaintext.
unit[0] &= 0x3F;
assert!(!aacs::content::ts_sync_destroyed(&unit));
assert!(aacs::content::is_clean(
&unit,
libfreemkv::disc::ContentFormat::BdTs
));
assert!(
!aacs::content::aacs_unit_encrypted(&unit, libfreemkv::disc::ContentFormat::BdTs),
"CPI-clear unit reports not-encrypted; the caller never decrypts it"
+12 -3
View File
@@ -108,16 +108,25 @@ fn aacs_encryption_flag_detection() {
off += 192;
}
// Encryption is the scrambled body (TS syncs destroyed), NOT a flag bit.
assert!(!aacs::content::ts_sync_destroyed(&unit));
assert!(aacs::content::is_clean(
&unit,
libfreemkv::disc::ContentFormat::BdTs
));
// Flag bits on a synced unit do not make it look encrypted.
unit[0] = 0xC0;
unit[7] = 0xC0;
assert!(!aacs::content::ts_sync_destroyed(&unit));
assert!(aacs::content::is_clean(
&unit,
libfreemkv::disc::ContentFormat::BdTs
));
// Scrambled body (syncs gone) → encrypted.
let scrambled = vec![0x99u8; aacs::content::ALIGNED_UNIT_LEN];
assert!(aacs::content::ts_sync_destroyed(&scrambled));
assert!(!aacs::content::is_clean(
&scrambled,
libfreemkv::disc::ContentFormat::BdTs
));
}
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.