diff --git a/src/aacs_handshake.rs b/src/aacs/handshake.rs similarity index 99% rename from src/aacs_handshake.rs rename to src/aacs/handshake.rs index 7a1ec5b..2b4146d 100644 --- a/src/aacs_handshake.rs +++ b/src/aacs/handshake.rs @@ -670,8 +670,8 @@ pub fn read_data_keys(session: &mut DriveSession, auth: &mut AacsAuth) -> Result enc_wdk.copy_from_slice(&response[20..36]); // Decrypt with bus key (AES-ECB) - let read_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_rdk); - let write_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_wdk); + let read_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_rdk); + let write_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_wdk); auth.read_data_key = Some(read_data_key); Ok((read_data_key, write_data_key)) diff --git a/src/aacs.rs b/src/aacs/mod.rs similarity index 99% rename from src/aacs.rs rename to src/aacs/mod.rs index e69c989..ce59492 100644 --- a/src/aacs.rs +++ b/src/aacs/mod.rs @@ -13,6 +13,8 @@ //! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc. //! Title keys decrypt m2ts stream content (AES-128-CBC). +pub mod handshake; + use std::collections::HashMap; use aes::Aes128; use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit, generic_array::GenericArray}; diff --git a/src/bin/aacs_test.rs b/src/bin/aacs_test.rs index a2d2b6c..df63423 100644 --- a/src/bin/aacs_test.rs +++ b/src/bin/aacs_test.rs @@ -49,7 +49,7 @@ fn main() { // AACS handshake println!(); print!("AACS authenticate... "); - let mut auth = match libfreemkv::aacs_handshake::aacs_authenticate( + let mut auth = match libfreemkv::aacs::handshake::aacs_authenticate( &mut session, &host_cert.private_key, &host_cert.certificate, @@ -69,7 +69,7 @@ fn main() { // Read Volume ID print!("Reading Volume ID... "); - match libfreemkv::aacs_handshake::read_volume_id(&mut session, &mut auth) { + match libfreemkv::aacs::handshake::read_volume_id(&mut session, &mut auth) { Ok(vid) => { println!("OK"); println!(" VID: {:02x?}", vid); @@ -91,7 +91,7 @@ fn main() { // Read data keys (AACS 2.0) print!("Reading data keys... "); - match libfreemkv::aacs_handshake::read_data_keys(&mut session, &mut auth) { + match libfreemkv::aacs::handshake::read_data_keys(&mut session, &mut auth) { Ok((rdk, wdk)) => { println!("OK (AACS 2.0 bus encryption)"); println!(" Read data key: {:02x?}", rdk); diff --git a/src/disc.rs b/src/disc.rs index 8305d66..9035cc8 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -426,7 +426,7 @@ impl Disc { keydb_path: &std::path::Path, ) -> Result { use crate::aacs::{self, KeyDb}; - use crate::aacs_handshake; + use crate::aacs::handshake; // Load KEYDB let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError { @@ -444,11 +444,11 @@ impl Disc { 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( + if let Ok(mut auth) = 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) + vid = handshake::read_volume_id(&mut aacs_session, &mut auth).ok(); + read_data_key = handshake::read_data_keys(&mut aacs_session, &mut auth) .ok().map(|(rdk, _)| rdk); } } diff --git a/src/lib.rs b/src/lib.rs index aea2bc0..646b86f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,6 @@ pub mod clpi; pub mod disc; pub mod jar; pub mod aacs; -pub mod aacs_handshake; pub use error::{Error, Result}; pub use drive::DriveSession; diff --git a/src/scsi.rs b/src/scsi.rs deleted file mode 100644 index cd9b45c..0000000 --- a/src/scsi.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! SCSI/MMC command interface. -//! -//! Platform backends: -//! - Linux: SG_IO ioctl -//! - macOS: IOKit SCSI passthrough (planned) -//! - Windows: SPTI (planned) - -use crate::error::{Error, Result}; -use std::path::Path; - -// ── SCSI opcodes (SPC-4, MMC-6) ──────────────────────────────────────────── - -pub const SCSI_INQUIRY: u8 = 0x12; -pub const SCSI_READ_CAPACITY: u8 = 0x25; -pub const SCSI_READ_10: u8 = 0x28; -pub const SCSI_READ_BUFFER: u8 = 0x3C; -pub const SCSI_READ_TOC: u8 = 0x43; -pub const SCSI_GET_CONFIGURATION: u8 = 0x46; -pub const SCSI_SEND_KEY: u8 = 0xA3; -pub const SCSI_REPORT_KEY: u8 = 0xA4; -pub const SCSI_READ_12: u8 = 0xA8; -pub const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD; - -/// AACS key class for REPORT KEY / SEND KEY commands. -pub const AACS_KEY_CLASS: u8 = 0x02; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum DataDirection { - None, - FromDevice, - ToDevice, -} - -#[derive(Debug)] -pub struct ScsiResult { - pub status: u8, - pub bytes_transferred: usize, - pub sense: [u8; 32], -} - -/// Low-level SCSI transport — implemented per platform. -pub trait ScsiTransport { - fn execute( - &mut self, - cdb: &[u8], - direction: DataDirection, - data: &mut [u8], - timeout_ms: u32, - ) -> Result; -} - -// ─── Linux: SG_IO ─────────────────────────────────────────────────────────── - -#[cfg(target_os = "linux")] -const SG_IO: u32 = 0x2285; -#[cfg(target_os = "linux")] -const SG_DXFER_NONE: i32 = -1; -#[cfg(target_os = "linux")] -const SG_DXFER_TO_DEV: i32 = -2; -#[cfg(target_os = "linux")] -const SG_DXFER_FROM_DEV: i32 = -3; - -#[cfg(target_os = "linux")] -#[repr(C)] -#[allow(non_camel_case_types)] -struct sg_io_hdr { - interface_id: i32, - dxfer_direction: i32, - cmd_len: u8, - mx_sb_len: u8, - iovec_count: u16, - dxfer_len: u32, - dxferp: *mut u8, - cmdp: *const u8, - sbp: *mut u8, - timeout: u32, - flags: u32, - pack_id: i32, - usr_ptr: *mut libc::c_void, - status: u8, - masked_status: u8, - msg_status: u8, - sb_len_wr: u8, - host_status: u16, - driver_status: u16, - resid: i32, - duration: u32, - info: u32, -} - -#[cfg(target_os = "linux")] -pub struct SgIoTransport { - fd: i32, -} - -#[cfg(target_os = "linux")] -impl SgIoTransport { - pub fn open(device: &Path) -> Result { - use std::os::unix::ffi::OsStrExt; - let path_bytes = device.as_os_str().as_bytes(); - let mut c_path = Vec::with_capacity(path_bytes.len() + 1); - c_path.extend_from_slice(path_bytes); - c_path.push(0); - - let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) }; - if fd < 0 { - return Err(Error::DeviceNotFound { path: device.display().to_string() }); - } - Ok(SgIoTransport { fd }) - } -} - -#[cfg(target_os = "linux")] -impl Drop for SgIoTransport { - fn drop(&mut self) { - unsafe { libc::close(self.fd); } - } -} - -#[cfg(target_os = "linux")] -impl ScsiTransport for SgIoTransport { - fn execute( - &mut self, - cdb: &[u8], - direction: DataDirection, - data: &mut [u8], - timeout_ms: u32, - ) -> Result { - let mut sense = [0u8; 32]; - - let dxfer_direction = match direction { - DataDirection::None => SG_DXFER_NONE, - DataDirection::FromDevice => SG_DXFER_FROM_DEV, - DataDirection::ToDevice => SG_DXFER_TO_DEV, - }; - - let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() }; - hdr.interface_id = b'S' as i32; - hdr.dxfer_direction = dxfer_direction; - hdr.cmd_len = cdb.len() as u8; - hdr.mx_sb_len = sense.len() as u8; - hdr.dxfer_len = data.len() as u32; - hdr.dxferp = data.as_mut_ptr(); - hdr.cmdp = cdb.as_ptr(); - hdr.sbp = sense.as_mut_ptr(); - hdr.timeout = timeout_ms; - - let ret = unsafe { - libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) - }; - - if ret < 0 { - return Err(Error::IoError { source: std::io::Error::last_os_error() }); - } - - let bytes_transferred = (data.len() as i32 - hdr.resid) as usize; - - if hdr.status != 0 { - let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 }; - return Err(Error::ScsiError { - opcode: cdb[0], - status: hdr.status, - sense_key, - }); - } - - Ok(ScsiResult { - status: hdr.status, - bytes_transferred, - sense, - }) - } -} - -// ─── macOS: IOKit (planned) ───────────────────────────────────────────────── - -// TODO: IOKit MMC SCSI passthrough -// Use IOSCSIPeripheralDeviceType05 (MMC device nub) -// Send SCSITaskInterface commands via IOKit user client - -// ─── Windows: SPTI (planned) ──────────────────────────────────────────────── - -// TODO: SCSI Pass Through Interface -// Use CreateFile on \\.\CdRomN -// Send IOCTL_SCSI_PASS_THROUGH_DIRECT - -// ─── Platform-agnostic open ───────────────────────────────────────────────── - -/// Open a SCSI transport for the given device path. -pub fn open(device: &Path) -> Result> { - #[cfg(target_os = "linux")] - { Ok(Box::new(SgIoTransport::open(device)?)) } - - #[cfg(not(target_os = "linux"))] - { Err(Error::DeviceNotFound { path: format!("{}: platform not yet supported (Linux only)", device.display()) }) } -} - -// ─── CDB builders (platform-agnostic) ─────────────────────────────────────── - -/// SCSI INQUIRY response. -#[derive(Debug, Clone)] -pub struct InquiryResult { - pub vendor_id: String, - pub model: String, - pub firmware: String, - pub raw: Vec, -} - -/// Send INQUIRY command and parse the standard response fields. -pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result { - let cdb = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00]; - let mut buf = [0u8; 96]; - scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; - - let vendor = String::from_utf8_lossy(&buf[8..16]).trim().to_string(); - let model = String::from_utf8_lossy(&buf[16..32]).trim().to_string(); - let firmware = String::from_utf8_lossy(&buf[32..36]).trim().to_string(); - - Ok(InquiryResult { - vendor_id: vendor, - model, - firmware, - raw: buf.to_vec(), - }) -} - -/// Send GET CONFIGURATION for feature 0x010C (Firmware Information). -pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result> { - let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; - let mut buf = [0u8; 16]; - scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; - Ok(buf.to_vec()) -} - -/// Build a READ BUFFER (0x3C) CDB. -pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] { - [ - 0x3C, mode, buffer_id, - (offset >> 16) as u8, (offset >> 8) as u8, offset as u8, - (length >> 16) as u8, (length >> 8) as u8, length as u8, - 0x00, - ] -} - -/// Build a SET CD SPEED (0xBB) CDB. -pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] { - [ - 0xBB, 0x00, - (read_speed >> 8) as u8, read_speed as u8, - 0xFF, 0xFF, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ] -} - -/// Build a READ(10) CDB with the raw read flag (0x08). -pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] { - [ - 0x28, 0x08, - (lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8, - 0x00, - (count >> 8) as u8, count as u8, - 0x00, - ] -} diff --git a/src/scsi/linux.rs b/src/scsi/linux.rs new file mode 100644 index 0000000..3c9b549 --- /dev/null +++ b/src/scsi/linux.rs @@ -0,0 +1,119 @@ +//! Linux SCSI transport via SG_IO ioctl. + +use crate::error::{Error, Result}; +use super::{ScsiTransport, ScsiResult, DataDirection}; +use std::path::Path; + +const SG_IO: u32 = 0x2285; +const SG_DXFER_NONE: i32 = -1; +const SG_DXFER_TO_DEV: i32 = -2; +const SG_DXFER_FROM_DEV: i32 = -3; + +#[repr(C)] +#[allow(non_camel_case_types)] +struct sg_io_hdr { + interface_id: i32, + dxfer_direction: i32, + cmd_len: u8, + mx_sb_len: u8, + iovec_count: u16, + dxfer_len: u32, + dxferp: *mut u8, + cmdp: *const u8, + sbp: *mut u8, + timeout: u32, + flags: u32, + pack_id: i32, + usr_ptr: *mut libc::c_void, + status: u8, + masked_status: u8, + msg_status: u8, + sb_len_wr: u8, + host_status: u16, + driver_status: u16, + resid: i32, + duration: u32, + info: u32, +} + +pub struct SgIoTransport { + fd: i32, +} + +impl SgIoTransport { + pub fn open(device: &Path) -> Result { + use std::os::unix::ffi::OsStrExt; + let path_bytes = device.as_os_str().as_bytes(); + let mut c_path = Vec::with_capacity(path_bytes.len() + 1); + c_path.extend_from_slice(path_bytes); + c_path.push(0); + + let fd = unsafe { + libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) + }; + if fd < 0 { + return Err(Error::DeviceNotFound { path: device.display().to_string() }); + } + Ok(SgIoTransport { fd }) + } +} + +impl Drop for SgIoTransport { + fn drop(&mut self) { + unsafe { libc::close(self.fd); } + } +} + +impl ScsiTransport for SgIoTransport { + fn execute( + &mut self, + cdb: &[u8], + direction: DataDirection, + data: &mut [u8], + timeout_ms: u32, + ) -> Result { + let mut sense = [0u8; 32]; + + let dxfer_direction = match direction { + DataDirection::None => SG_DXFER_NONE, + DataDirection::FromDevice => SG_DXFER_FROM_DEV, + DataDirection::ToDevice => SG_DXFER_TO_DEV, + }; + + let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() }; + hdr.interface_id = b'S' as i32; + hdr.dxfer_direction = dxfer_direction; + hdr.cmd_len = cdb.len() as u8; + hdr.mx_sb_len = sense.len() as u8; + hdr.dxfer_len = data.len() as u32; + hdr.dxferp = data.as_mut_ptr(); + hdr.cmdp = cdb.as_ptr(); + hdr.sbp = sense.as_mut_ptr(); + hdr.timeout = timeout_ms; + + let ret = unsafe { + libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) + }; + + if ret < 0 { + return Err(Error::IoError { source: std::io::Error::last_os_error() }); + } + + let bytes_transferred = (data.len() as i32 - hdr.resid) as usize; + + if hdr.status != 0 { + let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 }; + return Err(Error::ScsiError { + opcode: cdb[0], + status: hdr.status, + sense_key, + }); + } + + Ok(ScsiResult { + status: hdr.status, + bytes_transferred, + sense, + }) + } +} diff --git a/src/scsi/mod.rs b/src/scsi/mod.rs new file mode 100644 index 0000000..9a2747f --- /dev/null +++ b/src/scsi/mod.rs @@ -0,0 +1,138 @@ +//! SCSI/MMC command interface. +//! +//! Platform backends are in separate files: +//! - `linux.rs` — SG_IO ioctl +//! - `macos.rs` — IOKit (planned) +//! - `windows.rs` — SPTI (planned) + +#[cfg(target_os = "linux")] +mod linux; + +use crate::error::{Error, Result}; +use std::path::Path; + +// ── SCSI opcodes (SPC-4, MMC-6) ──────────────────────────────────────────── + +pub const SCSI_INQUIRY: u8 = 0x12; +pub const SCSI_READ_CAPACITY: u8 = 0x25; +pub const SCSI_READ_10: u8 = 0x28; +pub const SCSI_READ_BUFFER: u8 = 0x3C; +pub const SCSI_READ_TOC: u8 = 0x43; +pub const SCSI_GET_CONFIGURATION: u8 = 0x46; +pub const SCSI_SET_CD_SPEED: u8 = 0xBB; +pub const SCSI_SEND_KEY: u8 = 0xA3; +pub const SCSI_REPORT_KEY: u8 = 0xA4; +pub const SCSI_READ_12: u8 = 0xA8; +pub const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD; + +/// AACS key class for REPORT KEY / SEND KEY commands. +pub const AACS_KEY_CLASS: u8 = 0x02; + +// ── Types ─────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DataDirection { + None, + FromDevice, + ToDevice, +} + +#[derive(Debug)] +pub struct ScsiResult { + pub status: u8, + pub bytes_transferred: usize, + pub sense: [u8; 32], +} + +/// Low-level SCSI transport — one implementation per platform. +pub trait ScsiTransport { + fn execute( + &mut self, + cdb: &[u8], + direction: DataDirection, + data: &mut [u8], + timeout_ms: u32, + ) -> Result; +} + +// ── Platform-agnostic open ────────────────────────────────────────────────── + +/// Open a SCSI transport for the given device path. +/// Selects the right backend for the current platform. +pub fn open(device: &Path) -> Result> { + #[cfg(target_os = "linux")] + { Ok(Box::new(linux::SgIoTransport::open(device)?)) } + + #[cfg(target_os = "macos")] + { Err(Error::DeviceNotFound { path: format!("{}: macOS not yet supported", device.display()) }) } + + #[cfg(target_os = "windows")] + { Err(Error::DeviceNotFound { path: format!("{}: Windows not yet supported", device.display()) }) } + + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { Err(Error::DeviceNotFound { path: format!("{}: unsupported platform", device.display()) }) } +} + +// ── CDB builders (platform-agnostic) ──────────────────────────────────────── + +/// SCSI INQUIRY response. +#[derive(Debug, Clone)] +pub struct InquiryResult { + pub vendor_id: String, + pub model: String, + pub firmware: String, + pub raw: Vec, +} + +/// Send INQUIRY and parse standard response fields. +pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result { + let cdb = [SCSI_INQUIRY, 0x00, 0x00, 0x00, 0x60, 0x00]; + let mut buf = [0u8; 96]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; + + Ok(InquiryResult { + vendor_id: String::from_utf8_lossy(&buf[8..16]).trim().to_string(), + model: String::from_utf8_lossy(&buf[16..32]).trim().to_string(), + firmware: String::from_utf8_lossy(&buf[32..36]).trim().to_string(), + raw: buf.to_vec(), + }) +} + +/// Send GET CONFIGURATION for feature 0x010C (Firmware Information). +pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result> { + let cdb = [SCSI_GET_CONFIGURATION, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; + let mut buf = [0u8; 16]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; + Ok(buf.to_vec()) +} + +/// Build a READ BUFFER CDB. +pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] { + [ + SCSI_READ_BUFFER, mode, buffer_id, + (offset >> 16) as u8, (offset >> 8) as u8, offset as u8, + (length >> 16) as u8, (length >> 8) as u8, length as u8, + 0x00, + ] +} + +/// Build a SET CD SPEED CDB. +pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] { + [ + SCSI_SET_CD_SPEED, 0x00, + (read_speed >> 8) as u8, read_speed as u8, + 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] +} + +/// Build a READ(10) CDB with the raw read flag. +pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] { + [ + SCSI_READ_10, 0x08, + (lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8, + 0x00, + (count >> 8) as u8, count as u8, + 0x00, + ] +}