disc/scan: surface AACS resolution error on Disc.aacs_error
scan_with() collapsed every failure path from resolve_encryption() into None via .ok(), so callers couldn't tell the difference between "no KEYDB found", "KEYDB failed to parse", "disc hash not in KEYDB and fallback derivation failed", "AACS files unreadable on disc", and a handshake that rejected every host cert. autorip's UI was stuck printing "no decryption keys found (check KEYDB)" for all of them, which is a particularly bad message when the user has actually loaded a KEYDB and the real failure is something else. Changes: - New pub field Disc.aacs_error: Option<Error>. Populated by scan_with whenever encrypted && aacs.is_none(). Sentinel KeydbLoad path "<no keydb in search paths>" distinguishes the no-keydb case from a real load failure without adding a new Error variant (which would be a breaking change for downstream exhaustive matches). - tracing::warn in scan_with at scan_aacs_resolve_failed and scan_aacs_no_keydb, with error_code and keydb path for grepping. - tracing in do_handshake: keydb load failure, host-cert exhaustion (with cert count and last error code), VID read failure post-auth, and a debug-level success log. Lets us see whether handshake even got off the ground for a given disc. Test fixtures updated to set aacs_error: None.
This commit is contained in:
+49
-4
@@ -23,29 +23,74 @@ impl Disc {
|
||||
use crate::aacs::{self, KeyDb};
|
||||
|
||||
let keydb_path = opts.resolve_keydb()?;
|
||||
let keydb = KeyDb::load(&keydb_path).ok()?;
|
||||
let keydb = match KeyDb::load(&keydb_path) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::aacs",
|
||||
phase = "handshake_keydb_load_failed",
|
||||
io_error_kind = ?e.kind(),
|
||||
keydb = %keydb_path.display(),
|
||||
"KEYDB load failed; handshake skipped"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let host_cert_count = keydb.host_certs.len();
|
||||
tracing::debug!(
|
||||
target: "freemkv::aacs",
|
||||
phase = "handshake_start",
|
||||
host_cert_count,
|
||||
keydb = %keydb_path.display(),
|
||||
);
|
||||
|
||||
const MAX_CERT_ATTEMPTS: usize = 16;
|
||||
for hc in keydb.host_certs.iter().take(MAX_CERT_ATTEMPTS) {
|
||||
let mut last_err_code: Option<u16> = None;
|
||||
for (idx, hc) in keydb.host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
||||
match aacs::handshake::aacs_authenticate(session, &hc.private_key, &hc.certificate) {
|
||||
Ok(mut auth) => {
|
||||
let volume_id = match aacs::handshake::read_volume_id(session, &mut auth) {
|
||||
Ok(vid) => vid,
|
||||
Err(_) => return None, // handshake succeeded but can't read VID
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::aacs",
|
||||
phase = "handshake_vid_read_failed",
|
||||
cert_index = idx,
|
||||
error_code = e.code(),
|
||||
"auth ok but volume ID read failed"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let read_data_key = aacs::handshake::read_data_keys(session, &mut auth)
|
||||
.ok()
|
||||
.map(|(rdk, _)| rdk);
|
||||
tracing::debug!(
|
||||
target: "freemkv::aacs",
|
||||
phase = "handshake_ok",
|
||||
cert_index = idx,
|
||||
has_read_data_key = read_data_key.is_some(),
|
||||
);
|
||||
return Some(HandshakeResult {
|
||||
volume_id,
|
||||
read_data_key,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
last_err_code = Some(e.code());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!(
|
||||
target: "freemkv::aacs",
|
||||
phase = "handshake_all_certs_failed",
|
||||
host_cert_count,
|
||||
tried = host_cert_count.min(MAX_CERT_ATTEMPTS),
|
||||
last_error_code = last_err_code,
|
||||
"all host certs in KEYDB rejected by drive"
|
||||
);
|
||||
// All host certs failed — return None, not a fake success
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user