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
+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]),
})
}