diff --git a/src/aacs_handshake.rs b/src/aacs_handshake.rs index 3223649..4e8dbc3 100644 --- a/src/aacs_handshake.rs +++ b/src/aacs_handshake.rs @@ -529,7 +529,7 @@ pub fn aacs_authenticate( // Step 2: Allocate AGID let cdb = cdb_report_key(0, 0x00, 8); let response = scsi_read(session, &cdb, 8) - .map_err(|_| Error::AacsError { detail: "failed to allocate AGID".into() })?; + .map_err(|e| Error::AacsError { detail: format!("failed to allocate AGID: {}", e) })?; let agid = (response[7] >> 6) & 0x03; // Step 3: Generate host nonce and ephemeral key pair diff --git a/src/bin/aacs_test.rs b/src/bin/aacs_test.rs index 133508f..a2d2b6c 100644 --- a/src/bin/aacs_test.rs +++ b/src/bin/aacs_test.rs @@ -18,9 +18,9 @@ fn main() { println!("aacs-test v{}", env!("CARGO_PKG_VERSION")); println!(); - // Open drive - print!("Opening {}... ", device.display()); - let mut session = match libfreemkv::DriveSession::open(device) { + // Open drive WITHOUT unlock — AACS auth must happen before raw mode + print!("Opening {} (no unlock)... ", device.display()); + let mut session = match libfreemkv::DriveSession::open_no_unlock(device) { Ok(s) => { println!("OK"); s } Err(e) => { println!("FAILED: {}", e); std::process::exit(1); } }; diff --git a/src/disc.rs b/src/disc.rs index 23f538d..1b50e11 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -430,22 +430,30 @@ impl Disc { detail: "no host certificate in KEYDB".into(), })?; - // Step 1: SCSI handshake → bus key + Volume ID - let mut auth = aacs_handshake::aacs_authenticate( - session, - &host_cert.private_key, - &host_cert.certificate, - )?; + // Step 1: Try SCSI handshake for Volume ID + read_data_key + // Open a separate transport (AACS auth must happen before raw mode). + // If handshake fails (drive doesn't support AACS layer, e.g. raw-mode drives), + // fall back to disc-hash-only KEYDB lookup. + let device_path = session.device_path().to_string(); + let mut vid: Option<[u8; 16]> = None; + let mut read_data_key: Option<[u8; 16]> = None; - let vid = aacs_handshake::read_volume_id(session, &mut auth)?; + if !device_path.is_empty() { + if let Ok(mut aacs_session) = DriveSession::open_no_unlock(std::path::Path::new(&device_path)) { + if let Ok(hc) = keydb.host_cert.as_ref().ok_or(()) { + if let Ok(mut auth) = aacs_handshake::aacs_authenticate( + &mut aacs_session, &hc.private_key, &hc.certificate, + ) { + vid = aacs_handshake::read_volume_id(&mut aacs_session, &mut auth).ok(); + read_data_key = aacs_handshake::read_data_keys(&mut aacs_session, &mut auth) + .ok().map(|(rdk, _)| rdk); + } + } + } + // Handshake failure is not fatal — we can still resolve via disc hash + } - // Try to read data keys (AACS 2.0 bus encryption) - let read_data_key = match aacs_handshake::read_data_keys(session, &mut auth) { - Ok((rdk, _wdk)) => Some(rdk), - Err(_) => None, - }; - - // Step 2: Read Unit_Key_RO.inf from disc via UDF + // Step 2: Read Unit_Key_RO.inf from disc via UDF (uses the unlocked main session) let udf_fs = udf::read_filesystem(session)?; let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf") .or_else(|_| udf_fs.read_file(session, "/AACS/DUPLICATE/Unit_Key_RO.inf")) @@ -458,22 +466,24 @@ impl Disc { .or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer")) .ok(); - // Step 4: Resolve all keys via the full chain - // Path 1: disc hash → KEYDB → VUK (fast, 99% of discs) - // Path 2: KEYDB media key + VID → VUK - // Path 3: MKB + processing keys → media key → VUK (fallback) - // Read MKB from drive + // Step 4: Resolve keys + // If we have VID from handshake, use full 4-path chain. + // If no VID (handshake failed), use disc-hash-only KEYDB lookup. let mkb_data = aacs::read_mkb_from_drive(session).ok(); let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version); + // Use a zero VID placeholder if handshake failed — resolve_keys + // will still work via disc hash (path 1) + let vid_for_resolve = vid.unwrap_or([0u8; 16]); + let resolved = aacs::resolve_keys( &uk_ro_data, cc_data.as_deref(), - &vid, + &vid_for_resolve, &keydb, mkb_data.as_deref(), ).ok_or_else(|| Error::AacsError { - detail: "failed to resolve AACS keys".into(), + detail: "failed to resolve AACS keys — disc not in KEYDB".into(), })?; let key_source = match resolved.key_source { @@ -493,7 +503,7 @@ impl Disc { vuk: resolved.vuk, unit_keys: resolved.unit_keys, read_data_key, - volume_id: vid, + volume_id: vid.unwrap_or([0u8; 16]), }) } diff --git a/src/drive.rs b/src/drive.rs index 611ff02..ae9a96b 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -20,6 +20,7 @@ pub struct DriveSession { platform: Box, pub profile: DriveProfile, pub drive_id: DriveId, + device_path: String, } impl DriveSession { @@ -64,6 +65,7 @@ impl DriveSession { platform, profile, drive_id, + device_path: device.to_string_lossy().to_string(), }; // Always unlock on open — makes all reads work immediately. @@ -75,6 +77,46 @@ impl DriveSession { Ok(session) } + /// Open a drive WITHOUT unlocking (raw mode). + /// Used for AACS authentication which must happen before unlock. + pub fn open_no_unlock(device: &Path) -> Result { + let mut transport = crate::scsi::open(device)?; + let profiles = profile::load_bundled()?; + let drive_id = DriveId::from_drive(transport.as_mut())?; + + let profile = profile::find_by_drive_id(&profiles, &drive_id) + .cloned() + .ok_or_else(|| Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: drive_id.product_revision.trim().to_string(), + })?; + + let platform: Box = match profile.chipset { + Chipset::MediaTek => Box::new(Mt1959::new(profile.clone())), + Chipset::Renesas => { + return Err(Error::UnsupportedDrive { + vendor_id: drive_id.vendor_id.trim().to_string(), + product_id: drive_id.product_id.trim().to_string(), + product_revision: "Renesas not yet implemented".to_string(), + }); + } + }; + + Ok(DriveSession { + scsi: transport, + platform, + profile, + drive_id, + device_path: device.to_string_lossy().to_string(), + }) + } + + /// Device path this session was opened on. + pub fn device_path(&self) -> &str { + &self.device_path + } + /// Open with an explicit profile (skip auto-detection). pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result { let mut transport = crate::scsi::open(device)?; @@ -98,6 +140,7 @@ impl DriveSession { platform, profile, drive_id, + device_path: String::new(), }) } diff --git a/src/scsi.rs b/src/scsi.rs index 79c28a3..704e79d 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -141,6 +141,10 @@ impl ScsiTransport for SgIoTransport { if hdr.status != 0 { let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 }; + let asc = if hdr.sb_len_wr > 12 { sense[12] } else { 0 }; + let ascq = if hdr.sb_len_wr > 13 { sense[13] } else { 0 }; + eprintln!(" [scsi] CDB {:02x?} failed: status=0x{:02x} sense={:02x}/{:02x}/{:02x}", + &cdb[..cdb.len().min(12)], hdr.status, sense_key, asc, ascq); return Err(Error::ScsiError { opcode: cdb[0], status: hdr.status,