//! CSS drive authentication — full key hierarchy. //! //! Protocol: //! 1. Bus authentication (challenge-response) → bus key //! 2. Read disc key block (READ DVD STRUCTURE) → XOR with bus key → decrypt with player keys → disc key //! 3. Read title key (REPORT KEY format 0x04) → XOR with bus key → decrypt with disc key → title key //! //! Based on libdvdcss (VideoLAN) and Stevenson 1999 analysis. use crate::drive::Drive; use crate::error::{Error, Result}; // ── Player keys (from libdvdcss, Stevenson's PlayerKey cracker) ─────────── const PLAYER_KEYS: [[u8; 5]; 31] = [ [0x01, 0xaf, 0xe3, 0x12, 0x80], [0x12, 0x11, 0xca, 0x04, 0x3b], [0x14, 0x0c, 0x9e, 0xd0, 0x09], [0x14, 0x71, 0x35, 0xba, 0xe2], [0x1a, 0xa4, 0x33, 0x21, 0xa6], [0x26, 0xec, 0xc4, 0xa7, 0x4e], [0x2c, 0xb2, 0xc1, 0x09, 0xee], [0x2f, 0x25, 0x9e, 0x96, 0xdd], [0x33, 0x2f, 0x49, 0x6c, 0xe0], [0x35, 0x5b, 0xc1, 0x31, 0x0f], [0x36, 0x67, 0xb2, 0xe3, 0x85], [0x39, 0x3d, 0xf1, 0xf1, 0xbd], [0x3b, 0x31, 0x34, 0x0d, 0x91], [0x45, 0xed, 0x28, 0xeb, 0xd3], [0x48, 0xb7, 0x6c, 0xce, 0x69], [0x4b, 0x65, 0x0d, 0xc1, 0xee], [0x4c, 0xbb, 0xf5, 0x5b, 0x23], [0x51, 0x67, 0x67, 0xc5, 0xe0], [0x53, 0x94, 0xe1, 0x75, 0xbf], [0x57, 0x2c, 0x8b, 0x31, 0xae], [0x63, 0xdb, 0x4c, 0x5b, 0x4a], [0x7b, 0x1e, 0x5e, 0x2b, 0x57], [0x85, 0xf3, 0x85, 0xa0, 0xe0], [0xab, 0x1e, 0xe7, 0x7b, 0x72], [0xab, 0x36, 0xe3, 0xeb, 0x76], [0xb1, 0xb8, 0xf9, 0x38, 0x03], [0xb8, 0x5d, 0xd8, 0x53, 0xbd], [0xbf, 0x92, 0xc3, 0xb0, 0xe2], [0xcf, 0x1a, 0xb2, 0xf8, 0x0a], [0xec, 0xa0, 0xcf, 0xb3, 0xff], [0xfc, 0x95, 0xa9, 0x87, 0x35], ]; // ── CryptKey tables ─────────────────────────────────────────────────────── const CRYPT_TAB0: [u8; 256] = [ 0xB7, 0xF4, 0x82, 0x57, 0xDA, 0x4D, 0xDB, 0xE2, 0x2F, 0x52, 0x1A, 0xA8, 0x68, 0x5A, 0x8A, 0xFF, 0xFB, 0x0E, 0x6D, 0x35, 0xF7, 0x5C, 0x76, 0x12, 0xCE, 0x25, 0x79, 0x29, 0x39, 0x62, 0x08, 0x24, 0xA5, 0x85, 0x7B, 0x56, 0x01, 0x23, 0x68, 0xCF, 0x0A, 0xE2, 0x5A, 0xED, 0x3D, 0x59, 0xB0, 0xA9, 0xB0, 0x2C, 0xF2, 0xB8, 0xEF, 0x32, 0xA9, 0x40, 0x80, 0x71, 0xAF, 0x1E, 0xDE, 0x8F, 0x58, 0x88, 0xB8, 0x3A, 0xD0, 0xFC, 0xC4, 0x1E, 0xB5, 0xA0, 0xBB, 0x3B, 0x0F, 0x01, 0x7E, 0x1F, 0x9F, 0xD9, 0xAA, 0xB8, 0x3D, 0x9D, 0x74, 0x1E, 0x25, 0xDB, 0x37, 0x56, 0x8F, 0x16, 0xBA, 0x49, 0x2B, 0xAC, 0xD0, 0xBD, 0x95, 0x20, 0xBE, 0x7A, 0x28, 0xD0, 0x51, 0x64, 0x63, 0x1C, 0x7F, 0x66, 0x10, 0xBB, 0xC4, 0x56, 0x1A, 0x04, 0x6E, 0x0A, 0xEC, 0x9C, 0xD6, 0xE8, 0x9A, 0x7A, 0xCF, 0x8C, 0xDB, 0xB1, 0xEF, 0x71, 0xDE, 0x31, 0xFF, 0x54, 0x3E, 0x5E, 0x07, 0x69, 0x96, 0xB0, 0xCF, 0xDD, 0x9E, 0x47, 0xC7, 0x96, 0x8F, 0xE4, 0x2B, 0x59, 0xC6, 0xEE, 0xB9, 0x86, 0x9A, 0x64, 0x84, 0x72, 0xE2, 0x5B, 0xA2, 0x96, 0x58, 0x99, 0x50, 0x03, 0xF5, 0x38, 0x4D, 0x02, 0x7D, 0xE7, 0x7D, 0x75, 0xA7, 0xB8, 0x67, 0x87, 0x84, 0x3F, 0x1D, 0x11, 0xE5, 0xFC, 0x1E, 0xD3, 0x83, 0x16, 0xA5, 0x29, 0xF6, 0xC7, 0x15, 0x61, 0x29, 0x1A, 0x43, 0x4F, 0x9B, 0xAF, 0xC5, 0x87, 0x34, 0x6C, 0x0F, 0x3B, 0xA8, 0x1D, 0x45, 0x58, 0x25, 0xDC, 0xA8, 0xA3, 0x3B, 0xD1, 0x79, 0x1B, 0x48, 0xF2, 0xE9, 0x93, 0x1F, 0xFC, 0xDB, 0x2A, 0x90, 0xA9, 0x8A, 0x3D, 0x39, 0x18, 0xA3, 0x8E, 0x58, 0x6C, 0xE0, 0x12, 0xBB, 0x25, 0xCD, 0x71, 0x22, 0xA2, 0x64, 0xC6, 0xE7, 0xFB, 0xAD, 0x94, 0x77, 0x04, 0x9A, 0x39, 0xCF, 0x7C, ]; const CRYPT_TAB1: [u8; 256] = [ 0x8C, 0x47, 0xB0, 0xE1, 0xEB, 0xFC, 0xEB, 0x56, 0x10, 0xE5, 0x2C, 0x1A, 0x5D, 0xEF, 0xBE, 0x4F, 0x08, 0x75, 0x97, 0x4B, 0x0E, 0x25, 0x8E, 0x6E, 0x39, 0x5A, 0x87, 0x53, 0xC4, 0x1F, 0xF4, 0x5C, 0x4E, 0xE6, 0x99, 0x30, 0xE0, 0x42, 0x88, 0xAB, 0xE5, 0x85, 0xBC, 0x8F, 0xD8, 0x3C, 0x54, 0xC9, 0x53, 0x47, 0x18, 0xD6, 0x06, 0x5B, 0x41, 0x2C, 0x67, 0x1E, 0x41, 0x74, 0x33, 0xE2, 0xB4, 0xE0, 0x23, 0x29, 0x42, 0xEA, 0x55, 0x0F, 0x25, 0xB4, 0x24, 0x2C, 0x99, 0x13, 0xEB, 0x0A, 0x0B, 0xC9, 0xF9, 0x63, 0x67, 0x43, 0x2D, 0xC7, 0x7D, 0x07, 0x60, 0x89, 0xD1, 0xCC, 0xE7, 0x94, 0x77, 0x74, 0x9B, 0x7E, 0xD7, 0xE6, 0xFF, 0xBB, 0x68, 0x14, 0x1E, 0xA3, 0x25, 0xDE, 0x3A, 0xA3, 0x54, 0x7B, 0x87, 0x9D, 0x50, 0xCA, 0x27, 0xC3, 0xA4, 0x50, 0x91, 0x27, 0xD4, 0xB0, 0x82, 0x41, 0x97, 0x79, 0x94, 0x82, 0xAC, 0xC7, 0x8E, 0xA5, 0x4E, 0xAA, 0x78, 0x9E, 0xE0, 0x42, 0xBA, 0x28, 0xEA, 0xB7, 0x74, 0xAD, 0x35, 0xDA, 0x92, 0x60, 0x7E, 0xD2, 0x0E, 0xB9, 0x24, 0x5E, 0x39, 0x4F, 0x5E, 0x63, 0x09, 0xB5, 0xFA, 0xBF, 0xF1, 0x22, 0x55, 0x1C, 0xE2, 0x25, 0xDB, 0xC5, 0xD8, 0x50, 0x03, 0x98, 0xC4, 0xAC, 0x2E, 0x11, 0xB4, 0x38, 0x4D, 0xD0, 0xB9, 0xFC, 0x2D, 0x3C, 0x08, 0x04, 0x5A, 0xEF, 0xCE, 0x32, 0xFB, 0x4C, 0x92, 0x1E, 0x4B, 0xFB, 0x1A, 0xD0, 0xE2, 0x3E, 0xDA, 0x6E, 0x7C, 0x4D, 0x56, 0xC3, 0x3F, 0x42, 0xB1, 0x3A, 0x23, 0x4D, 0x6E, 0x84, 0x56, 0x68, 0xF4, 0x0E, 0x03, 0x64, 0xD0, 0xA9, 0x92, 0x2F, 0x8B, 0xBC, 0x39, 0x9C, 0xAC, 0x09, 0x5E, 0xEE, 0xE5, 0x97, 0xBF, 0xA5, 0xCE, 0xFA, 0x28, 0x2C, 0x6D, 0x4F, 0xEF, 0x77, 0xAA, 0x1B, 0x79, 0x8E, 0x97, 0xB4, 0xC3, 0xF4, ]; const CRYPT_TAB2: [u8; 256] = [ 0xB7, 0x75, 0x81, 0xD5, 0xDC, 0xCA, 0xDE, 0x66, 0x23, 0xDF, 0x15, 0x26, 0x62, 0xD1, 0x83, 0x77, 0xE3, 0x97, 0x76, 0xAF, 0xE9, 0xC3, 0x6B, 0x8E, 0xDA, 0xB0, 0x6E, 0xBF, 0x2B, 0xF1, 0x19, 0xB4, 0x95, 0x34, 0x48, 0xE4, 0x37, 0x94, 0x5D, 0x7B, 0x36, 0x5F, 0x65, 0x53, 0x07, 0xE2, 0x89, 0x11, 0x98, 0x85, 0xD9, 0x12, 0xC1, 0x9D, 0x84, 0xEC, 0xA4, 0xD4, 0x88, 0xB8, 0xFC, 0x2C, 0x79, 0x28, 0xD8, 0xDB, 0xB3, 0x1E, 0xA2, 0xF9, 0xD0, 0x44, 0xD7, 0xD6, 0x60, 0xEF, 0x14, 0xF4, 0xF6, 0x31, 0xD2, 0x41, 0x46, 0x67, 0x0A, 0xE1, 0x58, 0x27, 0x43, 0xA3, 0xF8, 0xE0, 0xC8, 0xBA, 0x5A, 0x5C, 0x80, 0x6C, 0xC6, 0xF2, 0xE8, 0xAD, 0x7D, 0x04, 0x0D, 0xB9, 0x3C, 0xC2, 0x25, 0xBD, 0x49, 0x63, 0x8C, 0x9F, 0x51, 0xCE, 0x20, 0xC5, 0xA1, 0x50, 0x92, 0x2D, 0xDD, 0xBC, 0x8D, 0x4F, 0x9A, 0x71, 0x2F, 0x30, 0x1D, 0x73, 0x39, 0x13, 0xFB, 0x1A, 0xCB, 0x24, 0x59, 0xFE, 0x05, 0x96, 0x57, 0x0F, 0x1F, 0xCF, 0x54, 0xBE, 0xF5, 0x06, 0x1B, 0xB2, 0x6D, 0xD3, 0x4D, 0x32, 0x56, 0x21, 0x33, 0x0B, 0x52, 0xE7, 0xAB, 0xEB, 0xA6, 0x74, 0x00, 0x4C, 0xB1, 0x7F, 0x82, 0x99, 0x87, 0x0E, 0x5E, 0xC0, 0x8F, 0xEE, 0x6F, 0x55, 0xF3, 0x7E, 0x08, 0x90, 0xFA, 0xB6, 0x64, 0x70, 0x47, 0x4A, 0x17, 0xA7, 0xB5, 0x40, 0x8A, 0x38, 0xE5, 0x68, 0x3E, 0x8B, 0x69, 0xAA, 0x9B, 0x42, 0xA5, 0x10, 0x01, 0x35, 0xFD, 0x61, 0x9E, 0xE6, 0x16, 0x9C, 0x86, 0xED, 0xCD, 0x2E, 0xFF, 0xC4, 0x5B, 0xA0, 0xAE, 0xCC, 0x4B, 0x3B, 0x03, 0xBB, 0x1C, 0x2A, 0xAC, 0x0C, 0x3F, 0x93, 0xC7, 0x72, 0x7A, 0x09, 0x22, 0x3D, 0x45, 0x78, 0xA9, 0xA8, 0xEA, 0xC9, 0x6A, 0xF7, 0x29, 0x91, 0xF0, 0x02, 0x18, 0x3A, 0x4E, 0x7C, ]; const CRYPT_TAB3: [u8; 288] = [ 0x73, 0x51, 0x95, 0xE1, 0x12, 0xE4, 0xC0, 0x58, 0xEE, 0xF2, 0x08, 0x1B, 0xA9, 0xFA, 0x98, 0x4C, 0xA7, 0x33, 0xE2, 0x1B, 0xA7, 0x6D, 0xF5, 0x30, 0x97, 0x1D, 0xF3, 0x02, 0x60, 0x5A, 0x82, 0x0F, 0x91, 0xD0, 0x9C, 0x10, 0x39, 0x7A, 0x83, 0x85, 0x3B, 0xB2, 0xB8, 0xAE, 0x0C, 0x09, 0x52, 0xEA, 0x1C, 0xE1, 0x8D, 0x66, 0x4F, 0xF3, 0xDA, 0x92, 0x29, 0xB9, 0xD5, 0xC5, 0x77, 0x47, 0x22, 0x53, 0x14, 0xF7, 0xAF, 0x22, 0x64, 0xDF, 0xC6, 0x72, 0x12, 0xF3, 0x75, 0xDA, 0xD7, 0xD7, 0xE5, 0x02, 0x9E, 0xED, 0xDA, 0xDB, 0x4C, 0x47, 0xCE, 0x91, 0x06, 0x06, 0x6D, 0x55, 0x8B, 0x19, 0xC9, 0xEF, 0x8C, 0x80, 0x1A, 0x0E, 0xEE, 0x4B, 0xAB, 0xF2, 0x08, 0x5C, 0xE9, 0x37, 0x26, 0x5E, 0x9A, 0x90, 0x00, 0xF3, 0x0D, 0xB2, 0xA6, 0xA3, 0xF7, 0x26, 0x17, 0x48, 0x88, 0xC9, 0x0E, 0x2C, 0xC9, 0x02, 0xE7, 0x18, 0x05, 0x4B, 0xF3, 0x39, 0xE1, 0x20, 0x02, 0x0D, 0x40, 0xC7, 0xCA, 0xB9, 0x48, 0x30, 0x57, 0x67, 0xCC, 0x06, 0xBF, 0xAC, 0x81, 0x08, 0x24, 0x7A, 0xD4, 0x8B, 0x19, 0x8E, 0xAC, 0xB4, 0x5A, 0x0F, 0x73, 0x13, 0xAC, 0x9E, 0xDA, 0xB6, 0xB8, 0x96, 0x5B, 0x60, 0x88, 0xE1, 0x81, 0x3F, 0x07, 0x86, 0x37, 0x2D, 0x79, 0x14, 0x52, 0xEA, 0x73, 0xDF, 0x3D, 0x09, 0xC8, 0x25, 0x48, 0xD8, 0x75, 0x60, 0x9A, 0x08, 0x27, 0x4A, 0x2C, 0xB9, 0xA8, 0x8B, 0x8A, 0x73, 0x62, 0x37, 0x16, 0x02, 0xBD, 0xC1, 0x0E, 0x56, 0x54, 0x3E, 0x14, 0x5F, 0x8C, 0x8F, 0x6E, 0x75, 0x1C, 0x07, 0x39, 0x7B, 0x4B, 0xDB, 0xD3, 0x4B, 0x1E, 0xC8, 0x7E, 0xFE, 0x3E, 0x72, 0x16, 0x83, 0x7D, 0xEE, 0xF5, 0xCA, 0xC5, 0x18, 0xF9, 0xD8, 0x68, 0xAB, 0x38, 0x85, 0xA8, 0xF0, 0xA1, 0x73, 0x9F, 0x5D, 0x19, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x72, 0x39, 0x25, 0x67, 0x26, 0x6D, 0x71, 0x36, 0x77, 0x3C, 0x20, 0x62, 0x23, 0x68, 0x74, 0xC3, 0x82, 0xC9, 0x15, 0x57, 0x16, 0x5D, 0x81, ]; const VARIANTS: [u8; 32] = [ 0xB7, 0x74, 0x85, 0xD0, 0xCC, 0xDB, 0xCA, 0x73, 0x03, 0xFE, 0x31, 0x03, 0x52, 0xE0, 0xB7, 0x42, 0x63, 0x16, 0xF2, 0x2A, 0x79, 0x52, 0xFF, 0x1B, 0x7A, 0x11, 0xCA, 0x1A, 0x9B, 0x40, 0xAD, 0x01, ]; const SECRET: [u8; 5] = [0x55, 0xD6, 0xC4, 0xC5, 0x28]; const PERM_CHALLENGE: [[usize; 10]; 3] = [ [1, 3, 0, 7, 5, 2, 9, 6, 4, 8], [6, 1, 9, 3, 8, 5, 7, 4, 0, 2], [4, 0, 3, 5, 7, 2, 8, 6, 1, 9], ]; const PERM_VARIANT: [[u8; 32]; 2] = [ [ 0x0A, 0x08, 0x0E, 0x0C, 0x0B, 0x09, 0x0F, 0x0D, 0x1A, 0x18, 0x1E, 0x1C, 0x1B, 0x19, 0x1F, 0x1D, 0x02, 0x00, 0x06, 0x04, 0x03, 0x01, 0x07, 0x05, 0x12, 0x10, 0x16, 0x14, 0x13, 0x11, 0x17, 0x15, ], [ 0x12, 0x1A, 0x16, 0x1E, 0x02, 0x0A, 0x06, 0x0E, 0x10, 0x18, 0x14, 0x1C, 0x00, 0x08, 0x04, 0x0C, 0x13, 0x1B, 0x17, 0x1F, 0x03, 0x0B, 0x07, 0x0F, 0x11, 0x19, 0x15, 0x1D, 0x01, 0x09, 0x05, 0x0D, ], ]; // ── SCSI constants ──────────────────────────────────────────────────────── const SCSI_READ_DVD_STRUCTURE: u8 = 0xAD; // ── Public API ──────────────────────────────────────────────────────────── /// Perform CSS bus authentication only. pub fn authenticate(drive: &mut Drive) -> Result<()> { let (_, _) = bus_auth(drive)?; Ok(()) } /// Full CSS key extraction: bus auth → disc key → title key. pub fn authenticate_and_read_title_key(drive: &mut Drive, lba: u32) -> Result<[u8; 5]> { // Session 1: bus auth → disc key (AGID consumed by READ_DVD_STRUCTURE) let (agid, bus_key) = bus_auth(drive)?; let disc_key = read_disc_key(drive, agid, &bus_key)?; // Session 2: fresh bus auth → title key (needs separate AGID) let (agid2, bus_key2) = bus_auth(drive)?; let encrypted_title = read_raw_title_key(drive, agid2, lba)?; // Decrypt title key: XOR with bus key, then decrypt with disc key let mut title_key = [0u8; 5]; for i in 0..5 { title_key[i] = encrypted_title[i] ^ bus_key2[i]; } if title_key == [0u8; 5] { return Ok(title_key); } let title_key = super::lfsr::decrypt_key(0xFF, &disc_key, &title_key); Ok(title_key) } // ── Step 1: Bus Authentication ──────────────────────────────────────────── fn bus_auth(drive: &mut Drive) -> Result<(u8, [u8; 5])> { let scsi = drive.scsi_mut(); // Invalidate all AGIDs via REPORT KEY format 0x3F for agid in 0..4u8 { let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_REPORT_KEY; // alloc_len = 0 (no data transfer) cdb[10] = (agid << 6) | 0x3F; let mut buf = [0u8; 8]; let _ = scsi.execute( &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ); } // Allocate AGID let mut buf = [0u8; 8]; scsi.execute( &report_key_cdb(0, 0x00, 8), crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ) .map_err(|_| Error::CssAuthFailed)?; let agid = (buf[7] >> 6) & 0x03; // Host sends challenge let host_challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut hc_buf = [0u8; 16]; hc_buf[0] = 0x00; hc_buf[1] = 0x0E; for i in 0..10 { hc_buf[4 + i] = host_challenge[9 - i]; } scsi.execute( &send_key_cdb(agid, 0x01, 16), crate::scsi::DataDirection::ToDevice, &mut hc_buf, 5_000, ) .map_err(|_| Error::CssAuthFailed)?; // Get Key1 from drive let mut dk_buf = [0u8; 12]; scsi.execute( &report_key_cdb(agid, 0x02, 12), crate::scsi::DataDirection::FromDevice, &mut dk_buf, 5_000, ) .map_err(|_| Error::CssAuthFailed)?; let mut key1 = [0u8; 5]; for i in 0..5 { key1[i] = dk_buf[4 + (4 - i)]; } // Brute-force variant (0-31) let mut variant: Option = None; for v in 0..32u8 { if crypt_key(0, v, &host_challenge) == key1 { variant = Some(v); break; } } let variant = variant.ok_or(Error::CssAuthFailed)?; // Get drive challenge let mut dc_buf = [0u8; 16]; scsi.execute( &report_key_cdb(agid, 0x01, 16), crate::scsi::DataDirection::FromDevice, &mut dc_buf, 5_000, ) .map_err(|_| Error::CssAuthFailed)?; let mut drive_challenge = [0u8; 10]; for i in 0..10 { drive_challenge[i] = dc_buf[4 + (9 - i)]; } // Compute Key2 and send it let key2 = crypt_key(1, variant, &drive_challenge); let mut hk_buf = [0u8; 12]; hk_buf[0] = 0x00; hk_buf[1] = 0x0A; for i in 0..5 { hk_buf[4 + i] = key2[4 - i]; } scsi.execute( &send_key_cdb(agid, 0x03, 12), crate::scsi::DataDirection::ToDevice, &mut hk_buf, 5_000, ) .map_err(|_| Error::CssAuthFailed)?; // Bus key = CryptKey(2, variant, key1 || key2) let mut combined = [0u8; 10]; combined[..5].copy_from_slice(&key1); combined[5..].copy_from_slice(&key2); let bus_key = crypt_key(2, variant, &combined); Ok((agid, bus_key)) } // ── Step 2: Disc Key ────────────────────────────────────────────────────── fn read_disc_key(drive: &mut Drive, agid: u8, bus_key: &[u8; 5]) -> Result<[u8; 5]> { let scsi = drive.scsi_mut(); // READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes let alloc_len: u16 = 2048 + 4; let mut cdb = [0u8; 12]; cdb[0] = SCSI_READ_DVD_STRUCTURE; // bytes 2-5: address = 0 cdb[6] = 0; // layer cdb[7] = 0x02; // format = disc key cdb[8] = (alloc_len >> 8) as u8; cdb[9] = alloc_len as u8; cdb[10] = agid << 6; let mut buf = vec![0u8; alloc_len as usize]; let dvd_result = scsi.execute( &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ); dvd_result.map_err(|_| Error::CssAuthFailed)?; // Disc key block starts at offset 4 (skip 4-byte header) let disc_key_block = &mut buf[4..4 + 2048]; // XOR with reversed bus key (per libdvdcss) for (i, byte) in disc_key_block.iter_mut().enumerate() { *byte ^= bus_key[4 - (i % 5)]; } // Try each player key against each of 408 disc key entries. // Each entry in the block is the disc key encrypted with a specific player key. // We try all known player keys and verify by checking that two different // entries produce the same disc key. let mut candidates: Vec<([u8; 5], usize, usize)> = Vec::new(); // (disc_key, pk_idx, pos) for (pk_idx, player_key) in PLAYER_KEYS.iter().enumerate() { for pos in 0..408 { let offset = pos * 5; if offset + 5 > disc_key_block.len() { break; } let mut enc = [0u8; 5]; enc.copy_from_slice(&disc_key_block[offset..offset + 5]); let candidate = super::lfsr::decrypt_key(0x00, player_key, &enc); // Check if any previous candidate matches (same disc key from different entry/pk) for (prev, _, _) in &candidates { if *prev == candidate { return Ok(candidate); } } candidates.push((candidate, pk_idx, pos)); } } Err(Error::CssAuthFailed) } // ── Step 3: Title Key ───────────────────────────────────────────────────── /// Read the raw (bus-encrypted) title key bytes from the drive. fn read_raw_title_key(drive: &mut Drive, agid: u8, lba: u32) -> Result<[u8; 5]> { let scsi = drive.scsi_mut(); let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_REPORT_KEY; cdb[2] = (lba >> 24) as u8; cdb[3] = (lba >> 16) as u8; cdb[4] = (lba >> 8) as u8; cdb[5] = lba as u8; cdb[8] = 0x00; cdb[9] = 0x0C; cdb[10] = (agid << 6) | 0x04; let mut buf = [0u8; 12]; let result = scsi.execute( &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ); result.map_err(|_| Error::CssAuthFailed)?; let mut key = [0u8; 5]; for i in 0..5 { key[i] = buf[5 + (4 - i)]; } Ok(key) } #[allow(dead_code)] fn read_title_key( drive: &mut Drive, agid: u8, lba: u32, bus_key: &[u8; 5], disc_key: &[u8; 5], ) -> Result<[u8; 5]> { let scsi = drive.scsi_mut(); let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_REPORT_KEY; cdb[2] = (lba >> 24) as u8; cdb[3] = (lba >> 16) as u8; cdb[4] = (lba >> 8) as u8; cdb[5] = lba as u8; cdb[8] = 0x00; cdb[9] = 0x0C; cdb[10] = (agid << 6) | 0x04; let mut buf = [0u8; 12]; let tk_result = scsi.execute( &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ); tk_result.map_err(|_| Error::CssAuthFailed)?; // Title key at bytes 5..10, byte-reversed let mut title_key = [0u8; 5]; for i in 0..5 { title_key[i] = buf[5 + (4 - i)]; } // XOR with reversed bus key (same pattern as disc key block) for i in 0..5 { title_key[i] ^= bus_key[4 - i]; } // Check for null key (title not encrypted) if title_key == [0u8; 5] { return Ok(title_key); } // Decrypt with disc key (invert=0xFF for title keys) let title_key = super::lfsr::decrypt_key(0xFF, disc_key, &title_key); Ok(title_key) } // ── CSSCryptKey ─────────────────────────────────────────────────────────── /// Exposed for testing only. pub fn test_crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] { crypt_key(key_type, variant, challenge) } fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] { let perm = &PERM_CHALLENGE[key_type]; let mut scratch = [0u8; 10]; for i in 0..10 { scratch[i] = challenge[perm[i]]; } let css_variant = match key_type { 0 => variant as usize, 1 => PERM_VARIANT[0][variant as usize] as usize, _ => PERM_VARIANT[1][variant as usize] as usize, }; let cse = VARIANTS[css_variant] ^ CRYPT_TAB2[css_variant]; let mut tmp1 = [0u8; 5]; for i in 0..5 { tmp1[i] = scratch[5 + i] ^ SECRET[i] ^ CRYPT_TAB2[i]; } let mut lfsr0: u32 = ((tmp1[0] as u32) << 17) | ((tmp1[1] as u32) << 9) | (((tmp1[2] as u32) & !7) << 1) | 8 | (tmp1[2] as u32 & 7); let mut lfsr1: u32 = ((tmp1[3] as u32) << 9) | 0x100 | (tmp1[4] as u32); let mut bits = [0u8; 30]; let mut carry: u32 = 0; for idx in (0..30).rev() { let mut val: u8 = 0; for bit in 0..8u8 { let lfsr0_out = ((lfsr0 >> 24) ^ (lfsr0 >> 21) ^ (lfsr0 >> 20) ^ (lfsr0 >> 12)) & 1; lfsr0 = ((lfsr0 << 1) | lfsr0_out) & 0x1FFFFFF; let lfsr1_out = ((lfsr1 >> 16) ^ (lfsr1 >> 2)) & 1; lfsr1 = ((lfsr1 << 1) | lfsr1_out) & 0x1FFFF; let combined = ((!lfsr1_out) & 1) + carry + ((!lfsr0_out) & 1); carry = (combined >> 1) & 1; val |= ((combined & 1) as u8) << bit; } bits[idx] = val; } let mut tmp1 = [scratch[0], scratch[1], scratch[2], scratch[3], scratch[4]]; let mut tmp2 = [0u8; 5]; // Round 1: bits[25..29] ^ scratch -> tmp1 (term from original scratch) { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[25 + i] ^ tmp1[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; tmp1[i] = CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term; term = scratch[i]; // original challenge, NOT modified tmp1 } tmp1[4] ^= tmp1[0]; } // Round 2 { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[20 + i] ^ tmp1[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; tmp2[i] = CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term; term = tmp1[i]; } tmp2[4] ^= tmp2[0]; } // Round 3 (uses CRYPT_TAB0) { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[15 + i] ^ tmp2[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; let idx3 = (CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term) as usize; tmp1[i] = CRYPT_TAB0[idx3] ^ CRYPT_TAB2[idx3]; term = tmp2[i]; } tmp1[4] ^= tmp1[0]; } // Round 4 (uses CRYPT_TAB0) { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[10 + i] ^ tmp1[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; let idx3 = (CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term) as usize; tmp2[i] = CRYPT_TAB0[idx3] ^ CRYPT_TAB2[idx3]; term = tmp1[i]; } tmp2[4] ^= tmp2[0]; } // Round 5 { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[5 + i] ^ tmp2[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; tmp1[i] = CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term; term = tmp2[i]; } tmp1[4] ^= tmp1[0]; } // Round 6 let mut key = [0u8; 5]; { let mut term: u8 = 0; for i in (0..5usize).rev() { let idx = (bits[i] ^ tmp1[i]) as usize; let idx2 = (CRYPT_TAB1[idx] ^ (!CRYPT_TAB2[idx]) ^ cse) as usize; key[i] = CRYPT_TAB2[idx2] ^ CRYPT_TAB3[idx2] ^ term; term = tmp1[i]; } } key } // ── SCSI CDB builders ──────────────────────────────────────────────────── fn report_key_cdb(agid: u8, format: u8, alloc_len: u16) -> [u8; 12] { let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_REPORT_KEY; cdb[8] = (alloc_len >> 8) as u8; cdb[9] = alloc_len as u8; cdb[10] = (agid << 6) | (format & 0x3F); cdb } fn send_key_cdb(agid: u8, format: u8, param_len: u16) -> [u8; 12] { let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_SEND_KEY; cdb[8] = (param_len >> 8) as u8; cdb[9] = param_len as u8; cdb[10] = (agid << 6) | (format & 0x3F); cdb } // ── Tests ───────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; #[test] fn crypt_key_is_deterministic() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; for v in 0..32u8 { let r1 = crypt_key(0, v, &challenge); let r2 = crypt_key(0, v, &challenge); assert_eq!(r1, r2); } } #[test] fn crypt_key_varies_by_variant() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; assert_ne!(crypt_key(0, 0, &challenge), crypt_key(0, 1, &challenge)); } #[test] fn crypt_key_varies_by_type() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; assert_ne!(crypt_key(0, 5, &challenge), crypt_key(1, 5, &challenge)); } #[test] fn crypt_key_nonzero() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; for v in 0..32u8 { assert_ne!(crypt_key(0, v, &challenge), [0u8; 5]); } } #[test] fn player_keys_count() { assert_eq!(PLAYER_KEYS.len(), 31); } }