unlock_bridge: run_features/run_bus dispatch; report LibreDrive vs Renesas
Adopt freemkv-unlock's split Unlocker trait: run_features drives the drive-prep capability, run_bus the content bus removal, each iterating unlockers until one doesn't decline (NotApplicable = try next; Ok or a real error stops). unlocker_matrix now reports which drive-prep unlocker actually ran — LibreDrive removes the bus at the drive; Renesas unlocks features but leaves the bus to the cert. Wire product_id through to fu::DriveId. Bump to 1.2.3.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
+3
-3
@@ -111,13 +111,13 @@ impl AacsCertUnlocker<'_> {
|
||||
// through method calls, so clone the (cheap) identity first.
|
||||
let drive_id = session.drive_id.clone();
|
||||
let fu_certs = crate::unlock_bridge::map_host_certs(&host_certs);
|
||||
let unlocked = crate::unlock_bridge::run_unlockers(
|
||||
let (_, unlock_res) = crate::unlock_bridge::run_bus(
|
||||
session.scsi_mut(),
|
||||
&drive_id,
|
||||
freemkv_unlock::DiscKind::Aacs,
|
||||
&fu_certs,
|
||||
)
|
||||
.map_err(CertUnlockFailure::Unlock)?;
|
||||
);
|
||||
let unlocked = unlock_res.map_err(CertUnlockFailure::Unlock)?;
|
||||
// The cert handshake yields a VID on success; its absence is VidUnavailable.
|
||||
let Some(volume_id) = unlocked.vid else {
|
||||
return Err(CertUnlockFailure::Unlock(UnlockError::VidUnavailable));
|
||||
|
||||
+16
-10
@@ -1644,12 +1644,13 @@ impl Disc {
|
||||
// key). Any failure is non-fatal: continue to the crack, which
|
||||
// simply finds nothing if the drive kept the sectors gated.
|
||||
let drive_id = session.drive_id.clone();
|
||||
if let Err(e) = crate::unlock_bridge::run_unlockers(
|
||||
let (_, css_unlock_res) = crate::unlock_bridge::run_bus(
|
||||
session.scsi_mut(),
|
||||
&drive_id,
|
||||
freemkv_unlock::DiscKind::Css,
|
||||
&[],
|
||||
) {
|
||||
);
|
||||
if let Err(e) = css_unlock_res {
|
||||
tracing::warn!(
|
||||
target: "freemkv::scan",
|
||||
outcome = ?e,
|
||||
@@ -2308,18 +2309,23 @@ impl Disc {
|
||||
/// that honestly (`AACS: no`), and on a *stock* drive that fell back to the
|
||||
/// cert route it reports `LibreDrive: no, AACS: yes` — the real diagnostic.
|
||||
pub fn unlocker_matrix(&self, drive: &crate::Drive) -> Vec<(&'static str, bool)> {
|
||||
// LibreDrive firmware-unlocked the drive iff a drive unlocker actually
|
||||
// succeeded at init (name recorded only on success).
|
||||
let ld_worked = drive.unlocker_name().is_some();
|
||||
// The drive-prep unlocker that actually ran (recorded on init):
|
||||
// "LibreDrive" (MediaTek) or "Renesas" — mutually exclusive per drive.
|
||||
let prep = drive.unlocker_name();
|
||||
// Only LibreDrive (MediaTek) removes AACS bus encryption AT THE DRIVE;
|
||||
// Renesas unlocks features but leaves the bus to the cert.
|
||||
let ld_removed_bus = prep == Some("LibreDrive");
|
||||
crate::unlock_bridge::unlocker_names()
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let did_work = match name {
|
||||
// The drive firmware unlock ran and succeeded.
|
||||
"LibreDrive" => ld_worked,
|
||||
// The AACS host-cert route removed the bus ONLY when LibreDrive
|
||||
// didn't (stock drive) AND AACS state was actually obtained.
|
||||
"AACS" => self.aacs.is_some() && !ld_worked,
|
||||
// Each firmware unlocker did work iff it was the one that ran.
|
||||
"LibreDrive" => ld_removed_bus,
|
||||
"Renesas" => prep == Some("Renesas"),
|
||||
// The AACS host-cert route removed the bus ONLY when the
|
||||
// firmware didn't (stock or Renesas drive) AND AACS state was
|
||||
// actually obtained.
|
||||
"AACS" => self.aacs.is_some() && !ld_removed_bus,
|
||||
// The CSS handshake/crack succeeded → title keys recovered.
|
||||
"CSS" => self.css.is_some(),
|
||||
// A newly-registered unlocker with no runtime signal wired
|
||||
|
||||
+7
-8
@@ -433,15 +433,14 @@ impl Drive {
|
||||
// that used to sit here was the v1.0.0-rc.1 regression — it skipped the
|
||||
// drive-prep for DVD, leaving DVDs riplocked at stock speed.
|
||||
self.init_ran = true;
|
||||
let r: Result<()> = match crate::unlock_bridge::run_unlockers(
|
||||
self.scsi.as_mut(),
|
||||
&self.drive_id,
|
||||
freemkv_unlock::DiscKind::Unknown,
|
||||
&[],
|
||||
) {
|
||||
let (matched, unlock_res) =
|
||||
crate::unlock_bridge::run_features(self.scsi.as_mut(), &self.drive_id);
|
||||
let r: Result<()> = match unlock_res {
|
||||
Ok(unlocked) => {
|
||||
self.unlocker_name =
|
||||
crate::unlock_bridge::unlocker_name(&self.drive_id).map(str::to_string);
|
||||
// Record WHICH drive-prep unlocker actually ran — "LibreDrive"
|
||||
// (MediaTek) or "Renesas" — not the ld-only identity lookup, so a
|
||||
// Renesas drive reports itself honestly rather than as nothing.
|
||||
self.unlocker_name = Some(matched.to_string());
|
||||
// Stash the OEM Volume ID the unlocker returned for the AACS
|
||||
// handshake phase (do_handshake reads it via `oem_vid()`). A
|
||||
// drive-prep unlocker always carries a VID; guard anyway.
|
||||
|
||||
+55
-11
@@ -9,6 +9,7 @@ use freemkv_unlock as fu;
|
||||
fn to_fu_drive_id(drive_id: &crate::identity::DriveId) -> fu::DriveId {
|
||||
fu::DriveId {
|
||||
vendor_id: drive_id.vendor_id.clone(),
|
||||
product_id: drive_id.product_id.clone(),
|
||||
product_revision: drive_id.product_revision.clone(),
|
||||
vendor_specific: drive_id.vendor_specific.clone(),
|
||||
firmware_date: drive_id.firmware_date.clone(),
|
||||
@@ -101,27 +102,70 @@ pub(crate) fn map_host_certs(certs: &[crate::aacs::types::HostCert]) -> Vec<fu::
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// News up the unlockers, build the context for `kind`, and run the FIRST
|
||||
/// matching one — returning its `Result` so the caller can both consume what it
|
||||
/// learned (vid / bus_key / drive_unlocked) AND render the specific failure
|
||||
/// (the AACS cert path maps the `UnlockError` to its outcome trace). `Err(
|
||||
/// NotApplicable)` when nothing matched. `host_certs` are collected by the
|
||||
/// caller — lazily, only for AACS; pass `&[]` for the drive-prep / CSS kinds.
|
||||
pub(crate) fn run_unlockers(
|
||||
/// Result of a capability dispatch: `(matched_name, result)`. `matched_name` is
|
||||
/// the unlocker that handled it (or `""` if none did) — lets the caller record
|
||||
/// WHICH unlocker ran (e.g. `LibreDrive` vs `Renesas`), distinct from the ld-only
|
||||
/// identity lookup [`unlocker_name`]. Iterating stops at the first unlocker whose
|
||||
/// capability method returns anything other than `NotApplicable` — i.e. an actual
|
||||
/// unlock (`Ok`) OR a real failure such as a dead bus (`Err(Transport)`), which
|
||||
/// the caller must surface rather than skip.
|
||||
type Dispatch = (
|
||||
&'static str,
|
||||
std::result::Result<fu::Unlocked, fu::UnlockError>,
|
||||
);
|
||||
|
||||
/// Try each unlocker's `capability` (`unlock_features` or `unlock_bus`) in
|
||||
/// registration order, stopping at the first that doesn't decline. Shared by
|
||||
/// [`run_features`] and [`run_bus`].
|
||||
fn dispatch(
|
||||
scsi: &mut dyn crate::scsi::ScsiTransport,
|
||||
drive_id: &crate::identity::DriveId,
|
||||
kind: fu::DiscKind,
|
||||
host_certs: &[fu::HostCert],
|
||||
) -> std::result::Result<fu::Unlocked, fu::UnlockError> {
|
||||
capability: impl Fn(
|
||||
&dyn fu::Unlocker,
|
||||
&mut dyn fu::scsi::ScsiTransport,
|
||||
&fu::UnlockCtx,
|
||||
) -> std::result::Result<fu::Unlocked, fu::UnlockError>,
|
||||
) -> Dispatch {
|
||||
let id = to_fu_drive_id(drive_id);
|
||||
let ctx = fu::UnlockCtx::new(&id, kind, host_certs);
|
||||
let mut adapter = ScsiAdapter(scsi);
|
||||
for u in fu::all_unlockers() {
|
||||
if u.matches(&ctx) {
|
||||
return u.unlock(&mut adapter, &ctx);
|
||||
match capability(u.as_ref(), &mut adapter, &ctx) {
|
||||
// This unlocker doesn't provide the capability for this drive/disc —
|
||||
// try the next one.
|
||||
Err(fu::UnlockError::NotApplicable) => continue,
|
||||
// An actual unlock, or a real failure (e.g. Transport) — stop here.
|
||||
other => return (u.name(), other),
|
||||
}
|
||||
}
|
||||
Err(fu::UnlockError::NotApplicable)
|
||||
("", Err(fu::UnlockError::NotApplicable))
|
||||
}
|
||||
|
||||
/// Drive-prep: unlock DRIVE FEATURES (riplock/speed, OEM VID). `host_certs` are
|
||||
/// not needed for features — pass `&[]`; `kind` is `Unknown` at drive-prep.
|
||||
pub(crate) fn run_features(
|
||||
scsi: &mut dyn crate::scsi::ScsiTransport,
|
||||
drive_id: &crate::identity::DriveId,
|
||||
) -> Dispatch {
|
||||
dispatch(scsi, drive_id, fu::DiscKind::Unknown, &[], |u, s, c| {
|
||||
u.unlock_features(s, c)
|
||||
})
|
||||
}
|
||||
|
||||
/// Content: remove BUS ENCRYPTION for the mounted disc. Called only when the bus
|
||||
/// isn't already clear (the `oem_vid`/`bus_encryption_removed` gate). `host_certs`
|
||||
/// are the caller-collected certs for the AACS route; `kind` selects Aacs vs Css.
|
||||
pub(crate) fn run_bus(
|
||||
scsi: &mut dyn crate::scsi::ScsiTransport,
|
||||
drive_id: &crate::identity::DriveId,
|
||||
kind: fu::DiscKind,
|
||||
host_certs: &[fu::HostCert],
|
||||
) -> Dispatch {
|
||||
dispatch(scsi, drive_id, kind, host_certs, |u, s, c| {
|
||||
u.unlock_bus(s, c)
|
||||
})
|
||||
}
|
||||
|
||||
/// The names of every REGISTERED unlocker, in dispatch order. Registry-driven —
|
||||
|
||||
Reference in New Issue
Block a user