collapse freemkv-unlock into one crate; ld becomes a module

Replace the ld/aacs/css member-crate workspace with a single freemkv-unlock
crate: the generic Unlocker contract + SCSI transport contract at the crate
root (lib.rs, scsi.rs, error.rs), and the firmware unlocker as the self-
contained src/ld module. The contract is repo-agnostic raw types (DriveId,
HostCert, DiscKind, Unlocked, UnlockError) with NO libfreemkv dependency, so
libfreemkv will depend on this crate (one-way, no cycle) and dispatch via
all_unlockers(). ld now impl crate::Unlocker, takes its own DriveId (4 raw
fields, no INQUIRY parsing), and returns a raw Option<[u8;16]> VID. The old
aacs/css plugin wrappers are removed; their crypto moves in from libfreemkv in
the next stages. 31 tests pass.
This commit is contained in:
Matthew Jackson
2026-06-29 19:11:21 -07:00
parent 58a4b9720f
commit 314c365290
19 changed files with 608 additions and 910 deletions
+137
View File
@@ -0,0 +1,137 @@
//! freemkv-unlock — the unlock layer for the freemkv toolchain.
//!
//! An **unlocker removes a drive-level bus-encryption barrier** so the drive
//! serves readable (de-bus'd / de-scrambled) sectors. Content-key decryption is
//! a separate layer — the consumer's (libfreemkv's) job.
//!
//! This crate defines the [`Unlocker`] contract + the SCSI transport contract,
//! and holds the self-contained unlocker modules (firmware / AACS cert / CSS).
//! libfreemkv depends on this crate and dispatches via [`all_unlockers`]; it
//! never names an individual unlocker. To remove an unlocker, delete its module
//! dir and its one line in [`all_unlockers`] — nothing else changes.
pub mod error;
pub mod scsi;
mod ld;
// mod aacs; // stage 2 — AACS host-certificate handshake
// mod css; // stage 3 — CSS bus-auth
use scsi::ScsiTransport;
/// Drive identity an unlocker matches against — four raw INQUIRY-derived fields,
/// filled by the consumer (this crate parses no INQUIRY itself).
#[derive(Debug, Clone, Default)]
pub struct DriveId {
pub vendor_id: String,
pub product_revision: String,
pub vendor_specific: String,
pub firmware_date: String,
}
/// Bus-encryption class of the mounted disc, probed by the consumer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscKind {
Unknown,
Unencrypted,
Aacs,
Css,
}
/// A host certificate for the AACS cert handshake (raw; the consumer collects
/// these from its key sources and passes them in).
#[derive(Debug, Clone)]
pub struct HostCert {
pub private_key: [u8; 20],
pub certificate: Vec<u8>,
}
/// Context handed to an unlocker: drive identity, disc kind, and (for the cert
/// route) the host certs the consumer collected.
pub struct UnlockCtx<'a> {
pub drive_id: &'a DriveId,
pub kind: DiscKind,
pub host_certs: &'a [HostCert],
}
impl<'a> UnlockCtx<'a> {
pub fn new(drive_id: &'a DriveId, kind: DiscKind, host_certs: &'a [HostCert]) -> Self {
Self {
drive_id,
kind,
host_certs,
}
}
}
/// What removing bus encryption yielded. `drive_unlocked` means the drive now
/// serves clear content (firmware route) — equivalent, for the gate, to a cert
/// `bus_key`.
#[derive(Debug, Clone, Default)]
pub struct Unlocked {
pub vid: Option<[u8; 16]>,
pub bus_key: Option<[u8; 16]>,
pub drive_unlocked: bool,
}
/// Why an unlock produced no usable result. Only `Transport` is a hard error
/// (bus dead → consumer aborts); the rest mean "fall through to the next
/// unlocker".
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnlockError {
/// This unlocker does not apply (wrong disc kind / no profile / no certs).
NotApplicable,
/// The AACS cert route had no usable host certificate.
NoUsableHostCert,
/// The drive rejected the auth handshake.
HandshakeRejected,
/// Auth succeeded but no Volume ID could be read.
VidUnavailable,
/// A genuine SCSI transport fault (bus dead). The consumer aborts.
Transport,
}
impl From<error::Error> for UnlockError {
fn from(e: error::Error) -> Self {
if e.is_transport_failure() {
UnlockError::Transport
} else {
// A logical firmware/handshake failure is not a dead bus — this
// unlocker simply didn't apply; the consumer falls through.
UnlockError::NotApplicable
}
}
}
/// An unlocker removes a drive-level bus-encryption barrier. Implementors are
/// the self-contained modules in this crate; the consumer only ever sees the
/// trait, via [`all_unlockers`].
pub trait Unlocker: Send + Sync {
/// True if this unlocker applies to the given context (drive id + disc kind).
fn matches(&self, ctx: &UnlockCtx) -> bool;
/// Remove the bus-encryption barrier, returning what was learned.
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
ctx: &UnlockCtx,
) -> std::result::Result<Unlocked, UnlockError>;
/// Best-effort: raise the drive to its maximum read speed. Default no-op.
fn set_max_read_speed(
&self,
_scsi: &mut dyn ScsiTransport,
_ctx: &UnlockCtx,
) -> error::Result<()> {
Ok(())
}
}
/// Every unlocker, in dispatch order (firmware → cert → css). This is the ONLY
/// place an unlocker is named. Remove one = delete its line here + its module
/// dir; the consumer never changes.
pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
vec![
Box::new(ld::LibreDrive::new()),
// Box::new(aacs::AacsCert::new()), // stage 2
// Box::new(css::Css::new()), // stage 3
]
}