AACS graceful fallback: handshake optional, disc-hash-only KEYDB path
- SCSI handshake now optional: if drive doesn't support AACS layer (e.g. raw-mode drives where AACS feature current=0), falls back to disc-hash-only KEYDB lookup - open_no_unlock(): open drive without entering raw mode - device_path tracking on DriveSession - Real drive test: BU40N with Civil War UHD - Handshake correctly fails (05/6F/01 key not present — expected) - Scan continues, finds AACS directory, reads Unit_Key_RO.inf - KEYDB lookup attempted (disc hash matching WIP)
This commit is contained in:
@@ -529,7 +529,7 @@ pub fn aacs_authenticate(
|
|||||||
// Step 2: Allocate AGID
|
// Step 2: Allocate AGID
|
||||||
let cdb = cdb_report_key(0, 0x00, 8);
|
let cdb = cdb_report_key(0, 0x00, 8);
|
||||||
let response = scsi_read(session, &cdb, 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;
|
let agid = (response[7] >> 6) & 0x03;
|
||||||
|
|
||||||
// Step 3: Generate host nonce and ephemeral key pair
|
// Step 3: Generate host nonce and ephemeral key pair
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ fn main() {
|
|||||||
println!("aacs-test v{}", env!("CARGO_PKG_VERSION"));
|
println!("aacs-test v{}", env!("CARGO_PKG_VERSION"));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Open drive
|
// Open drive WITHOUT unlock — AACS auth must happen before raw mode
|
||||||
print!("Opening {}... ", device.display());
|
print!("Opening {} (no unlock)... ", device.display());
|
||||||
let mut session = match libfreemkv::DriveSession::open(device) {
|
let mut session = match libfreemkv::DriveSession::open_no_unlock(device) {
|
||||||
Ok(s) => { println!("OK"); s }
|
Ok(s) => { println!("OK"); s }
|
||||||
Err(e) => { println!("FAILED: {}", e); std::process::exit(1); }
|
Err(e) => { println!("FAILED: {}", e); std::process::exit(1); }
|
||||||
};
|
};
|
||||||
|
|||||||
+32
-22
@@ -430,22 +430,30 @@ impl Disc {
|
|||||||
detail: "no host certificate in KEYDB".into(),
|
detail: "no host certificate in KEYDB".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Step 1: SCSI handshake → bus key + Volume ID
|
// Step 1: Try SCSI handshake for Volume ID + read_data_key
|
||||||
let mut auth = aacs_handshake::aacs_authenticate(
|
// Open a separate transport (AACS auth must happen before raw mode).
|
||||||
session,
|
// If handshake fails (drive doesn't support AACS layer, e.g. raw-mode drives),
|
||||||
&host_cert.private_key,
|
// fall back to disc-hash-only KEYDB lookup.
|
||||||
&host_cert.certificate,
|
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)
|
// Step 2: Read Unit_Key_RO.inf from disc via UDF (uses the unlocked main session)
|
||||||
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
|
|
||||||
let udf_fs = udf::read_filesystem(session)?;
|
let udf_fs = udf::read_filesystem(session)?;
|
||||||
let uk_ro_data = udf_fs.read_file(session, "/AACS/Unit_Key_RO.inf")
|
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"))
|
.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"))
|
.or_else(|_| udf_fs.read_file(session, "/AACS/Content001.cer"))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Step 4: Resolve all keys via the full chain
|
// Step 4: Resolve keys
|
||||||
// Path 1: disc hash → KEYDB → VUK (fast, 99% of discs)
|
// If we have VID from handshake, use full 4-path chain.
|
||||||
// Path 2: KEYDB media key + VID → VUK
|
// If no VID (handshake failed), use disc-hash-only KEYDB lookup.
|
||||||
// Path 3: MKB + processing keys → media key → VUK (fallback)
|
|
||||||
// Read MKB from drive
|
|
||||||
let mkb_data = aacs::read_mkb_from_drive(session).ok();
|
let mkb_data = aacs::read_mkb_from_drive(session).ok();
|
||||||
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
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(
|
let resolved = aacs::resolve_keys(
|
||||||
&uk_ro_data,
|
&uk_ro_data,
|
||||||
cc_data.as_deref(),
|
cc_data.as_deref(),
|
||||||
&vid,
|
&vid_for_resolve,
|
||||||
&keydb,
|
&keydb,
|
||||||
mkb_data.as_deref(),
|
mkb_data.as_deref(),
|
||||||
).ok_or_else(|| Error::AacsError {
|
).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 {
|
let key_source = match resolved.key_source {
|
||||||
@@ -493,7 +503,7 @@ impl Disc {
|
|||||||
vuk: resolved.vuk,
|
vuk: resolved.vuk,
|
||||||
unit_keys: resolved.unit_keys,
|
unit_keys: resolved.unit_keys,
|
||||||
read_data_key,
|
read_data_key,
|
||||||
volume_id: vid,
|
volume_id: vid.unwrap_or([0u8; 16]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct DriveSession {
|
|||||||
platform: Box<dyn Platform>,
|
platform: Box<dyn Platform>,
|
||||||
pub profile: DriveProfile,
|
pub profile: DriveProfile,
|
||||||
pub drive_id: DriveId,
|
pub drive_id: DriveId,
|
||||||
|
device_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DriveSession {
|
impl DriveSession {
|
||||||
@@ -64,6 +65,7 @@ impl DriveSession {
|
|||||||
platform,
|
platform,
|
||||||
profile,
|
profile,
|
||||||
drive_id,
|
drive_id,
|
||||||
|
device_path: device.to_string_lossy().to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always unlock on open — makes all reads work immediately.
|
// Always unlock on open — makes all reads work immediately.
|
||||||
@@ -75,6 +77,46 @@ impl DriveSession {
|
|||||||
Ok(session)
|
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<Self> {
|
||||||
|
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<dyn Platform> = 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).
|
/// Open with an explicit profile (skip auto-detection).
|
||||||
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
|
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
|
||||||
let mut transport = crate::scsi::open(device)?;
|
let mut transport = crate::scsi::open(device)?;
|
||||||
@@ -98,6 +140,7 @@ impl DriveSession {
|
|||||||
platform,
|
platform,
|
||||||
profile,
|
profile,
|
||||||
drive_id,
|
drive_id,
|
||||||
|
device_path: String::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ impl ScsiTransport for SgIoTransport {
|
|||||||
|
|
||||||
if hdr.status != 0 {
|
if hdr.status != 0 {
|
||||||
let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 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 {
|
return Err(Error::ScsiError {
|
||||||
opcode: cdb[0],
|
opcode: cdb[0],
|
||||||
status: hdr.status,
|
status: hdr.status,
|
||||||
|
|||||||
Reference in New Issue
Block a user