use crate::aacs::Vid;
use crate::error::Result;
use crate::identity::DriveId;
use crate::scsi::ScsiTransport;
use std::sync::RwLock;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnlockError {
FirmwareNotUnlockable,
NoUsableHostCert { mkb: Option<u32> },
CertRevoked { mkb: Option<u32> },
HandshakeRejected,
VidUnavailable,
Scsi(u16),
}
impl From<crate::error::Error> for UnlockError {
fn from(e: crate::error::Error) -> Self {
UnlockError::Scsi(e.code())
}
}
pub trait Unlocker: Send + Sync {
fn name(&self) -> &str;
fn matches(&self, id: &DriveId) -> bool;
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
id: &DriveId,
) -> std::result::Result<Vid, UnlockError>;
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
Ok(())
}
}
static REGISTRY: RwLock<Vec<Box<dyn Unlocker>>> = RwLock::new(Vec::new());
pub fn register_unlocker(u: Box<dyn Unlocker>) {
if let Ok(mut reg) = REGISTRY.write() {
reg.push(u);
}
}
pub(crate) fn route_unlock(
scsi: &mut dyn ScsiTransport,
id: &DriveId,
) -> Result<Option<(String, Vid)>> {
let reg = match REGISTRY.read() {
Ok(r) => r,
Err(_) => return Ok(None),
};
for u in reg.iter() {
if u.matches(id) {
let name = u.name().to_string();
match u.unlock(scsi, id) {
Ok(vid) => return Ok(Some((name, vid))),
Err(UnlockError::Scsi(code)) => {
tracing::error!(
target: "freemkv::unlock",
unlocker = %name,
code,
"unlocker hit a transport fault during unlock; aborting init"
);
return Err(crate::error::Error::ScsiError {
opcode: 0,
status: 0,
sense: None,
});
}
Err(e) => {
tracing::warn!(
target: "freemkv::unlock",
unlocker = %name,
outcome = ?e,
"unlocker matched but produced no VID; falling through to cert handshake"
);
return Ok(None);
}
}
}
}
Ok(None)
}
pub(crate) fn unlocker_set_max_read_speed(
scsi: &mut dyn ScsiTransport,
id: &DriveId,
) -> Result<()> {
let reg = match REGISTRY.read() {
Ok(r) => r,
Err(_) => return Ok(()),
};
for u in reg.iter() {
if u.matches(id) {
return u.set_max_read_speed(scsi, id);
}
}
Ok(())
}
#[doc(hidden)]
pub fn registered_count() -> usize {
REGISTRY.read().map(|r| r.len()).unwrap_or(0)
}
pub(crate) fn matching_name(id: &DriveId) -> Option<String> {
let reg = REGISTRY.read().ok()?;
reg.iter()
.find(|u| u.matches(id))
.map(|u| u.name().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scsi::{DataDirection, ScsiResult, ScsiTransport};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct NoopTransport;
impl ScsiTransport for NoopTransport {
fn execute(
&mut self,
_cdb: &[u8],
_dir: DataDirection,
_data: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
Ok(ScsiResult {
status: 0,
bytes_transferred: 0,
sense: [0u8; 32],
})
}
}
fn fake_id(vendor: &str) -> DriveId {
let mut inquiry = vec![0u8; 96];
let v = vendor.as_bytes();
inquiry[8..8 + v.len().min(8)].copy_from_slice(&v[..v.len().min(8)]);
DriveId::from_inquiry(&inquiry, "")
}
struct FakeUnlocker {
want_vendor: String,
ran: Arc<AtomicBool>,
vid: Option<[u8; 16]>,
scsi_err: Option<u16>,
speed_ran: Arc<AtomicBool>,
}
impl FakeUnlocker {
fn new(vendor: &str, ran: Arc<AtomicBool>) -> Self {
Self {
want_vendor: vendor.into(),
ran,
vid: Some([0u8; 16]),
scsi_err: None,
speed_ran: Arc::new(AtomicBool::new(false)),
}
}
fn with_vid(mut self, vid: Option<[u8; 16]>) -> Self {
self.vid = vid;
self
}
fn with_scsi_err(mut self, code: u16) -> Self {
self.scsi_err = Some(code);
self
}
fn with_speed(mut self, speed_ran: Arc<AtomicBool>) -> Self {
self.speed_ran = speed_ran;
self
}
}
impl Unlocker for FakeUnlocker {
fn name(&self) -> &str {
"fake"
}
fn matches(&self, id: &DriveId) -> bool {
id.vendor_id.trim() == self.want_vendor
}
fn unlock(
&self,
_scsi: &mut dyn ScsiTransport,
_id: &DriveId,
) -> std::result::Result<Vid, UnlockError> {
self.ran.store(true, Ordering::SeqCst);
if let Some(code) = self.scsi_err {
return Err(UnlockError::Scsi(code));
}
match self.vid {
Some(v) => Ok(Vid(v)),
None => Err(UnlockError::VidUnavailable),
}
}
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
self.speed_ran.store(true, Ordering::SeqCst);
Ok(())
}
}
#[test]
fn unlock_error_from_crate_error_carries_code() {
let e: UnlockError = crate::error::Error::AacsVidUnavailable.into();
assert_eq!(e, UnlockError::Scsi(crate::error::E_AACS_VID_UNAVAILABLE));
assert_ne!(
UnlockError::NoUsableHostCert { mkb: Some(1) },
UnlockError::NoUsableHostCert { mkb: Some(2) }
);
}
#[test]
fn registry_routes_match_else_oem() {
let ran = Arc::new(AtomicBool::new(false));
register_unlocker(Box::new(FakeUnlocker::new("MATCHVND", ran.clone())));
let mut scsi = NoopTransport;
let matched = route_unlock(&mut scsi, &fake_id("MATCHVND")).unwrap();
assert_eq!(
matched.as_ref().map(|(n, _)| n.as_str()),
Some("fake"),
"matching unlocker runs"
);
assert!(ran.load(Ordering::SeqCst), "unlock() was invoked");
ran.store(false, Ordering::SeqCst);
let none = route_unlock(&mut scsi, &fake_id("OTHERVND")).unwrap();
assert!(none.is_none(), "no match → cert fallback");
assert!(
!ran.load(Ordering::SeqCst),
"unlock() not invoked on no-match"
);
}
#[test]
fn route_unlock_returns_vid_else_cert() {
let mut scsi = NoopTransport;
let vid = [0x5Au8; 16];
register_unlocker(Box::new(
FakeUnlocker::new("VIDVNDOR", Arc::new(AtomicBool::new(false))).with_vid(Some(vid)),
));
let got = route_unlock(&mut scsi, &fake_id("VIDVNDOR")).unwrap();
assert_eq!(
got.map(|(_, v)| v),
Some(Vid(vid)),
"matching unlocker's OEM VID is used"
);
register_unlocker(Box::new(
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false))).with_vid(None),
));
let got = route_unlock(&mut scsi, &fake_id("NOVIDVND")).unwrap();
assert!(
got.is_none(),
"unlocker without OEM VID falls through to cert"
);
let got = route_unlock(&mut scsi, &fake_id("UNKNWNVD")).unwrap();
assert!(got.is_none(), "no match → cert fallback");
}
#[test]
fn route_unlock_propagates_scsi_transport_fault() {
let mut scsi = NoopTransport;
register_unlocker(Box::new(
FakeUnlocker::new("SCSIVNDR", Arc::new(AtomicBool::new(false)))
.with_scsi_err(crate::error::E_SCSI_ERROR),
));
let got = route_unlock(&mut scsi, &fake_id("SCSIVNDR"));
assert!(
got.is_err(),
"a transport fault during unlock aborts init (propagates Err)"
);
assert_eq!(
got.unwrap_err().code(),
crate::error::E_SCSI_ERROR,
"propagated error is the canonical transport-error code"
);
}
#[test]
fn unlocker_set_max_read_speed_routes_match_else_noop() {
let mut scsi = NoopTransport;
let speed_ran = Arc::new(AtomicBool::new(false));
register_unlocker(Box::new(
FakeUnlocker::new("SPEEDVND", Arc::new(AtomicBool::new(false)))
.with_speed(speed_ran.clone()),
));
unlocker_set_max_read_speed(&mut scsi, &fake_id("SPEEDVND")).unwrap();
assert!(
speed_ran.load(Ordering::SeqCst),
"set_max_read_speed() invoked on match"
);
speed_ran.store(false, Ordering::SeqCst);
unlocker_set_max_read_speed(&mut scsi, &fake_id("NOSPEEDV")).unwrap();
assert!(
!speed_ran.load(Ordering::SeqCst),
"no match → safe no-op, nothing invoked"
);
}
#[test]
fn matching_name_and_registered_count_introspection() {
let before = registered_count();
register_unlocker(Box::new(FakeUnlocker::new(
"NAMEVNDR",
Arc::new(AtomicBool::new(false)),
)));
assert!(
registered_count() > before,
"registered_count grows after register_unlocker"
);
assert_eq!(
matching_name(&fake_id("NAMEVNDR")).as_deref(),
Some("fake"),
"matching_name reports the supporting unlocker"
);
assert!(
matching_name(&fake_id("ZZNOMTCH")).is_none(),
"matching_name is None for an unsupported drive"
);
}
#[test]
fn route_unlock_first_registered_match_wins() {
let mut scsi = NoopTransport;
let first_ran = Arc::new(AtomicBool::new(false));
let second_ran = Arc::new(AtomicBool::new(false));
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", first_ran.clone())));
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", second_ran.clone())));
let matched = route_unlock(&mut scsi, &fake_id("DUPEVNDR")).unwrap();
assert_eq!(
matched.as_ref().map(|(n, _)| n.as_str()),
Some("fake"),
"a match was routed"
);
assert!(
first_ran.load(Ordering::SeqCst),
"the FIRST-registered matching unlocker ran"
);
assert!(
!second_ran.load(Ordering::SeqCst),
"the later-registered unlocker was never consulted (first-match-wins)"
);
}
}