unlock: introduce UnlockCtx + DiscKind; trait keys off context
Reshape the Unlocker seam so every unlocker is dispatched at ONE place from
ONE ordered registry — the firmware, cert, and CSS routes are all "remove the
bus-encryption barrier", differing only in what they key off. matches() and
unlock() now take an UnlockCtx { drive_id, kind: DiscKind } instead of a bare
DriveId: a firmware unlocker keys off drive_id (kind irrelevant), the cert
unlocker will match DiscKind::Aacs, the CSS unlocker DiscKind::Css. UnlockCtx
is #[non_exhaustive] so a host-cert source can be added without breaking
external unlockers. Drive-prep dispatch passes DiscKind::Unknown (no disc
probed yet); the cert/CSS registry impls + the single post-probe dispatch
point follow in subsequent commits.
This commit is contained in:
+13
-4
@@ -410,7 +410,12 @@ impl Drive {
|
|||||||
// Walk the unlock registry: the first unlocker whose identity
|
// Walk the unlock registry: the first unlocker whose identity
|
||||||
// matches runs; none matching leaves the drive in stock mode so the
|
// matches runs; none matching leaves the drive in stock mode so the
|
||||||
// host-cert AACS handshake (the OEM route) carries the disc.
|
// host-cert AACS handshake (the OEM route) carries the disc.
|
||||||
let r = crate::unlock::route_unlock(self.scsi.as_mut(), &self.drive_id);
|
// Drive-prep dispatch: disc structure has not been probed yet, so the
|
||||||
|
// kind is Unknown — only a drive-keyed (firmware) unlocker can match.
|
||||||
|
let r = crate::unlock::route_unlock(
|
||||||
|
self.scsi.as_mut(),
|
||||||
|
&crate::unlock::UnlockCtx::new(&self.drive_id, crate::unlock::DiscKind::Unknown),
|
||||||
|
);
|
||||||
self.init_ran = true;
|
self.init_ran = true;
|
||||||
let r = match r {
|
let r = match r {
|
||||||
Ok(Some((name, vid))) => {
|
Ok(Some((name, vid))) => {
|
||||||
@@ -421,9 +426,13 @@ impl Drive {
|
|||||||
// The matched unlocker may also be able to raise the drive to
|
// The matched unlocker may also be able to raise the drive to
|
||||||
// its maximum read speed. Best-effort: a failure here must NOT
|
// its maximum read speed. Best-effort: a failure here must NOT
|
||||||
// fail the rip — a slow drive still rips. Log and continue.
|
// fail the rip — a slow drive still rips. Log and continue.
|
||||||
if let Err(e) =
|
if let Err(e) = crate::unlock::unlocker_set_max_read_speed(
|
||||||
crate::unlock::unlocker_set_max_read_speed(self.scsi.as_mut(), &self.drive_id)
|
self.scsi.as_mut(),
|
||||||
{
|
&crate::unlock::UnlockCtx::new(
|
||||||
|
&self.drive_id,
|
||||||
|
crate::unlock::DiscKind::Unknown,
|
||||||
|
),
|
||||||
|
) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "freemkv::drive",
|
target: "freemkv::drive",
|
||||||
phase = "init",
|
phase = "init",
|
||||||
|
|||||||
+1
-1
@@ -183,7 +183,7 @@ pub use identity::DriveId;
|
|||||||
// and registers it once at process start via `register_unlocker`. At
|
// and registers it once at process start via `register_unlocker`. At
|
||||||
// drive-prep the registry is walked in order; the first matching unlocker
|
// drive-prep the registry is walked in order; the first matching unlocker
|
||||||
// runs, else the drive falls through to the host-cert AACS handshake.
|
// runs, else the drive falls through to the host-cert AACS handshake.
|
||||||
pub use unlock::{UnlockError, Unlocked, Unlocker, register_unlocker};
|
pub use unlock::{DiscKind, UnlockCtx, UnlockError, Unlocked, Unlocker, register_unlocker};
|
||||||
|
|
||||||
// ─── Decryption (AACS / CSS) ────────────────────────────────────────────────
|
// ─── Decryption (AACS / CSS) ────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
|
|||||||
+106
-24
@@ -60,8 +60,11 @@ pub trait Unlocker: Send + Sync {
|
|||||||
/// Stable, language-neutral identifier for this unlocker (logged).
|
/// Stable, language-neutral identifier for this unlocker (logged).
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
/// True if this unlocker handles the given drive.
|
/// True if this unlocker applies in the given [`UnlockCtx`]. A firmware
|
||||||
fn matches(&self, id: &DriveId) -> bool;
|
/// unlocker keys off `ctx.drive_id` (disc kind irrelevant); the cert
|
||||||
|
/// unlocker matches `ctx.kind == DiscKind::Aacs`; the CSS unlocker matches
|
||||||
|
/// `DiscKind::Css`.
|
||||||
|
fn matches(&self, ctx: &UnlockCtx) -> bool;
|
||||||
|
|
||||||
/// Put the drive into extended-access mode (firmware/bootloader/whatever
|
/// Put the drive into extended-access mode (firmware/bootloader/whatever
|
||||||
/// THIS unlocker needs) and report what it LEARNED — see [`Unlocked`]. The
|
/// THIS unlocker needs) and report what it LEARNED — see [`Unlocked`]. The
|
||||||
@@ -74,15 +77,52 @@ pub trait Unlocker: Send + Sync {
|
|||||||
fn unlock(
|
fn unlock(
|
||||||
&self,
|
&self,
|
||||||
scsi: &mut dyn ScsiTransport,
|
scsi: &mut dyn ScsiTransport,
|
||||||
id: &DriveId,
|
ctx: &UnlockCtx,
|
||||||
) -> std::result::Result<Unlocked, UnlockError>;
|
) -> std::result::Result<Unlocked, UnlockError>;
|
||||||
|
|
||||||
/// Raise the drive to its maximum read speed. Default: no-op.
|
/// Raise the drive to its maximum read speed. Default: no-op.
|
||||||
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _ctx: &UnlockCtx) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bus-encryption class of the loaded disc, as cheaply probed before the
|
||||||
|
/// full structure scan. An [`Unlocker::matches`] keys off this (plus the drive
|
||||||
|
/// identity in [`UnlockCtx`]): a firmware unlocker ignores it; the cert unlocker
|
||||||
|
/// matches [`DiscKind::Aacs`]; the CSS unlocker matches [`DiscKind::Css`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DiscKind {
|
||||||
|
/// Not yet probed — drive-prep phase, before any disc structure is read.
|
||||||
|
Unknown,
|
||||||
|
/// Disc carries no bus encryption; nothing to remove.
|
||||||
|
Unencrypted,
|
||||||
|
/// AACS (Blu-ray / UHD).
|
||||||
|
Aacs,
|
||||||
|
/// CSS (DVD-Video).
|
||||||
|
Css,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context handed to every [`Unlocker`] at the single dispatch point: the drive
|
||||||
|
/// identity and the disc's bus-encryption [`DiscKind`]. An unlocker reads only
|
||||||
|
/// what it needs — firmware keys off [`Self::drive_id`]; cert/CSS off
|
||||||
|
/// [`Self::kind`]. `#[non_exhaustive]` so more context (e.g. a host-cert source)
|
||||||
|
/// can be added later without breaking external unlockers.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct UnlockCtx<'a> {
|
||||||
|
/// Identity of the drive being unlocked.
|
||||||
|
pub drive_id: &'a DriveId,
|
||||||
|
/// Bus-encryption class of the loaded disc (`Unknown` during drive-prep).
|
||||||
|
pub kind: DiscKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UnlockCtx<'a> {
|
||||||
|
/// Construct a context for the given drive and disc kind.
|
||||||
|
pub fn new(drive_id: &'a DriveId, kind: DiscKind) -> Self {
|
||||||
|
Self { drive_id, kind }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What an [`Unlocker::unlock`] LEARNED. The hardware side-effect (the drive
|
/// What an [`Unlocker::unlock`] LEARNED. The hardware side-effect (the drive
|
||||||
/// entering extended mode, or CSS auth setting the ASF flag) already happened
|
/// entering extended mode, or CSS auth setting the ASF flag) already happened
|
||||||
/// inside `unlock`; this carries only the learned data, which libfreemkv files
|
/// inside `unlock`; this carries only the learned data, which libfreemkv files
|
||||||
@@ -135,7 +175,7 @@ pub fn register_unlocker(u: Box<dyn Unlocker>) {
|
|||||||
/// a cert handshake that would also fail.
|
/// a cert handshake that would also fail.
|
||||||
pub(crate) fn route_unlock(
|
pub(crate) fn route_unlock(
|
||||||
scsi: &mut dyn ScsiTransport,
|
scsi: &mut dyn ScsiTransport,
|
||||||
id: &DriveId,
|
ctx: &UnlockCtx,
|
||||||
) -> Result<Option<(String, Vid)>> {
|
) -> Result<Option<(String, Vid)>> {
|
||||||
let reg = match REGISTRY.read() {
|
let reg = match REGISTRY.read() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -146,9 +186,9 @@ pub(crate) fn route_unlock(
|
|||||||
// Walk in registration order — the registry is the single ordered place
|
// Walk in registration order — the registry is the single ordered place
|
||||||
// that decides which unlocker runs first (register ld, then aacs, then css).
|
// that decides which unlocker runs first (register ld, then aacs, then css).
|
||||||
for u in reg.iter() {
|
for u in reg.iter() {
|
||||||
if u.matches(id) {
|
if u.matches(ctx) {
|
||||||
let name = u.name().to_string();
|
let name = u.name().to_string();
|
||||||
match u.unlock(scsi, id) {
|
match u.unlock(scsi, ctx) {
|
||||||
// The firmware route reports a VID; `read_data_key` is None here
|
// The firmware route reports a VID; `read_data_key` is None here
|
||||||
// (this is the firmware seam — the cert handshake supplies the
|
// (this is the firmware seam — the cert handshake supplies the
|
||||||
// bus key on its own path). A drive that unlocked but produced no
|
// bus key on its own path). A drive that unlocked but produced no
|
||||||
@@ -207,7 +247,7 @@ pub(crate) fn route_unlock(
|
|||||||
/// still rips.
|
/// still rips.
|
||||||
pub(crate) fn unlocker_set_max_read_speed(
|
pub(crate) fn unlocker_set_max_read_speed(
|
||||||
scsi: &mut dyn ScsiTransport,
|
scsi: &mut dyn ScsiTransport,
|
||||||
id: &DriveId,
|
ctx: &UnlockCtx,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let reg = match REGISTRY.read() {
|
let reg = match REGISTRY.read() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -215,8 +255,8 @@ pub(crate) fn unlocker_set_max_read_speed(
|
|||||||
Err(_) => return Ok(()),
|
Err(_) => return Ok(()),
|
||||||
};
|
};
|
||||||
for u in reg.iter() {
|
for u in reg.iter() {
|
||||||
if u.matches(id) {
|
if u.matches(ctx) {
|
||||||
return u.set_max_read_speed(scsi, id);
|
return u.set_max_read_speed(scsi, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -232,9 +272,12 @@ pub fn registered_count() -> usize {
|
|||||||
/// running it. Used for drive-info display ("is this drive supported?")
|
/// running it. Used for drive-info display ("is this drive supported?")
|
||||||
/// before any unlock has been attempted.
|
/// before any unlock has been attempted.
|
||||||
pub(crate) fn matching_name(id: &DriveId) -> Option<String> {
|
pub(crate) fn matching_name(id: &DriveId) -> Option<String> {
|
||||||
|
// Drive-info introspection runs before any disc probe, so the kind is
|
||||||
|
// Unknown — only a drive-keyed (firmware) unlocker can match here.
|
||||||
|
let ctx = UnlockCtx::new(id, DiscKind::Unknown);
|
||||||
let reg = REGISTRY.read().ok()?;
|
let reg = REGISTRY.read().ok()?;
|
||||||
reg.iter()
|
reg.iter()
|
||||||
.find(|u| u.matches(id))
|
.find(|u| u.matches(&ctx))
|
||||||
.map(|u| u.name().to_string())
|
.map(|u| u.name().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,13 +358,13 @@ mod tests {
|
|||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"fake"
|
"fake"
|
||||||
}
|
}
|
||||||
fn matches(&self, id: &DriveId) -> bool {
|
fn matches(&self, ctx: &UnlockCtx) -> bool {
|
||||||
id.vendor_id.trim() == self.want_vendor
|
ctx.drive_id.vendor_id.trim() == self.want_vendor
|
||||||
}
|
}
|
||||||
fn unlock(
|
fn unlock(
|
||||||
&self,
|
&self,
|
||||||
_scsi: &mut dyn ScsiTransport,
|
_scsi: &mut dyn ScsiTransport,
|
||||||
_id: &DriveId,
|
_ctx: &UnlockCtx,
|
||||||
) -> std::result::Result<Unlocked, UnlockError> {
|
) -> std::result::Result<Unlocked, UnlockError> {
|
||||||
self.ran.store(true, Ordering::SeqCst);
|
self.ran.store(true, Ordering::SeqCst);
|
||||||
if let Some(code) = self.scsi_err {
|
if let Some(code) = self.scsi_err {
|
||||||
@@ -335,7 +378,11 @@ mod tests {
|
|||||||
None => Err(UnlockError::VidUnavailable),
|
None => Err(UnlockError::VidUnavailable),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId) -> Result<()> {
|
fn set_max_read_speed(
|
||||||
|
&self,
|
||||||
|
_scsi: &mut dyn ScsiTransport,
|
||||||
|
_ctx: &UnlockCtx,
|
||||||
|
) -> Result<()> {
|
||||||
self.speed_ran.store(true, Ordering::SeqCst);
|
self.speed_ran.store(true, Ordering::SeqCst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -367,7 +414,11 @@ mod tests {
|
|||||||
|
|
||||||
// Matching identity → unlocker runs, returns its name + VID.
|
// Matching identity → unlocker runs, returns its name + VID.
|
||||||
let mut scsi = NoopTransport;
|
let mut scsi = NoopTransport;
|
||||||
let matched = route_unlock(&mut scsi, &fake_id("MATCHVND")).unwrap();
|
let matched = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("MATCHVND"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matched.as_ref().map(|(n, _)| n.as_str()),
|
matched.as_ref().map(|(n, _)| n.as_str()),
|
||||||
Some("fake"),
|
Some("fake"),
|
||||||
@@ -377,7 +428,11 @@ mod tests {
|
|||||||
|
|
||||||
// Non-matching identity → no unlocker runs, cert path (None).
|
// Non-matching identity → no unlocker runs, cert path (None).
|
||||||
ran.store(false, Ordering::SeqCst);
|
ran.store(false, Ordering::SeqCst);
|
||||||
let none = route_unlock(&mut scsi, &fake_id("OTHERVND")).unwrap();
|
let none = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("OTHERVND"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(none.is_none(), "no match → cert fallback");
|
assert!(none.is_none(), "no match → cert fallback");
|
||||||
assert!(
|
assert!(
|
||||||
!ran.load(Ordering::SeqCst),
|
!ran.load(Ordering::SeqCst),
|
||||||
@@ -404,7 +459,11 @@ mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Matching identity → its VID is returned.
|
// Matching identity → its VID is returned.
|
||||||
let got = route_unlock(&mut scsi, &fake_id("VIDVNDOR")).unwrap();
|
let got = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("VIDVNDOR"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got.map(|(_, v)| v),
|
got.map(|(_, v)| v),
|
||||||
Some(Vid(vid)),
|
Some(Vid(vid)),
|
||||||
@@ -415,14 +474,22 @@ mod tests {
|
|||||||
register_unlocker(Box::new(
|
register_unlocker(Box::new(
|
||||||
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false))).with_vid(None),
|
FakeUnlocker::new("NOVIDVND", Arc::new(AtomicBool::new(false))).with_vid(None),
|
||||||
));
|
));
|
||||||
let got = route_unlock(&mut scsi, &fake_id("NOVIDVND")).unwrap();
|
let got = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("NOVIDVND"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
got.is_none(),
|
got.is_none(),
|
||||||
"unlocker without OEM VID falls through to cert"
|
"unlocker without OEM VID falls through to cert"
|
||||||
);
|
);
|
||||||
|
|
||||||
// No matching unlocker → Ok(None), cert fallback.
|
// No matching unlocker → Ok(None), cert fallback.
|
||||||
let got = route_unlock(&mut scsi, &fake_id("UNKNWNVD")).unwrap();
|
let got = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("UNKNWNVD"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(got.is_none(), "no match → cert fallback");
|
assert!(got.is_none(), "no match → cert fallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +508,10 @@ mod tests {
|
|||||||
.with_scsi_err(crate::error::E_SCSI_ERROR),
|
.with_scsi_err(crate::error::E_SCSI_ERROR),
|
||||||
));
|
));
|
||||||
|
|
||||||
let got = route_unlock(&mut scsi, &fake_id("SCSIVNDR"));
|
let got = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("SCSIVNDR"), DiscKind::Unknown),
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
got.is_err(),
|
got.is_err(),
|
||||||
"a transport fault during unlock aborts init (propagates Err)"
|
"a transport fault during unlock aborts init (propagates Err)"
|
||||||
@@ -470,7 +540,11 @@ mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Matching identity → set_max_read_speed invoked.
|
// Matching identity → set_max_read_speed invoked.
|
||||||
unlocker_set_max_read_speed(&mut scsi, &fake_id("SPEEDVND")).unwrap();
|
unlocker_set_max_read_speed(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("SPEEDVND"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
speed_ran.load(Ordering::SeqCst),
|
speed_ran.load(Ordering::SeqCst),
|
||||||
"set_max_read_speed() invoked on match"
|
"set_max_read_speed() invoked on match"
|
||||||
@@ -478,7 +552,11 @@ mod tests {
|
|||||||
|
|
||||||
// No matching unlocker → Ok(()), nothing invoked (safe no-op).
|
// No matching unlocker → Ok(()), nothing invoked (safe no-op).
|
||||||
speed_ran.store(false, Ordering::SeqCst);
|
speed_ran.store(false, Ordering::SeqCst);
|
||||||
unlocker_set_max_read_speed(&mut scsi, &fake_id("NOSPEEDV")).unwrap();
|
unlocker_set_max_read_speed(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("NOSPEEDV"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
!speed_ran.load(Ordering::SeqCst),
|
!speed_ran.load(Ordering::SeqCst),
|
||||||
"no match → safe no-op, nothing invoked"
|
"no match → safe no-op, nothing invoked"
|
||||||
@@ -544,7 +622,11 @@ mod tests {
|
|||||||
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", first_ran.clone())));
|
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", first_ran.clone())));
|
||||||
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", second_ran.clone())));
|
register_unlocker(Box::new(FakeUnlocker::new("DUPEVNDR", second_ran.clone())));
|
||||||
|
|
||||||
let matched = route_unlock(&mut scsi, &fake_id("DUPEVNDR")).unwrap();
|
let matched = route_unlock(
|
||||||
|
&mut scsi,
|
||||||
|
&UnlockCtx::new(&fake_id("DUPEVNDR"), DiscKind::Unknown),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
matched.as_ref().map(|(n, _)| n.as_str()),
|
matched.as_ref().map(|(n, _)| n.as_str()),
|
||||||
Some("fake"),
|
Some("fake"),
|
||||||
|
|||||||
Reference in New Issue
Block a user