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;
pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_SIZE || plain.len() < 10 {
return None;
}
let flags = (sector[FLAG_BYTE] >> 4) & 0x03;
if flags == 0 {
return None;
}
let crypted = §or[ENCRYPTED_START..];
let seed = §or[SEED_OFFSET..SEED_OFFSET + 5];
let mut buf = [0u8; 10];
for i in 0..10 {
if i >= crypted.len() || i >= plain.len() {
return None;
}
buf[i] = TAB1[crypted[i] as usize] ^ plain[i];
}
let mut result_key = [0u8; 5];
let mut found = false;
'outer: for i_try in 0u32..0x10000 {
let mut t1 = (i_try >> 8) | 0x100;
let mut t2 = i_try & 0xFF;
let mut t5: u32 = 0;
let mut t3: u32 = 0;
for &buf_byte in buf.iter().take(4) {
let t4 = TAB2[t2 as usize] ^ TAB3[t1 as usize];
t2 = t1 >> 1;
t1 = ((t1 & 1) << 8) ^ t4 as u32;
let t4_perm = TAB5[t4 as usize];
let mut t6 = buf_byte as u32;
if t5 > 0 {
t6 = (t6 + 0xFF) & 0xFF;
}
if t6 < t4_perm as u32 {
t6 += 0x100;
}
t6 -= t4_perm as u32;
t5 += t6 + t4_perm as u32;
let t6_inv = TAB4[t6 as usize & 0xFF];
t3 = t3.wrapping_shl(8) | t6_inv as u32;
t5 >>= 8;
}
let candidate = t3;
let mut valid = true;
for &buf_byte in buf.iter().skip(4) {
let t4 = TAB2[t2 as usize] ^ TAB3[t1 as usize];
t2 = t1 >> 1;
t1 = ((t1 & 1) << 8) ^ t4 as u32;
let t4_perm = TAB5[t4 as usize];
let t6 = ((((((t3 >> 8) ^ t3) >> 1) ^ t3) >> 3) ^ t3) >> 7;
t3 = t3.wrapping_shl(8) | (t6 & 0xFF);
let t6_perm = TAB4[(t6 & 0xFF) as usize];
t5 += t6_perm as u32 + t4_perm as u32;
if (t5 & 0xFF) as u8 != buf_byte {
valid = false;
break;
}
t5 >>= 8;
}
if !valid {
continue;
}
t3 = candidate;
let mut recovery_ok = true;
for _ in 0..4 {
let t1_byte = t3 & 0xFF;
t3 >>= 8;
let mut found_j = false;
for j in 0u32..256 {
t3 = (t3 & 0x1FFFF) | (j << 17);
let t6 = ((((((t3 >> 8) ^ t3) >> 1) ^ t3) >> 3) ^ t3) >> 7;
if (t6 & 0xFF) == t1_byte {
found_j = true;
break;
}
}
if !found_j {
recovery_ok = false;
break;
}
}
if !recovery_ok {
continue 'outer;
}
let t4 = (t3 >> 1).wrapping_sub(4);
for t5_off in 0u32..8 {
let val = t4.wrapping_add(t5_off);
if val.wrapping_mul(2).wrapping_add(8).wrapping_sub(val & 7) == t3 {
result_key[0] = (i_try >> 8) as u8;
result_key[1] = (i_try & 0xFF) as u8;
result_key[2] = (val & 0xFF) as u8;
result_key[3] = ((val >> 8) & 0xFF) as u8;
result_key[4] = ((val >> 16) & 0xFF) as u8;
found = true;
break;
}
}
if found {
break;
}
}
if !found {
return None;
}
result_key[0] ^= seed[0];
result_key[1] ^= seed[1];
result_key[2] ^= seed[2];
result_key[3] ^= seed[3];
result_key[4] ^= seed[4];
Some(result_key)
}
pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_SIZE {
return None;
}
let flags = (sector[FLAG_BYTE] >> 4) & 0x03;
if flags == 0 {
return None;
}
let mut patterns: Vec<[u8; 10]> = Vec::with_capacity(169);
for len_hi in 0u8..8 {
for len_lo_top in [0x00u8, 0x80, 0xFF] {
patterns.push([
0x00, 0x00, 0x01, 0xBE, len_hi, len_lo_top, 0xFF, 0xFF, 0xFF, 0xFF,
]);
}
}
for &sid in &[0xE0u8, 0xBD, 0xC0] {
for &flags1 in &[0x80u8, 0x81, 0x84, 0x85, 0x8C, 0x8D] {
for &flags2 in &[0x00u8, 0x05, 0x80, 0xC0] {
let hdr_len = if flags2 & 0x80 != 0 { 0x05u8 } else { 0x00 };
let pts0 = if flags2 & 0x80 != 0 { 0x21u8 } else { 0x00 };
for &len_hi in &[0x00u8, 0x07] {
patterns.push([
0x00, 0x00, 0x01, sid, len_hi, 0x00, flags1, flags2, hdr_len, pts0,
]);
}
}
}
}
patterns.push([0x00, 0x00, 0x01, 0xBB, 0x00, 0x12, 0x80, 0xC4, 0xE1, 0x04]);
for pattern in &patterns {
if let Some(key) = recover_title_key(sector, pattern) {
let mut test = sector.to_vec();
super::lfsr::descramble_sector(&key, &mut test);
if test[0x80] == 0x00 && test[0x81] == 0x00 && test[0x82] == 0x01 {
return Some(key);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crack_unscrambled_returns_none() {
let sector = vec![0u8; 2048];
assert!(crack_title_key(§or).is_none());
}
#[test]
fn crack_scrambled_sectors_never_overflow() {
for seed in 0u32..4 {
let mut sector = vec![0u8; SECTOR_SIZE];
sector[FLAG_BYTE] = 0x30; let mut x = seed.wrapping_mul(2_654_435_761).wrapping_add(1);
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) as u8;
}
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
let _ = recover_title_key(§or, &plain);
}
}
#[test]
fn crack_too_short_returns_none() {
let sector = vec![0u8; 100];
assert!(crack_title_key(§or).is_none());
}
#[test]
fn recover_needs_10_bytes_plain() {
let sector = vec![0u8; 2048];
let short_plain = [0u8; 5];
assert!(recover_title_key(§or, &short_plain).is_none());
}
#[test]
fn recover_rejects_sector_one_byte_short() {
let mut sector = vec![0u8; SECTOR_SIZE - 1];
sector[FLAG_BYTE] = 0x30;
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
assert!(recover_title_key(§or, &plain).is_none());
}
#[test]
fn recover_rejects_unscrambled_sector() {
let sector = vec![0x00u8; SECTOR_SIZE];
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
assert!(recover_title_key(§or, &plain).is_none());
}
#[test]
fn recover_high_flag_bits_are_not_scramble() {
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
for &flag in &[0x40u8, 0x80, 0xC0] {
let mut sector = vec![0x11u8; SECTOR_SIZE];
sector[FLAG_BYTE] = flag;
assert!(
recover_title_key(§or, &plain).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);
}
}
#[test]
#[ignore = "CSS crack/recover path is non-functional vs this crate's descrambler; \
see module docs. Regression gate for a future fix."]
fn css_crack_recovers_key_from_scrambled_sector() {
use super::super::lfsr::descramble_sector;
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let mut plaintext = vec![0x00u8; SECTOR_SIZE];
plaintext[0] = 0x00;
plaintext[1] = 0x00;
plaintext[2] = 0x01;
plaintext[3] = 0xBA;
plaintext[FLAG_BYTE] = 0x30;
plaintext[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
let exact_plain: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
plaintext[0x80..0x80 + 10].copy_from_slice(&exact_plain);
let original_plaintext = plaintext.clone();
descramble_sector(&title_key, &mut plaintext);
plaintext[FLAG_BYTE] = 0x30;
let cracked = crack_title_key(&plaintext);
assert!(
cracked.is_some(),
"crack_title_key returned None for a sector scrambled with a known key"
);
let cracked = cracked.unwrap();
let mut body = plaintext.clone();
descramble_sector(&cracked, &mut body);
assert_eq!(
&body[0x80..SECTOR_SIZE],
&original_plaintext[0x80..SECTOR_SIZE],
"crack_title_key key did not round-trip the body"
);
let recovered = recover_title_key(&plaintext, &exact_plain);
assert!(
recovered.is_some(),
"recover_title_key returned None for exact known plaintext"
);
let recovered = recovered.unwrap();
let mut body2 = plaintext.clone();
descramble_sector(&recovered, &mut body2);
assert_eq!(
&body2[0x80..SECTOR_SIZE],
&original_plaintext[0x80..SECTOR_SIZE],
"recover_title_key key did not round-trip the body"
);
}
}