fix(css): no provable key is a hard failure, matching AACS

descramble_region descrambled with the key a sector's own crib had just
proven stale, whenever the re-crack from that sector also failed. The
clear header is not scrambled, so it survives intact: the sector still
opens with a valid pack start and passes every structural check the PS
demuxer applies. Only the payload is corrupted — exactly where nothing
looks. Ok(0) dropped, exit 0.

CSS has no external key source. The title key comes only from cracking
the data, so on a READABLE sector "no key" is not a missing input, it is
recovery failing on bytes we can see. That should never happen, and when
it does the answer is not to emit something.

Now Error::DecryptFailed — the same verdict the AACS path already gives
for a unit no held key opens. Both alternatives to failing are bad data
reported as success: descrambled with a rejected key it is garbage
behind a valid header, and passed through untouched it is ciphertext
where plaintext is meant to be.

WHY IT WAS POSSIBLE, which matters more than the fix:

There is no single place that owns "what do we do when there is no key".
decrypt_sectors_impl looks like the central dispatch, but its AACS arm
is a `return Err` stub — AACS decrypts entirely through
decrypt_sectors_mapped, a separate top-level path. So CSS decided its
own policy inside css/, AACS decided in decrypt.rs and mux/resolve.rs,
and nothing held them to the same answer. The asymmetry was not an
oversight; it was structurally permitted.

How a disc decrypts is one process — resolve a key for this data, apply
it, refuse if it cannot be proven. Only the resolve-and-apply step is
scheme-specific. Filed as a task: the policy belongs in one orchestrator
with the schemes supplying only what genuinely differs.

Two tests changed rather than added, both of which pinned the old
behaviour: the unit test asserted the sector was descrambled, and the
integration test asserted the scramble flag was cleared, which is what
descrambling-with-any-key does. Neither established that the result was
CORRECT — the fourth bad-test shape.
This commit is contained in:
Matthew Jackson
2026-07-30 19:10:32 -07:00
parent b2b611fa3b
commit 30bea12392
3 changed files with 119 additions and 16 deletions
+25 -10
View File
@@ -24,20 +24,35 @@ fn decrypt_sectors_with_none_keys_is_noop() {
/// Test: decrypt_sectors with CSS keys descrambles sectors.
#[test]
fn decrypt_sectors_with_css_keys_works() {
fn css_decrypt_of_an_unkeyable_sector_fails_instead_of_emitting_data() {
// A scrambled sector whose header is uniformly periodic yields a crib, so
// the supplied key IS validated — and this arbitrary key is not the right
// one, so the crib check rejects it and the re-crack from this synthetic
// body finds nothing.
//
// CSS has no external key source: the title key comes only from cracking
// the data. So "no key" on a readable sector is recovery failing on bytes
// we can see, not a missing input — the same condition AACS answers with
// DecryptFailed rather than applying a neighbouring unit's key. Emitting
// the sector either way is bad data reported as success: descrambled with
// the rejected key it is garbage behind an intact clear header, and passed
// through untouched it is ciphertext where plaintext is meant to be.
//
// This test previously asserted the scramble flag was cleared, which pinned
// the old behaviour of descrambling with whatever key happened to be held.
let mut sector = vec![0xFFu8; 2048];
sector[0x14] |= 0x30; // CSS scramble flag, bits 4-5
// Set CSS scramble flag (bits 4-5 of byte 0x14)
sector[0x14] |= 0x30;
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF]; // Not used - defined later
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let mut keys = DecryptKeys::Css { title_key };
// Descramble (CSS uses same operation for encrypt/decrypt)
libfreemkv::decrypt::decrypt_sectors(&mut sector, &mut keys, 0).unwrap();
// Flag should be cleared
assert_eq!(sector[0x14] & 0x30, 0x00, "CSS flag should be cleared");
let err = libfreemkv::decrypt::decrypt_sectors(&mut sector, &mut keys, 0)
.expect_err("an unkeyable CSS sector must fail loud");
assert_eq!(
err.code(),
libfreemkv::error::Error::DecryptFailed.code(),
"CSS and AACS must give the SAME verdict for 'no provable key'"
);
}
/// Test: AACS unit encryption detection works.