//! CSS drive bus-authentication — read-unlock primitive. //! //! A CSS-enforcing DVD drive refuses to return scrambled sectors until a //! CSS bus-auth handshake has set its Authentication Success Flag (ASF=1). //! [`unlock_css_reads`] runs that bus-auth challenge-response (which is what //! actually opens scrambled-sector reads), then a best-effort, non-fatal //! disc-key REPORT KEY. The bytes are NOT used as keys: the descramble title //! key is recovered keylessly by the Stevenson known-plaintext attack (see //! [`super::crack_key`]). mod error; use crate::css::error::{Error, Result}; use crate::scsi::ScsiTransport; // ── 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; 256] = [ 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, ]; 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, ], ]; // ── Public API ──────────────────────────────────────────────────────────── /// CSS bus-auth **unlock** primitive. /// /// Runs the bus-auth challenge-response (which sets the drive's ASF=1 and is /// what actually unlocks scrambled-sector reads), then a best-effort, /// non-fatal disc-key REPORT KEY. The title-key REPORT KEY is NOT issued: it /// is unnecessary (the descramble key is recovered keylessly by the Stevenson /// attack in [`super::crack_key`]) and its hard failure on some USB bridges /// used to abort the whole unlock (the 7014 bug). The bytes are discarded. pub fn unlock_css_reads(scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> { let t0 = std::time::Instant::now(); tracing::info!(target: "freemkv::css", phase = "unlock_css_reads", lba, "begin"); let r = unlock_css_reads_inner(scsi, lba); tracing::info!( target: "freemkv::css", phase = "unlock_css_reads", lba, ok = r.is_ok(), elapsed_ms = t0.elapsed().as_millis() as u64, "end" ); r } /// The DVD unlocker (registry name `"DVD"`) — the DVD peer of the firmware and /// AACS-cert unlockers in the uniform [`crate::Unlocker`] registry. It removes /// the DVD scrambled-read barrier (drive ASF=1) via bus-auth and learns no VID /// or bus key — the descramble key is recovered keylessly downstream (the /// Stevenson attack). Named for the medium it unlocks (DVD), not the CSS /// scheme: the bus-auth is required to read a CSS-protected DVD at all, whether /// or not any given sector turns out to be scrambled. Lives in the `css` module /// beside the CSS-scheme primitives it drives. pub struct DvdUnlocker; impl DvdUnlocker { pub fn new() -> Self { DvdUnlocker } } impl Default for DvdUnlocker { fn default() -> Self { Self::new() } } impl crate::Unlocker for DvdUnlocker { fn name(&self) -> &'static str { // User-facing unlocker label. This unlocker's job is the DVD // read-enablement bus-auth (it clears the drive's scrambled-read // barrier and learns no key) — a property of the DVD medium, NOT of // whether the content happens to be CSS-scrambled. Reporting it as // "DVD" is honest: on any DVD the bus-auth ran; the CSS descramble // itself is keyless and handled downstream, so it is not a separate // "did an unlocker run" signal. "DVD" } /// CSS removes the scrambled-sector barrier (a bus-level concern); it /// provides no drive features. Self-guards against the hardware (below), so /// it declines cleanly when the consumer iterates it on a non-DVD. fn unlock_bus( &self, scsi: &mut dyn ScsiTransport, _ctx: &crate::UnlockCtx, ) -> std::result::Result { // Self-guard against the hardware — do NOT trust the caller-declared // DiscKind alone. If the drive does not report a DVD profile, refuse // (NotApplicable) WITHOUT issuing any CSS CDB, so a mis-routed // Blu-ray/UHD is never sent CSS bus-auth. if !mounted_disc_is_dvd(scsi) { tracing::debug!( target: "freemkv::css", phase = "dvd_unlocker_not_dvd", "DvdUnlocker invoked on a non-DVD profile; refusing (NotApplicable)" ); return Err(crate::UnlockError::NotApplicable); } // The bus-auth handshake is what unlocks scrambled-sector reads; the lba // is not consumed by the unlock primitive (the disc-key REPORT KEY is // best-effort). CSS yields neither a Volume ID nor an AACS bus key. unlock_css_reads(scsi, 0)?; Ok(crate::Unlocked::default()) } } /// Transport-level "is the mounted disc a DVD?" probe (GET CONFIGURATION /// current-profile, DVD family `0x0010..=0x001F`). Lets the DvdUnlocker /// self-verify against the drive instead of trusting the caller's DiscKind. fn mounted_disc_is_dvd(scsi: &mut dyn ScsiTransport) -> bool { // RT=0: the 8-byte feature header carries the Current Profile in bytes 6-7. let cdb = [ crate::scsi::SCSI_GET_CONFIGURATION, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, ]; let mut buf = [0u8; 8]; match scsi.execute( &cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000, ) { Ok(r) if r.bytes_transferred >= 8 => { let profile = ((buf[6] as u16) << 8) | buf[7] as u16; (0x0010..=0x001F).contains(&profile) } _ => false, } } fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()> { tracing::debug!(target: "freemkv::css", "css unlock: begin"); // The bus-auth challenge-response sets the drive's Authentication Success // Flag (ASF=1), which is what opens scrambled-sector reads. This is the // ONLY step required to unlock reads; a failure here is fatal — we // genuinely cannot read scrambled sectors. let agid = establish_authenticated_session(scsi).inspect_err(|e| { tracing::warn!(target: "freemkv::css", error_code = e.code(), "css unlock: bus authentication failed"); })?; tracing::debug!(target: "freemkv::css", agid, "css unlock: bus authentication ok"); // Disc-key REPORT KEY: issued BEST-EFFORT for any firmware that ties part // of its read-unlock to it. The bytes are unused (the descramble key is // recovered keylessly) and a failure is NON-FATAL — the gate is already // open from bus-auth. This replaces the title-key REPORT KEY, whose hard // failure used to abort the whole unlock (the 7014 bug on USB bridges). if let Err(e) = read_disc_key(scsi, agid) { tracing::debug!(target: "freemkv::css", error_code = e.code(), "css unlock: disc-key REPORT KEY skipped (non-fatal)"); } tracing::debug!(target: "freemkv::css", "css unlock: ok"); Ok(()) } // ── Step 1: Bus Authentication ──────────────────────────────────────────── /// Run the CSS bus-authentication challenge-response (invalidate AGIDs → /// allocate AGID → host challenge → brute-force the variant → drive challenge → /// send host key). Completing the handshake sets the drive's Authentication /// Success Flag (ASF=1) — which is the ENTIRE purpose: it unlocks /// scrambled-sector reads. Returns the negotiated AGID (the caller needs it for /// the best-effort disc-key REPORT KEY). The CSS bus key is intentionally NOT /// derived: descrambling is keyless (the Stevenson known-plaintext attack), so /// the bus key has no consumer. fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result { // 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. The spec wants a fresh per-session random nonce, // not a fixed constant — a predictable challenge weakens the bus-auth // handshake. let mut host_challenge = [0u8; 10]; { use rand::RngCore; rand::thread_rng().fill_bytes(&mut host_challenge); } 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)?; // The authenticated session (ASF=1) is now established — scrambled-sector // reads are unlocked, which is the only thing we needed. The CSS bus key // would be CryptKey(2, variant, key1 || key2), but it has no consumer // (descrambling is keyless via the Stevenson attack), so it is not derived. Ok(agid) } // ── Step 2: Disc Key ────────────────────────────────────────────────────── /// Issue READ DVD STRUCTURE format 0x02 (Copyright Information — opcode 0xAD, /// NOT the REPORT KEY 0xA4 disc-key block) purely for the bus-auth unlock side /// effect. The returned block contents are not used — the descramble title key /// is recovered keylessly elsewhere, so the genuine disc-key REPORT KEY is /// intentionally skipped. (If a drive is ever found where bus-auth alone does /// not open scrambled reads, a real REPORT KEY format 0x02 belongs here.) fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> { // READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes let alloc_len: u16 = 2048 + 4; let mut cdb = [0u8; 12]; cdb[0] = crate::scsi::SCSI_READ_DISC_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)?; Ok(()) } // ── CSSCryptKey ─────────────────────────────────────────────────────────── fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] { // key_type indexes PERM_CHALLENGE ([_;3]); variant indexes // VARIANTS/PERM_VARIANT ([_;32]). All internal callers pass key_type in // 0..3 and variant in 0..32; the asserts document the contract for the // pub(crate) test entry point test_crypt_key and turn a would-be // out-of-bounds panic into an explicit precondition violation. debug_assert!(key_type < 3, "crypt_key: key_type out of range"); debug_assert!((variant as usize) < 32, "crypt_key: variant out of range"); 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::*; /// SECURITY REGRESSION GUARD: no instrumentation in libfreemkv may emit /// raw key material. Scan every source file for a `tracing` field that /// binds a forbidden key name to a value-producing expression (`= expr` /// or `%expr` / `?expr`). The only allowed forms are a string literal /// (e.g. `disc_key = ""`) or a `_fp` fingerprint field. /// /// This is a source-scan test (not a runtime capture) so it stays cheap /// and catches re-introductions at compile/CI time. #[test] fn no_key_bytes_in_instrumentation() { use std::path::Path; // Forbidden field names whose VALUES must never be logged. const FORBIDDEN: &[&str] = &[ "title_key", "disc_key", "unit_key", "vuk", "player_key", "bus_key", ]; fn scan_dir(dir: &Path, forbidden: &[&str], violations: &mut Vec) { let entries = match std::fs::read_dir(dir) { Ok(e) => e, Err(_) => return, }; for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { scan_dir(&path, forbidden, violations); continue; } if path.extension().and_then(|e| e.to_str()) != Some("rs") { continue; } let src = match std::fs::read_to_string(&path) { Ok(s) => s, Err(_) => continue, }; for (lineno, line) in src.lines().enumerate() { let trimmed = line.trim_start(); // Only inspect tracing instrumentation lines. if !(trimmed.contains("tracing::") || trimmed.starts_with("debug!") || trimmed.starts_with("info!") || trimmed.starts_with("warn!") || trimmed.starts_with("trace!") || trimmed.starts_with("error!")) { continue; } // This guard test itself contains the forbidden names. if path.file_name().and_then(|n| n.to_str()) == Some("auth.rs") && line.contains("FORBIDDEN") { continue; } for &name in forbidden { // A fingerprint field (`_fp = ...`) is allowed. // Match `` followed by optional fingerprint // suffix then `=` and a value that is NOT a string // literal redaction marker. if let Some(idx) = line.find(name) { let after = &line[idx + name.len()..]; let after = after.trim_start(); // `_fp` / `_id` etc. are safe. if after.starts_with('_') { continue; } // Must be a field binding `name = ...`. let Some(rest) = after.strip_prefix('=') else { continue; }; let rest = rest.trim_start(); // Redaction string literal is the only allowed value. if rest.starts_with('"') { continue; } // Anything else (`%expr`, `?expr`, bare expr) leaks bytes. violations.push(format!( "{}:{}: forbidden key field `{}` logged with a value: {}", path.display(), lineno + 1, name, line.trim() )); } } } } } // Scan this crate's `src` plus the sibling workspace crates so the // key-material logging guard covers every crate that can reach the // CSS/AACS internals, not just libfreemkv. Missing sibling dirs (e.g. // when building the crate standalone) are simply skipped. let manifest = Path::new(env!("CARGO_MANIFEST_DIR")); let workspace = manifest.parent().unwrap_or(manifest); let mut violations = Vec::new(); scan_dir(&manifest.join("src"), FORBIDDEN, &mut violations); for sibling in ["autorip", "freemkv", "freemkv-keysources"] { let dir = workspace.join(sibling).join("src"); if dir.is_dir() { scan_dir(&dir, FORBIDDEN, &mut violations); } } assert!( violations.is_empty(), "key material logged in instrumentation:\n{}", violations.join("\n") ); } #[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]); } } // ── CSS constant-table integrity ─────────────────────────────────────── /// Each PERM_CHALLENGE row is a permutation of indices 0..10 (it reorders /// the 10 challenge bytes). A non-permutation would drop/duplicate /// challenge bytes, weakening or corrupting the bus key derivation. /// /// Grounding: crypt_key does `scratch[i] = challenge[perm[i]]` for i in /// 0..10 — perm must be a bijection on 0..10 to use every challenge byte /// exactly once. /// Mutation: change PERM_CHALLENGE[0] entry `9` to `8` (duplicate) -> the /// "covers 0..10" assert fires. #[test] fn perm_challenge_rows_are_permutations() { for (row, perm) in PERM_CHALLENGE.iter().enumerate() { let mut seen = [false; 10]; for &idx in perm.iter() { assert!(idx < 10, "PERM_CHALLENGE[{row}] index {idx} out of range"); assert!(!seen[idx], "PERM_CHALLENGE[{row}] duplicates index {idx}"); seen[idx] = true; } assert!( seen.iter().all(|&b| b), "PERM_CHALLENGE[{row}] misses an index" ); } } /// Each PERM_VARIANT row maps the 32 variants to 32 distinct 5-bit values /// (it is a permutation of 0..32). key_type 1 uses PERM_VARIANT[0], /// key_type 2 uses PERM_VARIANT[1] to pick the css_variant; a collision /// would make two variants indistinguishable. /// /// Grounding: `css_variant = PERM_VARIANT[k][variant]` then indexes /// VARIANTS[css_variant] (0..32). /// Mutation: set PERM_VARIANT[0][1] = PERM_VARIANT[0][0] -> duplicate /// assert fires; also any value >= 32 would later index VARIANTS OOB. #[test] fn perm_variant_rows_are_permutations_of_0_31() { for (row, perm) in PERM_VARIANT.iter().enumerate() { let mut seen = [false; 32]; for &v in perm.iter() { let v = v as usize; assert!(v < 32, "PERM_VARIANT[{row}] value {v} out of 0..32"); assert!(!seen[v], "PERM_VARIANT[{row}] duplicates {v}"); seen[v] = true; } assert!( seen.iter().all(|&b| b), "PERM_VARIANT[{row}] misses a value" ); } } // ── crypt_key behaviour ──────────────────────────────────────────────── /// crypt_key result depends on every challenge byte. The challenge is /// permuted into `scratch` and folded through the LFSR seeding and the 6 /// XOR rounds. Flipping any single challenge byte must change the output. /// /// Grounding: scratch[i]=challenge[perm[i]] for all 10 i, and scratch /// seeds both LFSRs (bytes 5..10 via tmp1) and the round terms (bytes /// 0..5). /// Mutation: in `scratch[i] = challenge[perm[i]]` replace with /// `challenge[i]` for a perm that drops a byte — or hardcode one scratch /// entry — and some challenge byte stops mattering; this fails. #[test] fn crypt_key_depends_on_every_challenge_byte() { let base: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let base_out = crypt_key(0, 5, &base); for i in 0..10 { let mut c = base; c[i] ^= 0x55; assert_ne!( crypt_key(0, 5, &c), base_out, "flipping challenge byte {i} did not change the bus-key derivation" ); } } /// crypt_key(0, v, ..) must produce a DISTINCT result for each of the 32 /// variants on a fixed challenge. bus_auth brute-forces the variant by /// matching crypt_key(0, v, host_challenge) == key1; if two variants /// collided, the wrong variant could be selected and the whole auth /// derail. /// /// Grounding: variant selects css_variant -> VARIANTS[css_variant] -> cse, /// which feeds every round; distinct variants give distinct cse-driven /// keys in practice. /// Mutation: make `cse` ignore the variant (e.g. `let cse = 0`) -> all 32 /// outputs collapse to one value; the distinctness assert fires. #[test] fn crypt_key_type0_distinct_per_variant() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut outs = Vec::new(); for v in 0..32u8 { let k = crypt_key(0, v, &challenge); assert!( !outs.contains(&k), "variant {v} collides with an earlier variant" ); outs.push(k); } } /// crypt_key enforces its documented precondition `key_type < 3` via /// debug_assert (active in test builds). A key_type of 3 would index /// PERM_CHALLENGE (len 3) out of bounds; the assert turns that into an /// explicit precondition panic. /// /// Grounding: `debug_assert!(key_type < 3, ...)`; PERM_CHALLENGE has 3 /// rows (indices 0,1,2). /// Mutation: delete the debug_assert AND the match-arm guard — but the /// match `_ =>` arm would then index PERM_CHALLENGE[3] OOB and panic /// differently; with the assert in place this test pins the contract. #[test] #[should_panic] fn crypt_key_rejects_out_of_range_key_type() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let _ = crypt_key(3, 0, &challenge); } /// crypt_key enforces `variant < 32` via debug_assert. A variant of 32 /// would index VARIANTS / PERM_VARIANT (len 32) out of bounds. /// /// Grounding: `debug_assert!((variant as usize) < 32, ...)`. /// Mutation: removing the assert makes this index VARIANTS[32] (still a /// panic, but unguarded); the assert documents/enforces the contract. #[test] #[should_panic] fn crypt_key_rejects_out_of_range_variant() { let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let _ = crypt_key(0, 32, &challenge); } // ── SCSI CDB builders (MMC REPORT KEY / SEND KEY layout) ─────────────── /// report_key_cdb encodes a 12-byte MMC REPORT KEY (opcode 0xA4) CDB: /// byte 0 = operation code 0xA4 /// bytes 8-9 = allocation length, big-endian /// byte 10 = (AGID << 6) | (key_format & 0x3F) /// All other bytes are zero. /// /// Grounding: MMC REPORT KEY CDB; the AGID is the top 2 bits of byte 10, /// key format the low 6 bits. /// Mutation: change `(alloc_len >> 8)` to `alloc_len` for byte 8 (lose the /// big-endian split) -> byte 8/9 assert fails. Change `agid << 6` to /// `agid << 5` -> the AGID-position assert fails. #[test] fn report_key_cdb_matches_mmc_layout() { let cdb = report_key_cdb(0b10, 0x04, 0x010C); // AGID=2, format=0x04, len=268 assert_eq!(cdb[0], 0xA4, "REPORT KEY opcode"); assert_eq!(cdb[8], 0x01, "alloc_len high byte (big-endian)"); assert_eq!(cdb[9], 0x0C, "alloc_len low byte"); assert_eq!( cdb[10], (0b10 << 6) | 0x04, "AGID in bits 6-7, format in bits 0-5" ); // Every other byte must be zero. for (i, &b) in cdb.iter().enumerate() { if ![0, 8, 9, 10].contains(&i) { assert_eq!(b, 0, "CDB byte {i} must be zero"); } } assert_eq!(cdb.len(), 12, "REPORT KEY is a 12-byte CDB"); } /// The key format field is masked to 6 bits: a format with high bits set /// must not corrupt the AGID. report_key_cdb(0, 0xFF, _) -> byte 10 low 6 /// bits = 0x3F, AGID = 0. /// /// Grounding: `(agid << 6) | (format & 0x3F)`. /// Mutation: drop the `& 0x3F` mask -> 0xFF would overwrite the AGID bits; /// byte 10 would be 0xFF not 0x3F, this fails. #[test] fn report_key_cdb_masks_format_to_6_bits() { let cdb = report_key_cdb(0, 0xFF, 8); assert_eq!(cdb[10], 0x3F, "format masked to 6 bits, AGID stays 0"); } /// send_key_cdb encodes a 12-byte MMC SEND KEY (opcode 0xA3) CDB with the /// parameter-list length at bytes 8-9 (big-endian) and AGID/format at byte /// 10. /// /// Grounding: MMC SEND KEY CDB layout. /// Mutation: change opcode to SCSI_REPORT_KEY -> opcode assert fails; /// swap bytes 8/9 -> length assert fails. #[test] fn send_key_cdb_matches_mmc_layout() { let cdb = send_key_cdb(0b11, 0x03, 0x000C); // AGID=3, format=3, param_len=12 assert_eq!(cdb[0], 0xA3, "SEND KEY opcode"); assert_eq!(cdb[8], 0x00, "param_len high byte"); assert_eq!(cdb[9], 0x0C, "param_len low byte"); assert_eq!( cdb[10], (0b11 << 6) | 0x03, "AGID bits 6-7, format bits 0-5" ); assert_eq!(cdb.len(), 12); } /// Allocation length larger than 255 must split across bytes 8 (high) and /// 9 (low) — a 16-bit big-endian field. report_key_cdb with alloc_len /// 0x0804 (2052, the disc-key block size used in read_disc_key) -> byte 8 /// = 0x08, byte 9 = 0x04. /// /// Grounding: read_disc_key uses `alloc_len = 2048 + 4 = 2052 = 0x0804` /// and writes `cdb[8] = (alloc_len >> 8); cdb[9] = alloc_len`. /// Mutation: write only byte 9 (`cdb[9] = alloc_len as u8`) without byte 8 /// -> the drive sees a 4-byte transfer, truncating the disc-key block; /// this asserts the high byte is present. #[test] fn report_key_cdb_alloc_len_is_16bit_big_endian() { let cdb = report_key_cdb(0, 0x00, 0x0804); assert_eq!(cdb[8], 0x08, "high byte of 2052-byte transfer"); assert_eq!(cdb[9], 0x04, "low byte of 2052-byte transfer"); } /// The unlocker's user-facing name is "DVD" (the medium it read-unlocks), /// not "CSS" (the scheme). Apps render the unlocker report from this name, /// so it is a stable contract — the bus-auth ran on any DVD, encrypted or /// not, and the report must say so rather than conflate it with a CSS crack. #[test] fn dvd_unlocker_is_named_dvd() { use crate::Unlocker; assert_eq!(DvdUnlocker::new().name(), "DVD"); } /// DvdUnlocker provides bus removal only — it never provides drive features. #[test] fn dvd_unlocker_provides_no_features() { use crate::scsi::{DataDirection, ScsiResult}; use crate::{DiscKind, DriveId, UnlockCtx, UnlockError, Unlocker}; struct DeadTransport; impl ScsiTransport for DeadTransport { fn execute( &mut self, _cdb: &[u8], _dir: DataDirection, _data: &mut [u8], _timeout_ms: u32, ) -> crate::scsi::Result { panic!("unlock_features must not touch the transport"); } } let id = DriveId::default(); let mut t = DeadTransport; let r = DvdUnlocker::new().unlock_features(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[])); assert_eq!(r.unwrap_err(), UnlockError::NotApplicable); } /// Defense in depth: even when the caller declares `DiscKind::Css`, the /// DvdUnlocker self-verifies against the drive's GET CONFIGURATION profile. /// A drive reporting a Blu-ray profile → `NotApplicable`, and NOT a single /// CSS CDB is issued (no bus-auth fired at a BD). #[test] fn dvd_unlocker_self_guards_against_non_dvd() { use crate::scsi::{DataDirection, ScsiResult}; use crate::{DiscKind, DriveId, UnlockCtx, UnlockError, Unlocker}; /// Reports a BD-ROM profile (0x0040) to GET CONFIGURATION and counts any /// other CDB (i.e. CSS bus-auth activity). struct BdTransport { non_config_cdbs: usize, } impl ScsiTransport for BdTransport { fn execute( &mut self, cdb: &[u8], _dir: DataDirection, data: &mut [u8], _timeout_ms: u32, ) -> crate::scsi::Result { if cdb[0] == crate::scsi::SCSI_GET_CONFIGURATION { if data.len() >= 8 { data[6] = 0x00; data[7] = 0x40; // BD-ROM current profile } return Ok(ScsiResult { status: 0, bytes_transferred: 8, sense: [0u8; 32], }); } self.non_config_cdbs += 1; Ok(ScsiResult { status: 0, bytes_transferred: 0, sense: [0u8; 32], }) } } let id = DriveId { vendor_id: "FAKEVNDR".to_string(), ..Default::default() }; let mut t = BdTransport { non_config_cdbs: 0 }; let r = DvdUnlocker::new().unlock_bus(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[])); assert_eq!( r.unwrap_err(), UnlockError::NotApplicable, "a BD-profile drive must be refused" ); assert_eq!( t.non_config_cdbs, 0, "no CSS CDB may be issued at a non-DVD drive" ); } }