From d6d79fc74cc746ab70b36c8beb8c023efc99498b Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:50:54 -0700 Subject: [PATCH] Implement LibreDrive::read_vid OEM Volume ID retrieval Recover the per-drive READ_BUFFER OEM VID path that lived in libfreemkv before the unlocker refactor (read_vid_oem in disc/encrypt.rs), now living inside the unlocker where the per-drive read_vid_cdb template belongs. read_vid finds the drive's profile; if it carries a read_vid_cdb, issues it and parses the 36-byte response (signature 00 22 00, VID at [4..20]), returning Some(vid). No matching profile or no OEM VID CDB returns Ok(None) so libfreemkv falls back to the cert handshake. VID is thus obtained without the host certificate + HRL. Adds response-parse unit tests (well-formed, short response, bad header, no-profile fallback). --- src/lib.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 1ff9257..059b187 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,8 +24,9 @@ pub mod profile; mod platform; -use error::Result; +use error::{Error, Result}; use libfreemkv::{DriveId, ScsiTransport, Unlocker}; +use scsi::DataDirection; /// The LibreDrive unlocker. /// @@ -76,4 +77,187 @@ impl Unlocker for LibreDrive { let _ = mt.probe_disc(scsi); Ok(()) } + + /// OEM Volume ID retrieval. + /// + /// An unlocker unlocks drive *functionality*, not just the disc: VID + /// retrieval via the drive's OEM CDB is a capability separate from + /// `unlock`. This recovers the per-drive READ_BUFFER VID path that lived + /// in libfreemkv before the unlocker refactor (it was `read_vid_oem` in + /// `libfreemkv/src/disc/encrypt.rs`), now living inside the unlocker + /// where the per-drive `read_vid_cdb` template belongs. + /// + /// Returns: + /// * `Ok(Some(vid))` — the drive's profile carries an OEM VID CDB and + /// the drive served a well-formed VID. No host certificate or HRL is + /// involved — VID is decoupled from the cert chain. + /// * `Ok(None)` — no profile matched, or the profile carries no OEM VID + /// CDB; libfreemkv falls back to the cert-based VID read. + /// * `Err(_)` — the OEM CDB ran but returned a short or malformed + /// response. + /// + /// Response layout (36 bytes): + /// * `[0..3]` 3-byte response signature; expected `00 22 00` + /// * `[3]` reserved + /// * `[4..20]` 16-byte Volume ID + /// * `[20..36]` reserved / per-drive padding + fn read_vid(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result> { + const RESPONSE_LEN: usize = 36; + const EXPECTED_HEADER: [u8; 3] = [0x00, 0x22, 0x00]; + + // No matching profile, or a profile without an OEM VID CDB → no OEM + // path; the caller falls back to the cert handshake. + let Some(m) = profile::find_bundled(id) else { + return Ok(None); + }; + let Some(cdb) = m.profile.read_vid_cdb else { + return Ok(None); + }; + + let mut buf = vec![0u8; RESPONSE_LEN]; + let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; + if result.bytes_transferred < RESPONSE_LEN { + tracing::warn!( + target: "freemkv::disc", + phase = "oem_vid_short_response", + bytes_transferred = result.bytes_transferred, + "OEM VID CDB returned short response" + ); + return Err(Error::AacsVidRead); + } + if buf[0..3] != EXPECTED_HEADER { + tracing::warn!( + target: "freemkv::disc", + phase = "oem_vid_bad_header", + header_0 = buf[0], + header_1 = buf[1], + header_2 = buf[2], + "OEM VID response header mismatch" + ); + return Err(Error::AacsVidRead); + } + let mut vid = [0u8; 16]; + vid.copy_from_slice(&buf[4..20]); + tracing::debug!( + target: "freemkv::disc", + phase = "oem_vid_ok", + "OEM VID retrieved via unlocker" + ); + Ok(Some(vid)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scsi::{DataDirection, ScsiResult, ScsiTransport}; + + /// A fake transport that fills the response buffer from a fixed payload + /// and reports a configurable transferred-byte count. Exercises the OEM + /// VID response-parse branches without a live drive. + struct FakeTransport { + payload: Vec, + bytes_transferred: usize, + } + impl ScsiTransport for FakeTransport { + fn execute( + &mut self, + _cdb: &[u8], + _dir: DataDirection, + data: &mut [u8], + _timeout_ms: u32, + ) -> Result { + let n = self.payload.len().min(data.len()); + data[..n].copy_from_slice(&self.payload[..n]); + Ok(ScsiResult { + status: 0, + bytes_transferred: self.bytes_transferred, + sense: [0u8; 32], + }) + } + } + + /// A DriveId for the bundled HL-DT-ST profile that carries a real + /// `read_vid_cdb`, so `read_vid` finds a profile and issues the CDB. + fn known_vid_drive_id() -> DriveId { + make_drive_id("HL-DT-ST", "1.01", "NM00100", "211711202000") + } + + fn make_drive_id(vendor: &str, rev: &str, vs: &str, date: &str) -> DriveId { + let mut inquiry = vec![0u8; 96]; + inquiry[8..8 + vendor.len().min(8)] + .copy_from_slice(&vendor.as_bytes()[..vendor.len().min(8)]); + inquiry[32..32 + rev.len().min(4)].copy_from_slice(&rev.as_bytes()[..rev.len().min(4)]); + inquiry[36..36 + vs.len().min(7)].copy_from_slice(&vs.as_bytes()[..vs.len().min(7)]); + DriveId::from_inquiry(&inquiry, date) + } + + /// A well-formed 36-byte response (signature 00 22 00, VID at [4..20]) + /// parses to that VID. + #[test] + fn read_vid_parses_well_formed_response() { + // Confirm the bundled profile actually carries a read_vid_cdb; + // otherwise read_vid would short-circuit to Ok(None) and this test + // wouldn't exercise the parse path. + let m = profile::find_bundled(&known_vid_drive_id()).expect("profile match"); + assert!( + m.profile.read_vid_cdb.is_some(), + "test fixture drive must carry an OEM VID CDB" + ); + + let mut payload = vec![0u8; 36]; + payload[0..3].copy_from_slice(&[0x00, 0x22, 0x00]); + let vid = [0x3Cu8; 16]; + payload[4..20].copy_from_slice(&vid); + let mut t = FakeTransport { + payload, + bytes_transferred: 36, + }; + let got = LibreDrive::new() + .read_vid(&mut t, &known_vid_drive_id()) + .expect("parse ok"); + assert_eq!(got, Some(vid), "VID parsed from [4..20]"); + } + + /// A short response (fewer than 36 transferred bytes) → AacsVidRead. + #[test] + fn read_vid_short_response_errors() { + let mut t = FakeTransport { + payload: vec![0u8; 36], + bytes_transferred: 20, + }; + let err = LibreDrive::new() + .read_vid(&mut t, &known_vid_drive_id()) + .expect_err("short response must error"); + assert!(matches!(err, Error::AacsVidRead)); + } + + /// A response whose 3-byte signature isn't `00 22 00` → AacsVidRead. + #[test] + fn read_vid_bad_header_errors() { + let mut payload = vec![0u8; 36]; + payload[0..3].copy_from_slice(&[0xDE, 0xAD, 0xBE]); // wrong signature + let mut t = FakeTransport { + payload, + bytes_transferred: 36, + }; + let err = LibreDrive::new() + .read_vid(&mut t, &known_vid_drive_id()) + .expect_err("bad header must error"); + assert!(matches!(err, Error::AacsVidRead)); + } + + /// A drive with no matching profile → Ok(None) (cert fallback), and the + /// transport is never issued a CDB. + #[test] + fn read_vid_no_profile_is_none() { + let mut t = FakeTransport { + payload: vec![0u8; 36], + bytes_transferred: 36, + }; + let got = LibreDrive::new() + .read_vid(&mut t, &make_drive_id("FAKE-VND", "9.99", "XX12345", "")) + .expect("no-profile is Ok(None)"); + assert!(got.is_none(), "no profile → cert fallback"); + } }