css: new freemkv-unlock-css plugin crate

The CSS (DVD-Video) bus-auth unlocker as a first-class external plugin, peer
to freemkv-unlock-ld. CssUnlocker impls libfreemkv::Unlocker — matches
DiscKind::Css, self-guards against the hardware (refuses NotApplicable without
issuing a CSS CDB if the drive reports a non-DVD profile), then runs the
libfreemkv bus-auth primitive (unlock_css_reads). libfreemkv keeps the bus-auth
+ keyless descramble; this crate owns the unlocker impl. Additive: a consuming
binary opts in with one register_unlocker(CssUnlocker::new()) line; the in-tree
built-in stays until clients are rewired.
This commit is contained in:
Matthew Jackson
2026-06-29 17:22:50 -07:00
parent 90fbf4164e
commit 26a2859eb4
3 changed files with 176 additions and 2 deletions
+3 -2
View File
@@ -6,8 +6,9 @@
# unlocker is deleting that one line plus the dependency (delete-to-comply).
#
# Members:
# ld — LibreDrive (MediaTek MT1959 firmware unlock)
# ld — LibreDrive (MediaTek MT1959 firmware unlock)
# css — CSS (DVD-Video) bus-auth unlock
# (future unlockers go here as additional members)
[workspace]
resolver = "2"
members = ["ld"]
members = ["ld", "css"]
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "freemkv-unlock-css"
version = "1.2.0"
edition = "2024"
rust-version = "1.86"
license = "AGPL-3.0-only"
description = "CSS (DVD-Video) bus-auth unlocker plugin for libfreemkv"
repository = "https://github.com/freemkv/freemkv-unlock"
[dependencies]
libfreemkv = "1.2.0"
tracing = "0.1"
+161
View File
@@ -0,0 +1,161 @@
//! freemkv-unlock-css — the CSS (DVD-Video) unlocker plugin for libfreemkv.
//!
//! A CSS-enforcing DVD drive refuses to return scrambled sectors until a CSS
//! bus-authentication handshake has set its Authentication Success Flag
//! (ASF=1). This crate owns the [`libfreemkv::Unlocker`] impl that performs
//! that unlock; libfreemkv owns the bus-auth primitive
//! ([`libfreemkv::css::auth::unlock_css_reads`]) and the keyless descramble.
//!
//! Plug it in once at process start:
//!
//! ```no_run
//! libfreemkv::register_unlocker(Box::new(freemkv_unlock_css::CssUnlocker::new()));
//! ```
//!
//! Remove the unlocker by deleting that one line and the dependency.
use libfreemkv::scsi::{DataDirection, SCSI_GET_CONFIGURATION, ScsiTransport};
use libfreemkv::{DiscKind, UnlockCtx, UnlockError, Unlocked, Unlocker};
/// The CSS unlocker. Matches a DVD (`DiscKind::Css`), self-verifies the drive
/// reports a DVD profile, then runs the CSS bus-auth handshake to unlock
/// scrambled-sector reads. It learns neither a Volume ID nor a bus key — the
/// descramble key is recovered keylessly (the Stevenson attack in libfreemkv).
pub struct CssUnlocker;
impl CssUnlocker {
pub fn new() -> Self {
CssUnlocker
}
}
impl Default for CssUnlocker {
fn default() -> Self {
Self::new()
}
}
impl Unlocker for CssUnlocker {
fn name(&self) -> &str {
"css"
}
fn matches(&self, ctx: &UnlockCtx) -> bool {
ctx.kind == DiscKind::Css
}
fn unlock(
&self,
scsi: &mut dyn ScsiTransport,
_ctx: &UnlockCtx,
) -> std::result::Result<Unlocked, UnlockError> {
// Self-guard against the hardware — do NOT trust the caller-declared
// DiscKind alone. If the drive does not report a DVD profile, refuse
// (NotApplicable) WITHOUT issuing any CSS CDB, so a mis-routed
// Blu-ray/UHD is never sent CSS bus-auth.
if !mounted_disc_is_dvd(scsi) {
tracing::debug!(
target: "freemkv::css",
phase = "css_unlocker_not_dvd",
"CssUnlocker invoked on a non-DVD profile; refusing (NotApplicable)"
);
return Err(UnlockError::NotApplicable);
}
// The bus-auth handshake (libfreemkv primitive) sets ASF=1; the lba is
// not consumed by the unlock. CSS yields no VID and no bus key.
libfreemkv::css::auth::unlock_css_reads(scsi, 0).map_err(UnlockError::from)?;
Ok(Unlocked::default())
}
}
/// Transport-level "is the mounted disc a DVD?" probe (GET CONFIGURATION
/// current-profile, DVD family `0x0010..=0x001F`). Keeps the CSS self-guard
/// inside the unlocker that needs it.
fn mounted_disc_is_dvd(scsi: &mut dyn ScsiTransport) -> bool {
// RT=0: the 8-byte feature header carries the Current Profile in bytes 6-7.
let cdb = [
SCSI_GET_CONFIGURATION,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x08,
0x00,
];
let mut buf = [0u8; 8];
match scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000) {
Ok(r) if r.bytes_transferred >= 8 => {
let profile = ((buf[6] as u16) << 8) | buf[7] as u16;
(0x0010..=0x001F).contains(&profile)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use libfreemkv::scsi::ScsiResult;
fn fake_id() -> libfreemkv::DriveId {
let mut inquiry = vec![0u8; 96];
inquiry[8..16].copy_from_slice(b"FAKEVNDR");
libfreemkv::DriveId::from_inquiry(&inquiry, "")
}
/// CssUnlocker matches only `DiscKind::Css` and carries the stable name.
#[test]
fn matches_only_css_kind() {
let id = fake_id();
let u = CssUnlocker::new();
assert_eq!(u.name(), "css");
assert!(u.matches(&UnlockCtx::new(&id, DiscKind::Css)));
for k in [DiscKind::Unknown, DiscKind::Unencrypted, DiscKind::Aacs] {
assert!(!u.matches(&UnlockCtx::new(&id, k)), "must not match {k:?}");
}
}
/// Self-guard: a drive reporting a Blu-ray profile yields NotApplicable and
/// no CSS CDB is issued.
#[test]
fn self_guards_against_non_dvd() {
struct BdTransport {
non_config_cdbs: usize,
}
impl ScsiTransport for BdTransport {
fn execute(
&mut self,
cdb: &[u8],
_dir: DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> libfreemkv::error::Result<ScsiResult> {
if cdb[0] == SCSI_GET_CONFIGURATION {
if data.len() >= 8 {
data[6] = 0x00;
data[7] = 0x40; // BD-ROM current profile
}
return Ok(ScsiResult {
status: 0,
bytes_transferred: 8,
sense: [0u8; 32],
});
}
self.non_config_cdbs += 1;
Ok(ScsiResult {
status: 0,
bytes_transferred: 0,
sense: [0u8; 32],
})
}
}
let id = fake_id();
let mut t = BdTransport { non_config_cdbs: 0 };
let r = CssUnlocker::new().unlock(&mut t, &UnlockCtx::new(&id, DiscKind::Css));
assert_eq!(r.unwrap_err(), UnlockError::NotApplicable);
assert_eq!(t.non_config_cdbs, 0, "no CSS CDB at a non-DVD drive");
}
}