diff --git a/src/lib.rs b/src/lib.rs index 80cd288..8866082 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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) ──────────────────────────────────────────────── // diff --git a/src/unlock.rs b/src/unlock.rs index 8a7174c..e55eff1 100644 --- a/src/unlock.rs +++ b/src/unlock.rs @@ -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; + ) -> std::result::Result; /// 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, + /// 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>> = 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 { + ) -> std::result::Result { 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), } }