Unlocker reshape: unlock() -> Result<Vid, UnlockError>

LibreDrive impl migrated to the single unlock() returning the OEM VID;
firmware-unsupported -> FirmwareNotUnlockable, no-VID -> VidUnavailable.
This commit is contained in:
Matthew Jackson
2026-06-26 12:19:24 -07:00
parent 165da09062
commit f0a9d618eb
+113 -77
View File
@@ -25,8 +25,9 @@ pub mod profile;
mod platform; mod platform;
use error::{Error, Result}; use error::Result;
use libfreemkv::{DriveId, ScsiTransport, Unlocker}; use libfreemkv::aacs::Vid;
use libfreemkv::{DriveId, ScsiTransport, UnlockError, Unlocker};
use scsi::DataDirection; use scsi::DataDirection;
/// The LibreDrive unlocker. /// The LibreDrive unlocker.
@@ -48,78 +49,48 @@ impl Default for LibreDrive {
} }
} }
impl Unlocker for LibreDrive { impl LibreDrive {
fn name(&self) -> &str { /// Read the OEM Volume ID via the matched profile's vendor CDB.
"LibreDrive"
}
fn matches(&self, id: &DriveId) -> bool {
profile::find_bundled(id).is_some()
}
fn unlock_drive(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()> {
let Some(m) = profile::find_bundled(id) else {
// matches() returned true but the profile vanished — treat as
// "nothing to do"; the caller falls back to the cert handshake.
return Ok(());
};
let is_variant_b = matches!(m.platform, profile::Platform::Mt1959B);
if matches!(m.platform, profile::Platform::Renesas) {
// Renesas firmware unlock is not implemented; leave the drive
// untouched so the host-cert handshake carries the disc.
return Ok(());
}
use platform::PlatformDriver;
let mut mt = platform::mt1959::Mt1959::new(m.profile, is_variant_b);
// Firmware unlock. On success, prime the per-region speed table so
// the drive manages zone speeds internally (best-effort — probe
// calibration failure must not fail an otherwise-good unlock).
mt.init(scsi)?;
let _ = mt.probe_disc(scsi);
Ok(())
}
/// OEM Volume ID retrieval.
/// ///
/// An unlocker unlocks drive *functionality*, not just the disc: VID /// This recovers the per-drive READ_BUFFER VID path that lived in
/// retrieval via the drive's OEM CDB is a capability separate from /// libfreemkv before the unlocker refactor (it was `read_vid_oem` in
/// `unlock`. This recovers the per-drive READ_BUFFER VID path that lived /// `libfreemkv/src/disc/encrypt.rs`), now living inside the unlocker where
/// in libfreemkv before the unlocker refactor (it was `read_vid_oem` in /// the per-drive `read_vid_cdb` template belongs. Folded into [`Self::unlock`]:
/// `libfreemkv/src/disc/encrypt.rs`), now living inside the unlocker /// an unlocked drive must hand back its Volume ID in one step.
/// where the per-drive `read_vid_cdb` template belongs.
/// ///
/// Returns: /// Returns:
/// * `Ok(Some(vid))` — the drive's profile carries an OEM VID CDB and /// * `Ok(Vid)` — the profile carries an OEM VID CDB and the drive served a
/// the drive served a well-formed VID. No host certificate or HRL is /// well-formed VID. No host certificate or HRL is involved.
/// involved — VID is decoupled from the cert chain. /// * `Err(UnlockError::VidUnavailable)` — no matching profile, no OEM VID
/// * `Ok(None)` — no profile matched, or the profile carries no OEM VID /// CDB, or a short / bad-signature response (auth succeeded but the VID
/// CDB; libfreemkv falls back to the cert-based VID read. /// could not be read → cert fallback).
/// * `Err(_)` — the OEM CDB ran but returned a short or malformed /// * `Err(UnlockError::Scsi(code))` — the OEM CDB itself failed at the
/// response. /// transport (via `?` / `From<Error>`).
/// ///
/// Response layout (36 bytes): /// Response layout (36 bytes):
/// * `[0..3]` 3-byte response signature; expected `00 22 00` /// * `[0..3]` 3-byte response signature; expected `00 22 00`
/// * `[3]` reserved /// * `[3]` reserved
/// * `[4..20]` 16-byte Volume ID /// * `[4..20]` 16-byte Volume ID
/// * `[20..36]` reserved / per-drive padding /// * `[20..36]` reserved / per-drive padding
fn read_volume_id( fn read_oem_vid(
&self, &self,
scsi: &mut dyn ScsiTransport, scsi: &mut dyn ScsiTransport,
id: &DriveId, id: &DriveId,
) -> Result<Option<[u8; 16]>> { ) -> std::result::Result<Vid, UnlockError> {
const RESPONSE_LEN: usize = 36; const RESPONSE_LEN: usize = 36;
const EXPECTED_HEADER: [u8; 3] = [0x00, 0x22, 0x00]; const EXPECTED_HEADER: [u8; 3] = [0x00, 0x22, 0x00];
// No matching profile, or a profile without an OEM VID CDB → no OEM // No matching profile, or a profile without an OEM VID CDB → no OEM VID
// path; the caller falls back to the cert handshake. // path; the caller falls back to the cert handshake.
let Some(m) = profile::find_bundled(id) else { let Some(m) = profile::find_bundled(id) else {
return Ok(None); return Err(UnlockError::VidUnavailable);
}; };
let Some(cdb) = m.profile.read_vid_cdb else { let Some(cdb) = m.profile.read_vid_cdb else {
return Ok(None); return Err(UnlockError::VidUnavailable);
}; };
let mut buf = vec![0u8; RESPONSE_LEN]; let mut buf = vec![0u8; RESPONSE_LEN];
// A transport failure here is a raw SCSI error → Scsi(code) via `?`.
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
if result.bytes_transferred < RESPONSE_LEN { if result.bytes_transferred < RESPONSE_LEN {
tracing::warn!( tracing::warn!(
@@ -128,7 +99,7 @@ impl Unlocker for LibreDrive {
bytes_transferred = result.bytes_transferred, bytes_transferred = result.bytes_transferred,
"OEM VID CDB returned short response" "OEM VID CDB returned short response"
); );
return Err(Error::AacsVidRead); return Err(UnlockError::VidUnavailable);
} }
if buf[0..3] != EXPECTED_HEADER { if buf[0..3] != EXPECTED_HEADER {
tracing::warn!( tracing::warn!(
@@ -139,7 +110,7 @@ impl Unlocker for LibreDrive {
header_2 = buf[2], header_2 = buf[2],
"OEM VID response header mismatch" "OEM VID response header mismatch"
); );
return Err(Error::AacsVidRead); return Err(UnlockError::VidUnavailable);
} }
let mut vid = [0u8; 16]; let mut vid = [0u8; 16];
vid.copy_from_slice(&buf[4..20]); vid.copy_from_slice(&buf[4..20]);
@@ -148,7 +119,56 @@ impl Unlocker for LibreDrive {
phase = "oem_vid_ok", phase = "oem_vid_ok",
"OEM VID retrieved via unlocker" "OEM VID retrieved via unlocker"
); );
Ok(Some(vid)) Ok(Vid(vid))
}
}
impl Unlocker for LibreDrive {
fn name(&self) -> &str {
"LibreDrive"
}
fn matches(&self, id: &DriveId) -> bool {
profile::find_bundled(id).is_some()
}
/// Firmware-unlock the drive AND return the disc's OEM Volume ID in one step
/// (the new trait folds the old `unlock_drive()` + `read_volume_id()`).
///
/// Maps to [`UnlockError`]:
/// * no matching profile / Renesas (no firmware unlock implemented) →
/// [`UnlockError::FirmwareNotUnlockable`];
/// * a SCSI failure during the firmware-unlock handshake →
/// [`UnlockError::Scsi`] (via `?` / `From<Error>`);
/// * unlocked but no readable OEM VID → [`UnlockError::VidUnavailable`].
///
/// Any error makes libfreemkv fall through to the in-tree cert handshake.
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
id: &DriveId,
) -> std::result::Result<Vid, UnlockError> {
let Some(m) = profile::find_bundled(id) else {
// matches() returned true but the profile vanished — this unlocker
// cannot put the firmware into extended mode.
return Err(UnlockError::FirmwareNotUnlockable);
};
if matches!(m.platform, profile::Platform::Renesas) {
// Renesas firmware unlock is not implemented; cannot unlock the
// firmware, so the host-cert handshake must carry the disc.
return Err(UnlockError::FirmwareNotUnlockable);
}
let is_variant_b = matches!(m.platform, profile::Platform::Mt1959B);
use platform::PlatformDriver;
let mut mt = platform::mt1959::Mt1959::new(m.profile, is_variant_b);
// Firmware unlock. A SCSI failure becomes UnlockError::Scsi via `?`.
mt.init(scsi)?;
// On success, prime the per-region speed table so the drive manages
// zone speeds internally (best-effort — calibration failure must not
// fail an otherwise-good unlock).
let _ = mt.probe_disc(scsi);
// An unlocked drive must hand back its Volume ID in the same step.
self.read_oem_vid(scsi, id)
} }
/// Raise the drive to its maximum read speed. /// Raise the drive to its maximum read speed.
@@ -221,12 +241,12 @@ mod tests {
} }
/// A well-formed 36-byte response (signature 00 22 00, VID at [4..20]) /// A well-formed 36-byte response (signature 00 22 00, VID at [4..20])
/// parses to that VID. /// parses to that VID via the OEM-VID helper `unlock` folds in.
#[test] #[test]
fn read_vid_parses_well_formed_response() { fn read_oem_vid_parses_well_formed_response() {
// Confirm the bundled profile actually carries a read_vid_cdb; // Confirm the bundled profile actually carries a read_vid_cdb; otherwise
// otherwise read_vid would short-circuit to Ok(None) and this test // read_oem_vid would short-circuit and this test wouldn't exercise the
// wouldn't exercise the parse path. // parse path.
let m = profile::find_bundled(&known_vid_drive_id()).expect("profile match"); let m = profile::find_bundled(&known_vid_drive_id()).expect("profile match");
assert!( assert!(
m.profile.read_vid_cdb.is_some(), m.profile.read_vid_cdb.is_some(),
@@ -242,27 +262,28 @@ mod tests {
bytes_transferred: 36, bytes_transferred: 36,
}; };
let got = LibreDrive::new() let got = LibreDrive::new()
.read_volume_id(&mut t, &known_vid_drive_id()) .read_oem_vid(&mut t, &known_vid_drive_id())
.expect("parse ok"); .expect("parse ok");
assert_eq!(got, Some(vid), "VID parsed from [4..20]"); assert_eq!(got, Vid(vid), "VID parsed from [4..20]");
} }
/// A short response (fewer than 36 transferred bytes) → AacsVidRead. /// A short response (fewer than 36 transferred bytes) → VidUnavailable
/// (unlocked but no readable VID → cert fallback).
#[test] #[test]
fn read_vid_short_response_errors() { fn read_oem_vid_short_response_is_vid_unavailable() {
let mut t = FakeTransport { let mut t = FakeTransport {
payload: vec![0u8; 36], payload: vec![0u8; 36],
bytes_transferred: 20, bytes_transferred: 20,
}; };
let err = LibreDrive::new() let err = LibreDrive::new()
.read_volume_id(&mut t, &known_vid_drive_id()) .read_oem_vid(&mut t, &known_vid_drive_id())
.expect_err("short response must error"); .expect_err("short response must error");
assert!(matches!(err, Error::AacsVidRead)); assert_eq!(err, UnlockError::VidUnavailable);
} }
/// A response whose 3-byte signature isn't `00 22 00` → AacsVidRead. /// A response whose 3-byte signature isn't `00 22 00` → VidUnavailable.
#[test] #[test]
fn read_vid_bad_header_errors() { fn read_oem_vid_bad_header_is_vid_unavailable() {
let mut payload = vec![0u8; 36]; let mut payload = vec![0u8; 36];
payload[0..3].copy_from_slice(&[0xDE, 0xAD, 0xBE]); // wrong signature payload[0..3].copy_from_slice(&[0xDE, 0xAD, 0xBE]); // wrong signature
let mut t = FakeTransport { let mut t = FakeTransport {
@@ -270,23 +291,38 @@ mod tests {
bytes_transferred: 36, bytes_transferred: 36,
}; };
let err = LibreDrive::new() let err = LibreDrive::new()
.read_volume_id(&mut t, &known_vid_drive_id()) .read_oem_vid(&mut t, &known_vid_drive_id())
.expect_err("bad header must error"); .expect_err("bad header must error");
assert!(matches!(err, Error::AacsVidRead)); assert_eq!(err, UnlockError::VidUnavailable);
} }
/// A drive with no matching profile → Ok(None) (cert fallback), and the /// A drive with no matching profile → `read_oem_vid` is VidUnavailable
/// transport is never issued a CDB. /// (cert fallback), and the transport is never issued a CDB.
#[test] #[test]
fn read_vid_no_profile_is_none() { fn read_oem_vid_no_profile_is_vid_unavailable() {
let mut t = FakeTransport { let mut t = FakeTransport {
payload: vec![0u8; 36], payload: vec![0u8; 36],
bytes_transferred: 36, bytes_transferred: 36,
}; };
let got = LibreDrive::new() let err = LibreDrive::new()
.read_volume_id(&mut t, &make_drive_id("FAKE-VND", "9.99", "XX12345", "")) .read_oem_vid(&mut t, &make_drive_id("FAKE-VND", "9.99", "XX12345", ""))
.expect("no-profile is Ok(None)"); .expect_err("no profile → VidUnavailable");
assert!(got.is_none(), "no profile → cert fallback"); assert_eq!(err, UnlockError::VidUnavailable);
}
/// `unlock` on a drive with no matching profile → FirmwareNotUnlockable
/// (this unlocker cannot put the firmware into extended mode), short-
/// circuiting before any firmware handshake is attempted.
#[test]
fn unlock_no_profile_is_firmware_not_unlockable() {
let mut t = FakeTransport {
payload: vec![0u8; 36],
bytes_transferred: 36,
};
let err = LibreDrive::new()
.unlock(&mut t, &make_drive_id("FAKE-VND", "9.99", "XX12345", ""))
.expect_err("no profile → FirmwareNotUnlockable");
assert_eq!(err, UnlockError::FirmwareNotUnlockable);
} }
/// A transport that records the CDB issued (and how many CDBs it saw), /// A transport that records the CDB issued (and how many CDBs it saw),