Compare commits

...
18 Commits
Author SHA1 Message Date
Matthew Jackson 28a6e50ede 1.6.0: version sync + fix stale docs
Bump to 1.6.0 (workspace sync, no functional change). Fix the README
example that called the removed matches()/unlock() API — rewrite to the
real unlock_features()/unlock_bus() contract (+ ld/README).
2026-07-28 15:35:19 -07:00
MattJackson 3c5cfd02d9 changelog: date the released 1.5.x sections (were stale UNRELEASED) 2026-07-28 13:55:23 -07:00
Matthew Jackson 93588379c6 Rename the DVD read-unlocker: CSS -> DVD
The unlocker performs the DVD bus-auth that clears the drive's
scrambled-read barrier, letting a protected DVD be read at all. That is
a property of the DVD medium, not of the CSS scheme — the descramble key
is recovered keylessly downstream — so naming it "CSS" conflated the
read-enable with the encryption. Rename the struct to DvdUnlocker, its
registry name to "DVD", and the tests to match. Bump 1.5.2.
2026-07-22 13:48:56 -07:00
Matthew Jackson cc4c9e496d v1.5.1: bump version (unified release) 2026-07-20 17:02:02 -07:00
Matthew Jackson 4a6f0dee19 v1.5.0: bump version (unified release) 2026-07-19 15:37:17 -07:00
Matthew Jackson 93988bebba v1.4.5: bump version (unified release) 2026-07-18 16:07:53 -07:00
Matthew Jackson c5cf5e6a0c v1.4.4: bump version (unified release) 2026-07-16 21:43:57 -07:00
Matthew Jackson 71380a0b26 v1.4.3: bump version (unified release) 2026-07-16 21:05:53 -07:00
Matthew Jackson f1cedd63d9 v1.4.2: bump version (unified release) 2026-07-15 19:37:43 -07:00
Matthew Jackson fb7d74039f v1.4.1: bump version (unified release) 2026-07-14 14:45:21 -07:00
Matthew Jackson aa82e9cf6b Changelog for 1.4.1 2026-07-14 14:43:14 -07:00
Matthew Jackson d32d43c2e2 v1.4.0: bump version (unified release) 2026-07-13 19:28:44 -07:00
Matthew Jackson eb35571632 Changelog: 1.4.0 (Blu-ray 3D / MVC) 2026-07-13 12:47:03 -07:00
Matthew Jackson 075352158c ld: restore module README with LibreDrive credit to Mike Chen + the MakeMKV team 2026-07-10 14:35:44 -07:00
Matthew Jackson 9fbbe15b29 1.3.2: version sync
Inherits libfreemkv 1.3.2.
2026-07-10 14:00:35 -07:00
Matthew Jackson b7f34c46bc 1.3.1: relicense to MIT
Relicensed from AGPL-3.0 to MIT, effective 1.3.1 (<=1.3.0 remain AGPL).
Version sync to 1.3.1.
2026-07-10 12:31:19 -07:00
Matthew Jackson 4120d38151 docs: add 1.3.0 changelog 2026-07-10 08:19:22 -07:00
Matthew Jackson ca8119a167 renesis: detection-only stub; drop unverified vendor command 2026-07-09 16:36:47 -07:00
8 changed files with 159 additions and 57 deletions
+46
View File
@@ -0,0 +1,46 @@
# Changelog
## [1.6.0] — UNRELEASED
Version sync with the workspace. No functional change in this crate.
## [1.5.2] — 2026-07-22
### Changed
- The DVD read-unlocker (bus-auth) is renamed `CSS``DVD`: it reports the
medium it unlocks, not whether a title-key crack ran. The unlocker report now
reads `DVD: yes` on any DVD.
## [1.4.1] — 2026-07-14
Version sync with the workspace; inherits libfreemkv 1.4.1.
## [1.4.0] — 2026-07-13
Version sync with the workspace; inherits libfreemkv 1.4.0.
## [1.3.2] — 2026-07-10
Version sync with the workspace; inherits libfreemkv 1.3.2.
## [1.3.1] — 2026-07-10
### Licensing
- **Relicensed to the MIT License, from 1.3.1 onwards** (releases up to and
including 1.3.0 remain under AGPL-3.0).
Version sync with the workspace; inherits libfreemkv 1.3.1.
## [1.3.0] — 2026-07-08
### Changed
- **`Unlocker` is now two capability methods.** `unlock_features` (drive
riplock / speed / OEM VID at drive-prep) and `unlock_bus` (bus-encryption
removal for the mounted disc) replace the single `matches()` + `unlock()`
contract. Each defaults to `NotApplicable`, so an unlocker implements only the
capabilities it actually provides.
- **`DriveId` gains a `product_id` field** carrying the SCSI INQUIRY product
string, so consumers can match on it.
+2 -2
View File
@@ -1,9 +1,9 @@
[package]
name = "freemkv-unlock"
version = "1.3.0"
version = "1.6.0"
edition = "2024"
rust-version = "1.86"
license = "AGPL-3.0-only"
license = "MIT"
description = "Unlock layer for the freemkv toolchain: the Unlocker contract + self-contained firmware/AACS/CSS unlocker modules. libfreemkv depends on this and dispatches via all_unlockers()."
repository = "https://github.com/freemkv/freemkv-unlock"
# NEVER crates.io: this crate carries drive firmware. Consumed only by git tag.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Matthew Jackson & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11 -3
View File
@@ -14,9 +14,17 @@ clients of libfreemkv are oblivious to unlockers entirely (as they are to the
SCSI layer).
```rust
use freemkv_unlock::UnlockError;
// Drive-prep: try each unlocker's feature unlock until one claims the drive.
// `NotApplicable` means "not this unlocker's drive" — move on; a transport
// error means a dead bus — abort. `unlock_bus` follows the same contract for
// removing per-disc bus encryption.
for u in freemkv_unlock::all_unlockers() {
if u.matches(&ctx) {
return u.unlock(&mut scsi, &ctx);
match u.unlock_features(&mut scsi, &ctx) {
Ok(unlocked) => return Ok(unlocked),
Err(UnlockError::NotApplicable) => continue,
Err(e) => return Err(e),
}
}
```
@@ -24,4 +32,4 @@ for u in freemkv_unlock::all_unlockers() {
To remove an unlocker, delete its module directory and its one line in
`all_unlockers()` — nothing else changes.
License: AGPL-3.0-only.
License: MIT.
+40 -19
View File
@@ -141,27 +141,38 @@ pub fn unlock_css_reads(scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
r
}
/// The CSS unlocker — the DVD peer of the firmware and AACS-cert unlockers in
/// the uniform [`crate::Unlocker`] registry. It removes the CSS
/// scrambled-read barrier (drive ASF=1) and learns no VID or bus key — the
/// descramble key is recovered keylessly downstream (the Stevenson attack).
pub struct CssUnlocker;
/// The DVD unlocker (registry name `"DVD"`) — the DVD peer of the firmware and
/// AACS-cert unlockers in the uniform [`crate::Unlocker`] registry. It removes
/// the DVD scrambled-read barrier (drive ASF=1) via bus-auth and learns no VID
/// or bus key — the descramble key is recovered keylessly downstream (the
/// Stevenson attack). Named for the medium it unlocks (DVD), not the CSS
/// scheme: the bus-auth is required to read a CSS-protected DVD at all, whether
/// or not any given sector turns out to be scrambled. Lives in the `css` module
/// beside the CSS-scheme primitives it drives.
pub struct DvdUnlocker;
impl CssUnlocker {
impl DvdUnlocker {
pub fn new() -> Self {
CssUnlocker
DvdUnlocker
}
}
impl Default for CssUnlocker {
impl Default for DvdUnlocker {
fn default() -> Self {
Self::new()
}
}
impl crate::Unlocker for CssUnlocker {
impl crate::Unlocker for DvdUnlocker {
fn name(&self) -> &'static str {
"CSS"
// User-facing unlocker label. This unlocker's job is the DVD
// read-enablement bus-auth (it clears the drive's scrambled-read
// barrier and learns no key) — a property of the DVD medium, NOT of
// whether the content happens to be CSS-scrambled. Reporting it as
// "DVD" is honest: on any DVD the bus-auth ran; the CSS descramble
// itself is keyless and handled downstream, so it is not a separate
// "did an unlocker run" signal.
"DVD"
}
/// CSS removes the scrambled-sector barrier (a bus-level concern); it
@@ -179,8 +190,8 @@ impl crate::Unlocker for CssUnlocker {
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)"
phase = "dvd_unlocker_not_dvd",
"DvdUnlocker invoked on a non-DVD profile; refusing (NotApplicable)"
);
return Err(crate::UnlockError::NotApplicable);
}
@@ -193,7 +204,7 @@ impl crate::Unlocker for CssUnlocker {
}
/// Transport-level "is the mounted disc a DVD?" probe (GET CONFIGURATION
/// current-profile, DVD family `0x0010..=0x001F`). Lets the CssUnlocker
/// current-profile, DVD family `0x0010..=0x001F`). Lets the DvdUnlocker
/// self-verify against the drive instead of trusting the caller's DiscKind.
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.
@@ -922,9 +933,19 @@ mod tests {
assert_eq!(cdb[9], 0x04, "low byte of 2052-byte transfer");
}
/// CssUnlocker provides bus removal only — it never provides drive features.
/// The unlocker's user-facing name is "DVD" (the medium it read-unlocks),
/// not "CSS" (the scheme). Apps render the unlocker report from this name,
/// so it is a stable contract — the bus-auth ran on any DVD, encrypted or
/// not, and the report must say so rather than conflate it with a CSS crack.
#[test]
fn css_unlocker_provides_no_features() {
fn dvd_unlocker_is_named_dvd() {
use crate::Unlocker;
assert_eq!(DvdUnlocker::new().name(), "DVD");
}
/// DvdUnlocker provides bus removal only — it never provides drive features.
#[test]
fn dvd_unlocker_provides_no_features() {
use crate::scsi::{DataDirection, ScsiResult};
use crate::{DiscKind, DriveId, UnlockCtx, UnlockError, Unlocker};
struct DeadTransport;
@@ -942,16 +963,16 @@ mod tests {
let id = DriveId::default();
let mut t = DeadTransport;
let r =
CssUnlocker::new().unlock_features(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
DvdUnlocker::new().unlock_features(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
assert_eq!(r.unwrap_err(), UnlockError::NotApplicable);
}
/// Defense in depth: even when the caller declares `DiscKind::Css`, the
/// CssUnlocker self-verifies against the drive's GET CONFIGURATION profile.
/// DvdUnlocker self-verifies against the drive's GET CONFIGURATION profile.
/// A drive reporting a Blu-ray profile → `NotApplicable`, and NOT a single
/// CSS CDB is issued (no bus-auth fired at a BD).
#[test]
fn css_unlocker_self_guards_against_non_dvd() {
fn dvd_unlocker_self_guards_against_non_dvd() {
use crate::scsi::{DataDirection, ScsiResult};
use crate::{DiscKind, DriveId, UnlockCtx, UnlockError, Unlocker};
@@ -994,7 +1015,7 @@ mod tests {
};
let mut t = BdTransport { non_config_cdbs: 0 };
let r = CssUnlocker::new().unlock_bus(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
let r = DvdUnlocker::new().unlock_bus(&mut t, &UnlockCtx::new(&id, DiscKind::Css, &[]));
assert_eq!(
r.unwrap_err(),
UnlockError::NotApplicable,
+27
View File
@@ -0,0 +1,27 @@
# ld — the LibreDrive unlocker
The **LibreDrive** unlocker module for freemkv-unlock.
freemkv-unlock defines the generic `Unlocker` contract; this module is one
implementation of it. It recognizes a bundled catalog of supported drives
(`profiles.json`) and, for a matching drive, lifts the drive-level
bus-encryption barrier so the drive serves readable sectors. Content-key
decryption is a separate concern, handled by the consumer.
Clients never name this module directly —
[libfreemkv](https://github.com/freemkv/libfreemkv) dispatches through
`freemkv_unlock::all_unlockers()`, and this module answers `unlock_features()` /
`unlock_bus()` when the drive identity is one it supports.
## Scope: non-persistent unlock only
This module performs only the **non-persistent** unlock — the access state it
sets up lives in drive RAM and is gone on power cycle. The one-time, permanent
drive preparation is the drive owner's own manual step and is **never**
automated here.
## Credits
LibreDrive was created by **Mike Chen** and the **MakeMKV team**. This module
builds on their work — our thanks and full credit to them for the LibreDrive
capabilities.
+2 -2
View File
@@ -115,7 +115,7 @@ pub enum UnlockError {
/// here — that is the consumer's concern, not bus removal.
pub trait Unlocker: Send + Sync {
/// Short, stable identifier for this unlocker (e.g. "LibreDrive", "AACS",
/// "CSS", "Renesas"). The ONE place a name lives — apps render the unlocker
/// "DVD", "Renesas"). The ONE place a name lives — apps render the unlocker
/// report from [`all_unlockers`], never hardcoding names, so adding/removing
/// an unlocker updates every report with no app change.
fn name(&self) -> &'static str;
@@ -167,6 +167,6 @@ pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
Box::new(ld::LibreDrive::new()),
Box::new(renesis::Renesis::new()),
Box::new(aacs::AacsCert::new()),
Box::new(css::CssUnlocker::new()),
Box::new(css::DvdUnlocker::new()),
]
}
+10 -31
View File
@@ -1,16 +1,10 @@
//! renesis — the Renesas-platform unlocker (Pioneer + HL-DT-ST Renesas drives).
//! renesis — Renesas-platform detection (Pioneer + HL-DT-ST Renesas drives).
//!
//! Optical drives split into two controller families: MediaTek (handled by
//! [`crate::ld`], per-drive firmware) and Renesas. This module owns the Renesas
//! side. Detection is a single vendor probe — a Renesas controller serves the
//! READ_BUFFER 0x02/0xF1 identity block (ASCII `SAT` interface marker at
//! `[16..19]`); a MediaTek drive rejects the command with ILLEGAL REQUEST. See
//! [`is_renesas`].
//!
//! renesis provides the drive-FEATURES capability ([`Renesis::unlock_features`])
//! and NOT bus removal (the cert handles the bus). The feature unlock is a no-op
//! for now — it recognizes the drive and reports the match, deferring the bus to
//! the cert stage.
//! [`crate::ld`]) and Renesas. This module identifies the Renesas side via a
//! single vendor identity probe (see [`is_renesas`]) and reports the match so a
//! Renesas drive is named honestly. It does not modify drive state; AACS bus
//! decryption is handled by the host cert.
use crate::scsi::{DataDirection, ScsiTransport};
use crate::{UnlockCtx, UnlockError, Unlocked, Unlocker};
@@ -22,17 +16,6 @@ const RB_F1_LEN: usize = 48;
const RENESAS_MARKER: &[u8] = b"SAT";
const RENESAS_MARKER_OFFSET: usize = 16;
/// Renesas feature-unlock command (RS8xxx+ platforms). A single fixed vendor
/// WRITE BUFFER (opcode `0x3B`, mode `0x02`, buffer id `0x41`): issuing it
/// authenticates the host and enables the drive's extended feature set.
///
/// Platform-invariant — the same command unlocks every supported Renesas
/// firmware; it does not vary per drive or per firmware revision. Sent by
/// [`Renesis::unlock_features`]. (Verify on hardware before relying on it.)
#[allow(dead_code)]
const RENESAS_CHALLENGE_CDB: [u8; 10] =
[0x3B, 0x02, 0x41, 0xA5, 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00];
/// True if `scsi` is a Renesas-platform drive (Pioneer or HL-DT-ST Renesas).
///
/// Issues the vendor READ_BUFFER 0x02/0xF1 probe: a Renesas controller serves a
@@ -68,12 +51,10 @@ impl Unlocker for Renesis {
"Renesas"
}
/// `if is_renesas() { recognized }`. Renesas is a distinct platform from
/// LibreDrive (MediaTek): it provides DRIVE FEATURES only — it does NOT remove
/// AACS bus encryption (`unlock_bus` is left at the default, so the cert stage
/// handles the bus). The feature unlock itself is not implemented yet (no-op),
/// but the match IS reported (`Ok`, `drive_unlocked: false`) so the drive is
/// recognized as Renesas. A non-Renesas drive → `NotApplicable`.
/// Report whether the drive is a Renesas platform. On a match, returns `Ok`
/// with `drive_unlocked: false` — the drive is recognized but its state is
/// not modified here (AACS bus decryption is handled by the host cert). A
/// non-Renesas drive → `NotApplicable`.
fn unlock_features(
&self,
scsi: &mut dyn ScsiTransport,
@@ -82,12 +63,10 @@ impl Unlocker for Renesis {
if !is_renesas(scsi) {
return Err(UnlockError::NotApplicable);
}
// Recognized Renesas drive (Pioneer / HL-DT-ST Renesas). Feature unlock:
// TODO. `drive_unlocked: false` → bus encryption is left for the cert.
tracing::debug!(
target: "freemkv::disc",
phase = "renesas_recognized",
"Renesas drive recognized; feature unlock TODO, bus deferred to cert"
"Renesas drive recognized; bus handled by cert"
);
Ok(Unlocked {
vid: None,