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:
MattJackson
2026-04-07 11:41:34 -07:00
parent 193b571967
commit cc58ab1754
5 changed files with 83 additions and 26 deletions
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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); }
};
+32 -22
View File
@@ -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]),
})
}
+43
View File
@@ -20,6 +20,7 @@ pub struct DriveSession {
platform: Box<dyn Platform>,
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<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).
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
let mut transport = crate::scsi::open(device)?;
@@ -98,6 +140,7 @@ impl DriveSession {
platform,
profile,
drive_id,
device_path: String::new(),
})
}
+4
View File
@@ -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,