Restructure: scsi/ and aacs/ as module directories

- scsi.rs → scsi/mod.rs (trait, constants, CDB builders) + scsi/linux.rs (SG_IO)
 Ready for scsi/macos.rs and scsi/windows.rs when needed.
- aacs.rs + aacs_handshake.rs → aacs/mod.rs (keys, decrypt) + aacs/handshake.rs (SCSI auth)
- All internal refs use super:: within module, crate:: across modules
- Zero warnings, 31 tests passing
This commit is contained in:
MattJackson
2026-04-07 12:31:05 -07:00
parent d0c5c7fb97
commit df652c5735
8 changed files with 268 additions and 274 deletions
@@ -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))
+2
View File
@@ -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};
+3 -3
View File
@@ -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);
+4 -4
View File
@@ -426,7 +426,7 @@ impl Disc {
keydb_path: &std::path::Path,
) -> Result<AacsState> {
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);
}
}
-1
View File
@@ -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;
-264
View File
@@ -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<ScsiResult>;
}
// ─── 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<Self> {
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<ScsiResult> {
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<Box<dyn ScsiTransport>> {
#[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<u8>,
}
/// Send INQUIRY command and parse the standard response fields.
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
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<Vec<u8>> {
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,
]
}
+119
View File
@@ -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<Self> {
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<ScsiResult> {
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,
})
}
}
+138
View File
@@ -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<ScsiResult>;
}
// ── 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<Box<dyn ScsiTransport>> {
#[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<u8>,
}
/// Send INQUIRY and parse standard response fields.
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
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<Vec<u8>> {
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,
]
}