unlock: Unlocker::unlock returns Unlocked { vid, read_data_key }

Generalize the unlock seam toward uniform unlockers (firmware / cert / CSS).
`unlock` now reports what it LEARNED — a Volume ID and/or an AACS bus key —
instead of just a Vid; libfreemkv files those onto the disc/drive in one
place, so a plugin never touches Disc. The hardware side-effect (extended
mode) still happens inside unlock. route_unlock walks the registry in
registration order (the single ordered place that decides run order); no
priority field. Firmware route reports {vid: Some, read_data_key: None}.
This commit is contained in:
Matthew Jackson
2026-06-29 15:09:17 -07:00
parent a7c8ee09b0
commit 93fbfac6f0
2 changed files with 40 additions and 10 deletions
+1 -1
View File
@@ -183,7 +183,7 @@ pub use identity::DriveId;
// and registers it once at process start via `register_unlocker`. At
// drive-prep the registry is walked in order; the first matching unlocker
// runs, else the drive falls through to the host-cert AACS handshake.
pub use unlock::{UnlockError, Unlocker, register_unlocker};
pub use unlock::{UnlockError, Unlocked, Unlocker, register_unlocker};
// ─── Decryption (AACS / CSS) ────────────────────────────────────────────────
//
+39 -9
View File
@@ -64,16 +64,18 @@ pub trait Unlocker: Send + Sync {
fn matches(&self, id: &DriveId) -> bool;
/// Put the drive into extended-access mode (firmware/bootloader/whatever
/// THIS unlocker needs) AND return the disc's Volume ID — folding the old
/// `unlock_drive()` + `read_volume_id()` into one step. A firmware unlocker
/// that cannot unlock returns [`UnlockError::FirmwareNotUnlockable`]; one
/// that unlocks but has no OEM VID path returns [`UnlockError::VidUnavailable`].
/// Either error makes libfreemkv fall through to the in-tree cert handshake.
/// THIS unlocker needs) and report what it LEARNED — see [`Unlocked`]. The
/// hardware side-effect (extended mode / auth flag) happens here; the
/// returned value is only the learned data (VID, bus key), which libfreemkv
/// files onto the disc/drive in one place. A firmware unlocker that cannot
/// unlock returns [`UnlockError::FirmwareNotUnlockable`]; one that unlocks
/// but has no OEM VID returns an [`Unlocked`] with `vid: None`. Either makes
/// libfreemkv fall through to the next unlocker / the cert handshake.
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
id: &DriveId,
) -> std::result::Result<Vid, UnlockError>;
) -> std::result::Result<Unlocked, UnlockError>;
/// Raise the drive to its maximum read speed. Default: no-op.
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
@@ -81,6 +83,22 @@ pub trait Unlocker: Send + Sync {
}
}
/// What an [`Unlocker::unlock`] LEARNED. The hardware side-effect (the drive
/// entering extended mode, or CSS auth setting the ASF flag) already happened
/// inside `unlock`; this carries only the learned data, which libfreemkv files
/// onto the disc/drive in a single place (the plugin never touches `Disc`).
///
/// - a firmware unlocker: `{ vid: Some, read_data_key: None }` (serves clear)
/// - the cert handshake: `{ vid: Some, read_data_key: Some }` (AACS bus key)
/// - CSS auth: `{ vid: None, read_data_key: None }` (reads enabled)
#[derive(Debug, Default, Clone)]
pub struct Unlocked {
/// Disc Volume ID, if this route obtained one.
pub vid: Option<Vid>,
/// AACS 2.x bus key (`read_data_key`) from the cert handshake, if any.
pub read_data_key: Option<[u8; 16]>,
}
/// Process-wide ordered registry of unlockers.
static REGISTRY: RwLock<Vec<Box<dyn Unlocker>>> = RwLock::new(Vec::new());
@@ -125,11 +143,20 @@ pub(crate) fn route_unlock(
// "no unlocker available" so the cert fallback still runs.
Err(_) => return Ok(None),
};
// Walk in registration order — the registry is the single ordered place
// that decides which unlocker runs first (register ld, then aacs, then css).
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))),
// The firmware route reports a VID; `read_data_key` is None here
// (this is the firmware seam — the cert handshake supplies the
// bus key on its own path). A drive that unlocked but produced no
// VID falls through to the cert handshake.
Ok(unlocked) => match unlocked.vid {
Some(vid) => return Ok(Some((name, vid))),
None => return Ok(None),
},
// A genuine SCSI/transport fault is not "this drive can't be
// unlocked" — the bus is broken. Propagate so init() aborts
// instead of falling through to a cert handshake that will
@@ -295,13 +322,16 @@ mod tests {
&self,
_scsi: &mut dyn ScsiTransport,
_id: &DriveId,
) -> std::result::Result<Vid, UnlockError> {
) -> std::result::Result<Unlocked, 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)),
Some(v) => Ok(Unlocked {
vid: Some(Vid(v)),
read_data_key: None,
}),
None => Err(UnlockError::VidUnavailable),
}
}