From bc04ee7bd29f9eb048d107321d48d2c936c8a823 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:36:05 -0700 Subject: [PATCH] unlock_bridge: run_features/run_bus dispatch; report LibreDrive vs Renesas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.toml | 2 +- src/disc/encrypt.rs | 6 ++-- src/disc/mod.rs | 26 ++++++++++------- src/drive/mod.rs | 15 +++++----- src/unlock_bridge.rs | 66 ++++++++++++++++++++++++++++++++++++-------- 5 files changed, 82 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af9e674..52e7227 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index 8046d9d..2a01134 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -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)); diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 5fd2c91..cc8c4f9 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -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 diff --git a/src/drive/mod.rs b/src/drive/mod.rs index b9dafe7..52f1622 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -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. diff --git a/src/unlock_bridge.rs b/src/unlock_bridge.rs index ad35c6a..062382b 100644 --- a/src/unlock_bridge.rs +++ b/src/unlock_bridge.rs @@ -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, +); + +/// 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 { + capability: impl Fn( + &dyn fu::Unlocker, + &mut dyn fu::scsi::ScsiTransport, + &fu::UnlockCtx, + ) -> std::result::Result, +) -> 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 —