css: auth primitives operate on &mut dyn ScsiTransport; clean up bus_auth

Thread &mut dyn ScsiTransport through unlock_css_reads and its bus-auth /
disc-key primitives instead of &mut Drive — they only ever issued SCSI via
drive.scsi_mut(), so this is mechanical and lossless, and it makes the CSS
unlock speak the same transport interface as the registry Unlocker trait
(prerequisite for CSS becoming a uniform unlocker).

Also clean up the badly-named, half-dead bus_auth:
- rename bus_auth -> establish_authenticated_session: it is run for its
  side effect (sets the drive's ASF=1, unlocking scrambled-sector reads),
  which the name now states.
- drop the derived CSS bus key from the return + computation: it had no
  consumer (descrambling is keyless via the Stevenson attack), so it was
  dead crypto computed on every DVD unlock. Return just the negotiated AGID,
  which the caller genuinely needs for the best-effort disc-key REPORT KEY.
This commit is contained in:
Matthew Jackson
2026-06-29 16:01:07 -07:00
parent f682405973
commit 03820c68f8
2 changed files with 24 additions and 22 deletions
+23 -21
View File
@@ -8,8 +8,8 @@
//! key is recovered keylessly by the Stevenson known-plaintext attack (see //! key is recovered keylessly by the Stevenson known-plaintext attack (see
//! [`super::crack_key`]). //! [`super::crack_key`]).
use crate::drive::Drive;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::scsi::ScsiTransport;
// ── CryptKey tables ─────────────────────────────────────────────────────── // ── CryptKey tables ───────────────────────────────────────────────────────
@@ -125,10 +125,10 @@ const PERM_VARIANT: [[u8; 32]; 2] = [
/// is unnecessary (the descramble key is recovered keylessly by the Stevenson /// is unnecessary (the descramble key is recovered keylessly by the Stevenson
/// attack in [`super::crack_key`]) and its hard failure on some USB bridges /// 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. /// used to abort the whole unlock (the 7014 bug). The bytes are discarded.
pub fn unlock_css_reads(drive: &mut Drive, lba: u32) -> Result<()> { pub fn unlock_css_reads(scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
let t0 = std::time::Instant::now(); let t0 = std::time::Instant::now();
tracing::info!(target: "freemkv::css", phase = "unlock_css_reads", lba, "begin"); tracing::info!(target: "freemkv::css", phase = "unlock_css_reads", lba, "begin");
let r = unlock_css_reads_inner(drive, lba); let r = unlock_css_reads_inner(scsi, lba);
tracing::info!( tracing::info!(
target: "freemkv::css", target: "freemkv::css",
phase = "unlock_css_reads", phase = "unlock_css_reads",
@@ -140,22 +140,22 @@ pub fn unlock_css_reads(drive: &mut Drive, lba: u32) -> Result<()> {
r r
} }
fn unlock_css_reads_inner(drive: &mut Drive, _lba: u32) -> Result<()> { fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()> {
tracing::debug!(target: "freemkv::css", "css unlock: begin"); tracing::debug!(target: "freemkv::css", "css unlock: begin");
// The bus-auth challenge-response sets the drive's Authentication Success // The bus-auth challenge-response sets the drive's Authentication Success
// Flag (ASF=1), which is what opens scrambled-sector reads. This is the // 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 // ONLY step required to unlock reads; a failure here is fatal — we
// genuinely cannot read scrambled sectors. // genuinely cannot read scrambled sectors.
let (agid, _bus_key) = bus_auth(drive).inspect_err(|e| { let agid = establish_authenticated_session(scsi).inspect_err(|e| {
tracing::warn!(target: "freemkv::css", error_code = e.code(), "css unlock: bus_auth failed"); tracing::warn!(target: "freemkv::css", error_code = e.code(), "css unlock: bus authentication failed");
})?; })?;
tracing::debug!(target: "freemkv::css", agid, "css unlock: bus_auth ok"); tracing::debug!(target: "freemkv::css", agid, "css unlock: bus authentication ok");
// Disc-key REPORT KEY: issued BEST-EFFORT for any firmware that ties part // 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 // 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 // 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 // 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). // failure used to abort the whole unlock (the 7014 bug on USB bridges).
if let Err(e) = read_disc_key(drive, agid) { 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", error_code = e.code(), "css unlock: disc-key REPORT KEY skipped (non-fatal)");
} }
tracing::debug!(target: "freemkv::css", "css unlock: ok"); tracing::debug!(target: "freemkv::css", "css unlock: ok");
@@ -164,9 +164,15 @@ fn unlock_css_reads_inner(drive: &mut Drive, _lba: u32) -> Result<()> {
// ── Step 1: Bus Authentication ──────────────────────────────────────────── // ── Step 1: Bus Authentication ────────────────────────────────────────────
fn bus_auth(drive: &mut Drive) -> Result<(u8, [u8; 5])> { /// Run the CSS bus-authentication challenge-response (invalidate AGIDs →
let scsi = drive.scsi_mut(); /// 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<u8> {
// Invalidate all AGIDs via REPORT KEY format 0x3F // Invalidate all AGIDs via REPORT KEY format 0x3F
for agid in 0..4u8 { for agid in 0..4u8 {
let mut cdb = [0u8; 12]; let mut cdb = [0u8; 12];
@@ -269,13 +275,11 @@ fn bus_auth(drive: &mut Drive) -> Result<(u8, [u8; 5])> {
) )
.map_err(|_| Error::CssAuthFailed)?; .map_err(|_| Error::CssAuthFailed)?;
// Bus key = CryptKey(2, variant, key1 || key2) // The authenticated session (ASF=1) is now established — scrambled-sector
let mut combined = [0u8; 10]; // reads are unlocked, which is the only thing we needed. The CSS bus key
combined[..5].copy_from_slice(&key1); // would be CryptKey(2, variant, key1 || key2), but it has no consumer
combined[5..].copy_from_slice(&key2); // (descrambling is keyless via the Stevenson attack), so it is not derived.
let bus_key = crypt_key(2, variant, &combined); Ok(agid)
Ok((agid, bus_key))
} }
// ── Step 2: Disc Key ────────────────────────────────────────────────────── // ── Step 2: Disc Key ──────────────────────────────────────────────────────
@@ -286,9 +290,7 @@ fn bus_auth(drive: &mut Drive) -> Result<(u8, [u8; 5])> {
/// is recovered keylessly elsewhere, so the genuine disc-key REPORT KEY is /// 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 /// 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.) /// not open scrambled reads, a real REPORT KEY format 0x02 belongs here.)
fn read_disc_key(drive: &mut Drive, agid: u8) -> Result<()> { fn read_disc_key(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()> {
let scsi = drive.scsi_mut();
// READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes // READ DVD STRUCTURE, format 0x02 (disc key), 2048+4 bytes
let alloc_len: u16 = 2048 + 4; let alloc_len: u16 = 2048 + 4;
let mut cdb = [0u8; 12]; let mut cdb = [0u8; 12];
+1 -1
View File
@@ -1478,7 +1478,7 @@ impl Disc {
// unreliable). The real descramble key is recovered from the // unreliable). The real descramble key is recovered from the
// scrambled movie data itself via the known-plaintext attack — no // scrambled movie data itself via the known-plaintext attack — no
// player keys, no disc-key crack, no REPORT-KEY-derived title key. // player keys, no disc-key crack, no REPORT-KEY-derived title key.
if let Err(e) = crate::css::auth::unlock_css_reads(session, unlock_lba) { if let Err(e) = crate::css::auth::unlock_css_reads(session.scsi_mut(), unlock_lba) {
tracing::warn!( tracing::warn!(
target: "freemkv::scan", target: "freemkv::scan",
error_code = e.code(), error_code = e.code(),