pub mod auth;
pub mod crack;
pub mod lfsr;
pub(crate) mod tables;
use crate::disc::Extent;
use crate::drive::Drive;
use crate::sector::SectorSource;
#[derive(Debug, Clone)]
pub struct CssState {
pub title_key: [u8; 5],
}
pub struct CssContext<'a> {
pub drive: Option<&'a mut Drive>,
pub auth_lba: Option<u32>,
pub reader: Option<&'a mut dyn SectorSource>,
pub extents: Option<&'a [Extent]>,
}
pub fn resolve(ctx: &mut CssContext<'_>) -> Option<CssState> {
if let (Some(drive), Some(lba)) = (ctx.drive.as_deref_mut(), ctx.auth_lba) {
if let Ok(title_key) = auth::authenticate_and_read_title_key(drive, lba) {
return Some(CssState { title_key });
}
}
if let (Some(reader), Some(extents)) = (ctx.reader.as_deref_mut(), ctx.extents) {
return crack_key(reader, extents);
}
None
}
pub fn crack_key(reader: &mut dyn SectorSource, extents: &[Extent]) -> Option<CssState> {
let mut tried = 0u32;
let max_tries = 50_000;
let mut buf = vec![0u8; 2048];
for ext in extents {
let mut i = 0;
while i < ext.sector_count && tried < max_tries {
tried += 1;
if reader
.read_sectors(ext.start_lba + i, 1, &mut buf, true)
.is_ok()
&& is_scrambled(&buf)
{
if let Some(key) = crack::crack_title_key(&buf) {
return Some(CssState { title_key: key });
}
}
i += 1;
}
if tried >= max_tries {
break;
}
}
None
}
pub fn descramble_sector(state: &CssState, sector: &mut [u8]) {
lfsr::descramble_sector(&state.title_key, sector);
}
pub fn is_scrambled(sector: &[u8]) -> bool {
sector.len() >= 2048 && (sector[0x14] >> 4) & 0x03 != 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{Error, Result};
#[test]
fn is_scrambled_short_buffer_is_false_no_panic() {
assert!(!is_scrambled(&[]));
assert!(!is_scrambled(&[0u8; 20])); assert!(!is_scrambled(&[0xFFu8; 2047])); }
#[test]
fn is_scrambled_uses_bits_4_5_only() {
let mut s = vec![0u8; 2048];
for (flag, expected) in [
(0x00u8, false),
(0x10, true),
(0x20, true),
(0x30, true),
(0x40, false),
(0x80, false),
(0xC0, false),
(0xFF, true), ] {
s[0x14] = flag;
assert_eq!(
is_scrambled(&s),
expected,
"flag byte {flag:#04x} scramble detection"
);
}
}
#[test]
fn is_scrambled_exact_sector_length_accepted() {
let mut s = vec![0u8; 2048];
s[0x14] = 0x30;
assert!(is_scrambled(&s), "exactly 2048 bytes must be eligible");
}
struct MockSource {
reads: std::cell::RefCell<Vec<u32>>,
flag_byte: u8,
fail_all: bool,
}
impl MockSource {
fn new(flag_byte: u8) -> Self {
Self {
reads: std::cell::RefCell::new(Vec::new()),
flag_byte,
fail_all: false,
}
}
}
impl SectorSource for MockSource {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
self.reads.borrow_mut().push(lba);
if self.fail_all {
return Err(Error::DecryptFailed);
}
let n = count as usize * 2048;
let end = n.min(buf.len());
for b in buf[..end].iter_mut() {
*b = 0;
}
if buf.len() > 0x14 {
buf[0x14] = self.flag_byte;
}
Ok(n)
}
}
#[test]
fn crack_key_caps_total_tries_at_50000() {
let mut src = MockSource::new(0x00); let extents = [Extent {
start_lba: 0,
sector_count: 200_000,
}];
let res = crack_key(&mut src, &extents);
assert!(res.is_none(), "clear sectors yield no key");
assert_eq!(
src.reads.borrow().len(),
50_000,
"scan must stop at the 50_000-sector budget"
);
}
#[test]
fn crack_key_budget_is_shared_across_extents() {
let mut src = MockSource::new(0x00);
let extents = [
Extent {
start_lba: 0,
sector_count: 40_000,
},
Extent {
start_lba: 100_000,
sector_count: 40_000,
},
];
let res = crack_key(&mut src, &extents);
assert!(res.is_none());
assert_eq!(
src.reads.borrow().len(),
50_000,
"the 50_000 budget is shared across all extents"
);
}
#[test]
fn crack_key_scans_from_extent_start_lba() {
let mut src = MockSource::new(0x00);
let extents = [Extent {
start_lba: 5_000,
sector_count: 4,
}];
let _ = crack_key(&mut src, &extents);
let reads = src.reads.borrow();
assert_eq!(
&reads[..],
&[5_000, 5_001, 5_002, 5_003],
"sequential scan from start_lba"
);
}
#[test]
fn crack_key_continues_past_read_errors() {
let mut src = MockSource::new(0x30);
src.fail_all = true;
let extents = [Extent {
start_lba: 0,
sector_count: 10,
}];
let res = crack_key(&mut src, &extents);
assert!(res.is_none());
assert_eq!(
src.reads.borrow().len(),
10,
"read errors must not abort the scan"
);
}
#[test]
fn crack_key_empty_extent_reads_nothing() {
let mut src = MockSource::new(0x30);
let extents = [Extent {
start_lba: 42,
sector_count: 0,
}];
let res = crack_key(&mut src, &extents);
assert!(res.is_none());
assert_eq!(
src.reads.borrow().len(),
0,
"zero-sector extent reads nothing"
);
}
#[test]
fn crack_key_no_extents_is_none() {
let mut src = MockSource::new(0x30);
let res = crack_key(&mut src, &[]);
assert!(res.is_none());
assert_eq!(src.reads.borrow().len(), 0);
}
}