From d7b9d8707551213e10bea3bd4ea57a559b998bb5 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:01:48 +0000 Subject: [PATCH] v0.10.3: CSS drive authentication for DVD ripping --- CHANGELOG.md | 7 + Cargo.toml | 2 +- src/css/auth.rs | 425 +++++++++++++++++++++++++++++++++++++++++++++++ src/css/mod.rs | 1 + src/disc/mod.rs | 7 + src/drive/mod.rs | 5 + src/error.rs | 3 + 7 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 src/css/auth.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f9904e3..7fee719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.10.3 (2026-04-16) + +### DVD CSS authentication +- **CSS drive authentication** — full SCSI REPORT KEY / SEND KEY handshake with 6-round substitution-permutation cipher (CSSCryptKey). Brute-forces variant from 32 possibilities. Drive serves scrambled sectors after auth completes. +- **CSS auth runs before scan** — chicken-and-egg fix: auth must happen before reading VOB sectors for title key cracking, not after. +- **Remove debug output** — strip temporary eprintln from drive reads and CSS auth. + ## 0.10.2 (2026-04-15) ### Fixes diff --git a/Cargo.toml b/Cargo.toml index 2ae5f9d..7a56914 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.10.2" +version = "0.10.3" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/css/auth.rs b/src/css/auth.rs new file mode 100644 index 0000000..af2a5a3 --- /dev/null +++ b/src/css/auth.rs @@ -0,0 +1,425 @@ +//! CSS drive authentication — SCSI handshake to unlock scrambled sector reads. +//! +//! Pure SCSI (REPORT KEY / SEND KEY). Works on all platforms via ScsiTransport. +//! +//! The CSS auth uses a challenge-response with a 6-round substitution-permutation +//! cipher (CSSCryptKey). This is DIFFERENT from the content descrambling cipher. +//! +//! Protocol: +//! 1. Allocate AGID +//! 2. Host sends challenge, drive returns Key1 +//! 3. Host brute-forces variant (0-31) by checking which produces Key1 +//! 4. Drive sends challenge, host computes Key2 using found variant +//! 5. Host sends Key2, drive verifies — authentication complete +//! +//! Based on Stevenson 1999 analysis and libdvdcss CSSAuth()/CSSCryptKey(). + +use crate::drive::Drive; +use crate::error::{Error, Result}; + +// ── Tables from csstables.h (libdvdcss) ──────────────────────────────────── + +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, + ], +]; + +// ── Public API ────────────────────────────────────────────────────────────── + +/// Perform CSS authentication with the drive. +pub fn authenticate(drive: &mut Drive) -> Result<()> { + let scsi = drive.scsi_mut(); + + // Invalidate all AGIDs + for agid in 0..4u8 { + let mut buf = [0u8; 4]; + let _ = scsi.execute( + &report_key_cdb(agid, 0x3F, 0), + 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; + + // Step 1: Send host 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; + // Byte-reverse on wire + 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)?; + + // Step 2: 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)]; // byte-reverse + } + + // Step 3: Brute-force variant (0-31) + let mut variant: Option = None; + for v in 0..32u8 { + let check = crypt_key(0, v, &host_challenge); + if check == key1 { + variant = Some(v); + break; + } + } + let variant = variant.ok_or(Error::CssAuthFailed)?; + + // Step 4: 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)]; // byte-reverse + } + + // Step 5: 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]; // byte-reverse + } + scsi.execute( + &send_key_cdb(agid, 0x03, 12), // format 0x03 for Key2 + crate::scsi::DataDirection::ToDevice, + &mut hk_buf, + 5_000, + ) + .map_err(|_| Error::CssAuthFailed)?; + + Ok(()) +} + +// ── CSSCryptKey — the bus key challenge-response cipher ───────────────────── + +/// Compute CSS bus key challenge response. +/// key_type: 0=Key1, 1=Key2, 2=bus_key +fn crypt_key(key_type: usize, variant: u8, challenge: &[u8; 10]) -> [u8; 5] { + // Permute challenge + let perm = &PERM_CHALLENGE[key_type]; + let mut scratch = [0u8; 10]; + for i in 0..10 { + scratch[i] = challenge[perm[i]]; + } + + // Resolve CSS variant + 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]; + + // LFSR init from upper 5 challenge bytes + secret + 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); + + // Generate 30 pseudo-random bytes + 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; + } + + // Six substitution-permutation rounds (NOT all identical — rounds 3,4 use CRYPT_TAB0) + // Matches libdvdcss CryptKey() exactly. + 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 + { + 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]; + } + tmp1[4] ^= tmp1[0]; + } + + // Round 2: bits[20..24] ^ tmp1 -> tmp2 + { + 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: bits[15..19] ^ tmp2 -> tmp1 (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: bits[10..14] ^ tmp1 -> tmp2 (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: bits[5..9] ^ tmp2 -> tmp1 + { + 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: bits[0..4] ^ tmp1 -> key (output) + 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]; + let r0 = crypt_key(0, 0, &challenge); + let r1 = crypt_key(0, 1, &challenge); + assert_ne!(r0, r1); + } + + #[test] + fn crypt_key_varies_by_type() { + let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + let r0 = crypt_key(0, 5, &challenge); + let r1 = crypt_key(1, 5, &challenge); + assert_ne!(r0, r1); + } + + #[test] + fn crypt_key_nonzero() { + let challenge: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + for v in 0..32u8 { + let r = crypt_key(0, v, &challenge); + assert_ne!(r, [0u8; 5], "zero result for variant {}", v); + } + } +} diff --git a/src/css/mod.rs b/src/css/mod.rs index 5b2af5f..9df51d8 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -10,6 +10,7 @@ //! css::descramble_sector(&key, &mut sector); //! ``` +pub mod auth; pub mod crack; pub mod lfsr; pub(crate) mod tables; diff --git a/src/disc/mod.rs b/src/disc/mod.rs index f0da508..4a7fc60 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -942,7 +942,14 @@ impl Disc { pub fn scan(session: &mut Drive, opts: &ScanOptions) -> Result { // READ CAPACITY may fail in LibreDrive mode — proceed with 0 and estimate later let capacity = Self::read_capacity(session).unwrap_or(0); + + // AACS handshake (Blu-ray/UHD) let handshake = Self::do_handshake(session, opts); + + // CSS authentication (DVD) — must happen before scan reads VOB sectors. + // Harmless on BD (AGID alloc fails, no effect). + let _ = crate::css::auth::authenticate(session); + Self::scan_with(session, capacity, handshake, opts) } diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 2569bbc..16e0ee7 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -111,6 +111,11 @@ impl Drive { self.profile.is_some() } + /// Access the SCSI transport for direct commands (used by CSS/AACS auth). + pub fn scsi_mut(&mut self) -> &mut dyn ScsiTransport { + self.scsi.as_mut() + } + pub fn wait_ready(&mut self) -> Result<()> { let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00]; let mut tried_reset = false; diff --git a/src/error.rs b/src/error.rs index e69d229..f813bbf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,6 +63,7 @@ pub const E_AACS_VID_READ: u16 = 7009; pub const E_AACS_VID_MAC: u16 = 7010; pub const E_AACS_DATA_KEY: u16 = 7011; pub const E_DECRYPT_FAILED: u16 = 7013; +pub const E_CSS_AUTH_FAILED: u16 = 7014; // Keydb (8xxx) pub const E_KEYDB_CONNECT: u16 = 8000; @@ -160,6 +161,7 @@ pub enum Error { AacsVidMac, AacsDataKey, DecryptFailed, + CssAuthFailed, // Keydb (8xxx) KeydbConnect { @@ -233,6 +235,7 @@ impl Error { Error::AacsVidMac => E_AACS_VID_MAC, Error::AacsDataKey => E_AACS_DATA_KEY, Error::DecryptFailed => E_DECRYPT_FAILED, + Error::CssAuthFailed => E_CSS_AUTH_FAILED, Error::KeydbConnect { .. } => E_KEYDB_CONNECT, Error::KeydbHttp { .. } => E_KEYDB_HTTP, Error::KeydbInvalid => E_KEYDB_INVALID,