use super::lfsr::descramble_sector;
use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
const SECTOR_SIZE: usize = 2048;
const ENCRYPTED_START: usize = 0x80; const SEED_OFFSET: usize = 0x54; const FLAG_BYTE: usize = 0x14;
fn recover_title_key_from_plain(
crypted: &[u8],
decrypted: &[u8],
seed: &[u8; 5],
) -> Option<[u8; 5]> {
if crypted.len() < 10 || decrypted.len() < 10 {
return None;
}
let mut buffer = [0u8; 10];
for (i, b) in buffer.iter_mut().enumerate() {
*b = TAB1[crypted[i] as usize] ^ decrypted[i];
}
let mut key = [0u8; 5];
let mut found = false;
for i_try in 0u32..0x1_0000 {
let mut i_t1 = (i_try >> 8) | 0x100;
let mut i_t2 = i_try & 0xff;
let mut i_t3: u32 = 0; let mut i_t5: u32 = 0;
for &b in buffer.iter().take(4) {
let i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32;
i_t2 = i_t1 >> 1;
i_t1 = ((i_t1 & 1) << 8) ^ i_t4;
let i_t4 = TAB5[i_t4 as usize] as u32;
let mut i_t6 = b as u32;
if i_t5 != 0 {
i_t6 = (i_t6 + 0xff) & 0xff;
}
if i_t6 < i_t4 {
i_t6 += 0x100;
}
i_t6 -= i_t4;
i_t5 += i_t6 + i_t4;
let i_t6 = TAB4[i_t6 as usize] as u32;
i_t3 = (i_t3 << 8) | i_t6;
i_t5 >>= 8;
}
let i_candidate = i_t3;
let mut i = 4usize;
while i < 10 {
let i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32;
i_t2 = i_t1 >> 1;
i_t1 = ((i_t1 & 1) << 8) ^ i_t4;
let i_t4 = TAB5[i_t4 as usize] as u32;
let mut i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xff;
i_t3 = (i_t3 << 8) | i_t6;
i_t6 = TAB4[i_t6 as usize] as u32;
i_t5 += i_t6 + i_t4;
if (i_t5 & 0xff) as u8 != buffer[i] {
break;
}
i_t5 >>= 8;
i += 1;
}
if i != 10 {
continue;
}
i_t3 = i_candidate;
for _ in 0..4 {
let i_t1_byte = i_t3 & 0xff;
i_t3 >>= 8;
for j in 0u32..256 {
i_t3 = (i_t3 & 0x1_ffff) | (j << 17);
let i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xff;
if i_t6 == i_t1_byte {
break;
}
}
}
let i_t4 = (i_t3 >> 1).wrapping_sub(4);
for i_t5 in 0u32..8 {
let val = i_t4.wrapping_add(i_t5);
if val.wrapping_mul(2).wrapping_add(8).wrapping_sub(val & 7) == i_t3 {
key[0] = (i_try >> 8) as u8;
key[1] = (i_try & 0xff) as u8;
key[2] = (val & 0xff) as u8;
key[3] = ((val >> 8) & 0xff) as u8;
key[4] = ((val >> 16) & 0xff) as u8;
found = true;
break;
}
}
if found {
break;
}
}
if found {
for (k, &s) in key.iter_mut().zip(seed.iter()) {
*k ^= s;
}
Some(key)
} else {
None
}
}
pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_SIZE || plain.len() < 10 {
return None;
}
if sector[FLAG_BYTE] & 0x30 == 0 {
return None;
}
let seed: [u8; 5] = [
sector[SEED_OFFSET],
sector[SEED_OFFSET + 1],
sector[SEED_OFFSET + 2],
sector[SEED_OFFSET + 3],
sector[SEED_OFFSET + 4],
];
let crypted = §or[ENCRYPTED_START..ENCRYPTED_START + 10];
let key = recover_title_key_from_plain(crypted, plain, &seed)?;
if descramble_matches(sector, &key, plain) {
Some(key)
} else {
None
}
}
fn descramble_matches(sector: &[u8], title: &[u8; 5], plain: &[u8]) -> bool {
let mut test = sector.to_vec();
test[FLAG_BYTE] |= 0x10; descramble_sector(title, &mut test);
let n = plain.len().min(SECTOR_SIZE - ENCRYPTED_START);
test[ENCRYPTED_START..ENCRYPTED_START + n] == plain[..n]
}
pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_SIZE {
return None;
}
if sector[FLAG_BYTE] & 0x30 == 0 {
return None;
}
let crack_t0 = std::time::Instant::now();
let result = crack_title_key_inner(sector);
let elapsed = crack_t0.elapsed();
if elapsed.as_secs_f64() > 2.0 {
tracing::warn!(
target: "freemkv::css",
elapsed_ms = elapsed.as_millis() as u64,
found = result.is_some(),
"css crack: single-sector recovery exceeded 2s (runaway guard)"
);
}
result
}
fn crack_title_key_inner(sector: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_SIZE || sector[FLAG_BYTE] & 0x30 == 0 {
return None;
}
let mut best_plen: usize = 0;
let mut best_p: usize = 0;
for i in 2usize..0x30 {
let mut j = i + 1;
while j < 0x80 && sector[0x7f - (j % i)] == sector[0x7f - j] {
if j > best_plen {
best_plen = j;
best_p = i;
}
j += 1;
}
}
if best_plen > 3 && best_p > 0 && best_plen / best_p >= 2 {
let seed: [u8; 5] = [
sector[SEED_OFFSET],
sector[SEED_OFFSET + 1],
sector[SEED_OFFSET + 2],
sector[SEED_OFFSET + 3],
sector[SEED_OFFSET + 4],
];
let cycles = best_plen / best_p;
let plain_start = 0x80 - cycles * best_p;
let crypted = §or[0x80..0x80 + 10];
let mut plain = [0u8; 10];
for (i, p) in plain.iter_mut().enumerate() {
*p = sector[plain_start + (i % best_p)];
}
if let Some(key) = recover_title_key_from_plain(crypted, &plain, &seed) {
if descramble_matches(sector, &key, &plain) {
return Some(key);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::super::lfsr::scramble_sector;
use super::*;
fn synth_sector(title_key: &[u8; 5], seed: &[u8; 5], plain: &[u8]) -> (Vec<u8>, Vec<u8>) {
let mut plaintext = vec![0u8; SECTOR_SIZE];
plaintext[0..4].copy_from_slice(&[0x00, 0x00, 0x01, 0xBA]);
plaintext[FLAG_BYTE] = 0x10;
plaintext[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(seed);
plaintext[ENCRYPTED_START..ENCRYPTED_START + plain.len()].copy_from_slice(plain);
let body = plaintext.clone();
scramble_sector(title_key, &mut plaintext);
(plaintext, body)
}
fn synth_periodic_sector(
title_key: &[u8; 5],
seed: &[u8; 5],
period: usize,
) -> (Vec<u8>, Vec<u8>) {
let mut plaintext = vec![0u8; SECTOR_SIZE];
plaintext[FLAG_BYTE] = 0x10;
const RUN_START: usize = 0x59;
let pat: Vec<u8> = (0..period)
.map(|k| (0xA0u8.wrapping_add(k as u8)) ^ 0x5A)
.collect();
for (i, b) in plaintext.iter_mut().enumerate().skip(RUN_START) {
*b = pat[i % period];
}
plaintext[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(seed);
let body = plaintext.clone();
scramble_sector(title_key, &mut plaintext);
(plaintext, body)
}
#[test]
fn crack_unscrambled_returns_none() {
let sector = vec![0u8; 2048];
assert!(crack_title_key(§or).is_none());
}
#[test]
fn crack_too_short_returns_none() {
let sector = vec![0u8; 100];
assert!(crack_title_key(§or).is_none());
}
#[test]
fn recover_needs_min_plain() {
let sector = vec![0u8; 2048];
let short_plain = [0u8; 4];
assert!(recover_title_key(§or, &short_plain).is_none());
}
const PES: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
#[test]
fn recover_round_trips_known_keys() {
let cases: &[([u8; 5], [u8; 5])] = &[
(
[0x42, 0x13, 0x37, 0xBE, 0xEF],
[0x11, 0x22, 0x33, 0x44, 0x55],
),
(
[0x01, 0x02, 0x03, 0x04, 0x05],
[0xDE, 0xAD, 0xBE, 0xEF, 0x42],
),
(
[0xFE, 0xDC, 0xBA, 0x98, 0x76],
[0x00, 0xFF, 0x80, 0x7F, 0x01],
),
(
[0x9A, 0x78, 0x56, 0x34, 0x12],
[0xA5, 0x5A, 0x0F, 0xF0, 0xCC],
),
(
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
[0x01, 0x01, 0x01, 0x01, 0x01],
),
];
for (title_key, seed) in cases {
let (mut sector, body) = synth_sector(title_key, seed, &PES);
let recovered =
recover_title_key(§or, &PES).expect("recover_title_key returned None");
descramble_sector(&recovered, &mut sector);
assert_eq!(
§or[ENCRYPTED_START..SECTOR_SIZE],
&body[ENCRYPTED_START..SECTOR_SIZE],
"recovered key did not descramble the full body for \
title={title_key:02x?} seed={seed:02x?}"
);
}
}
#[test]
fn crack_title_key_recovers_via_attack_pattern() {
for &period in &[2usize, 3, 5, 8, 16] {
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let seed = [0x11, 0x22, 0x33, 0x44, 0x55];
let (sector, body) = synth_periodic_sector(&title_key, &seed, period);
let cracked = crack_title_key(§or)
.unwrap_or_else(|| panic!("crack_title_key returned None for period {period}"));
let mut test = sector.clone();
descramble_sector(&cracked, &mut test);
assert_eq!(
&test[ENCRYPTED_START..SECTOR_SIZE],
&body[ENCRYPTED_START..SECTOR_SIZE],
"crack_title_key key did not round-trip the body (period {period})"
);
}
}
#[test]
fn recovered_key_descrambles_back_to_plaintext() {
let cases: &[([u8; 5], [u8; 5])] = &[
(
[0x42, 0x13, 0x37, 0xBE, 0xEF],
[0x11, 0x22, 0x33, 0x44, 0x55],
),
(
[0x9A, 0x78, 0x56, 0x34, 0x12],
[0xA5, 0x5A, 0x0F, 0xF0, 0xCC],
),
(
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
[0x01, 0x01, 0x01, 0x01, 0x01],
),
];
for (title_key, seed) in cases {
let (mut sector, body) = synth_sector(title_key, seed, &PES);
let recovered =
recover_title_key(§or, &PES).expect("recover_title_key returned None");
descramble_sector(&recovered, &mut sector);
assert_eq!(
§or[ENCRYPTED_START..SECTOR_SIZE],
&body[ENCRYPTED_START..SECTOR_SIZE],
"descramble with recovered key did not reproduce the body \
for title={title_key:02x?} seed={seed:02x?}"
);
}
}
#[test]
fn recover_rejects_sector_one_byte_short() {
let mut sector = vec![0u8; SECTOR_SIZE - 1];
sector[FLAG_BYTE] = 0x30;
assert!(recover_title_key(§or, &PES).is_none());
}
#[test]
fn recover_rejects_unscrambled_sector() {
let sector = vec![0x00u8; SECTOR_SIZE];
assert!(recover_title_key(§or, &PES).is_none());
}
#[test]
fn recover_high_flag_bits_are_not_scramble() {
for &flag in &[0x40u8, 0x80, 0xC0] {
let mut sector = vec![0x11u8; SECTOR_SIZE];
sector[FLAG_BYTE] = flag;
assert!(
recover_title_key(§or, &PES).is_none(),
"flag {flag:#04x} has scramble bits clear; recover must return None"
);
}
}
#[test]
fn crack_high_flag_bits_are_not_scramble() {
for &flag in &[0x40u8, 0x80, 0xC0] {
let mut sector = vec![0x11u8; SECTOR_SIZE];
sector[FLAG_BYTE] = flag;
assert!(
crack_title_key(§or).is_none(),
"flag {flag:#04x} clear scramble bits -> crack must return None"
);
}
}
#[test]
fn crack_rejects_sector_one_byte_short() {
let mut sector = vec![0u8; SECTOR_SIZE - 1];
if sector.len() > FLAG_BYTE {
sector[FLAG_BYTE] = 0x30;
}
assert!(crack_title_key(§or).is_none());
}
#[test]
fn crack_full_path_never_panics() {
for seed in 0u32..3 {
let mut sector = vec![0u8; SECTOR_SIZE];
sector[FLAG_BYTE] = 0x30;
let mut x = seed.wrapping_mul(2_654_435_761).wrapping_add(7);
for b in sector.iter_mut().skip(0x80) {
x = x.wrapping_mul(1_103_515_245).wrapping_add(12_345);
*b = (x >> 16) as u8;
}
for (i, b) in sector[SEED_OFFSET..SEED_OFFSET + 5].iter_mut().enumerate() {
*b = (seed.wrapping_add(i as u32) ^ 0xA5) as u8;
}
let _ = crack_title_key(§or);
}
}
}