mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
Compare commits
8
Commits
v1.6.11
...
v1.6.13-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56688764ea | ||
|
|
e7107040bf | ||
|
|
b88feff027 | ||
|
|
1016a294db | ||
|
|
958b15f806 | ||
|
|
508b533f78 | ||
|
|
052ab804e2 | ||
|
|
0964097e78 |
@@ -51,7 +51,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
shared-key: freemkv-unlock-coverage
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- run: cargo llvm-cov --workspace --ignore-run-fail --lcov --output-path lcov.info
|
||||
- uses: codecov/codecov-action@v5
|
||||
- uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: lcov.info
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-unlock
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.90.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-unlock
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.13] — 2026-08-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.13 for the unified release.
|
||||
|
||||
## [1.6.12] — 2026-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.12 for the unified release; no functional changes.
|
||||
|
||||
## [1.6.11] — 2026-08-26
|
||||
|
||||
### Changed
|
||||
|
||||
+5
-2
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "freemkv-unlock"
|
||||
version = "1.6.11"
|
||||
version = "1.6.13"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
rust-version = "1.88"
|
||||
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"
|
||||
@@ -26,3 +26,6 @@ sha1 = "0.10"
|
||||
sha2 = "0.11"
|
||||
aes = "0.9"
|
||||
rand = "0.10"
|
||||
|
||||
[lints.clippy]
|
||||
uninlined_format_args = "allow"
|
||||
|
||||
+1268
File diff suppressed because it is too large
Load Diff
+16
-4
@@ -25,6 +25,11 @@ pub mod ld;
|
||||
// bus as `Err(UnlockError::Transport)` rather than "not a Renesas drive"); the unlocker impl
|
||||
// (`Renesis`) is `pub(crate)` — reached only through [`all_unlockers`].
|
||||
pub mod renesis;
|
||||
// `freemkv` carries no public catalog (the firmware self-identifies rather
|
||||
// than matching a bundled profile) and no emulation wire format yet, so it
|
||||
// stays fully private — the unlocker impl (`FreemkvUnlocker`) is reached only
|
||||
// through [`all_unlockers`].
|
||||
mod freemkv;
|
||||
|
||||
use scsi::ScsiTransport;
|
||||
|
||||
@@ -178,11 +183,18 @@ pub fn unlocker_name(drive_id: &DriveId) -> Option<&'static str> {
|
||||
ld::firmware_name(drive_id)
|
||||
}
|
||||
|
||||
/// Every unlocker, in dispatch order (firmware → cert → css). This is the ONLY
|
||||
/// place an unlocker is named. Remove one = delete its line here + its module
|
||||
/// dir; the consumer never changes.
|
||||
/// Every unlocker, in dispatch order (freemkv → firmware → cert → css). This
|
||||
/// is the ONLY place an unlocker is named. Remove one = delete its line here +
|
||||
/// its module dir; the consumer never changes.
|
||||
///
|
||||
/// `FreemkvUnlocker` runs FIRST: its detection is a single, unambiguous
|
||||
/// self-identification probe (the drive's own firmware answers `"freemkv"`)
|
||||
/// rather than a profile-catalog match, so it is the cheapest and most
|
||||
/// certain check to run before falling back to the MT1959 profile lookup or
|
||||
/// the Renesas/cert/css routes.
|
||||
pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
|
||||
vec![
|
||||
Box::new(freemkv::FreemkvUnlocker::new()),
|
||||
Box::new(ld::Mt1959Unlocker::new()),
|
||||
Box::new(renesis::Renesis::new()),
|
||||
Box::new(aacs::AacsCert::new()),
|
||||
@@ -202,7 +214,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_unlockers_dispatch_order_is_firmware_then_cert_then_css() {
|
||||
let names: Vec<&'static str> = all_unlockers().iter().map(|u| u.name()).collect();
|
||||
assert_eq!(names, vec!["MT1959", "Renesas", "AACS", "DVD"]);
|
||||
assert_eq!(names, vec!["freemkv", "MT1959", "Renesas", "AACS", "DVD"]);
|
||||
}
|
||||
|
||||
/// Every unlocker declines by default rather than claiming a capability it
|
||||
|
||||
+135
-6
@@ -69,6 +69,72 @@ impl Renesis {
|
||||
pub(crate) fn new() -> Self {
|
||||
Renesis
|
||||
}
|
||||
|
||||
/// The Pioneer/Renesas vendor "open" — MakeMKV's live sequence, replicated in
|
||||
/// full: a primary read, and on its failure a payload-less "knock" + a second
|
||||
/// read at a different window.
|
||||
///
|
||||
/// A. `READ_BUFFER 0x02/0xB0 @0x000004` (164 B) — the primary open.
|
||||
/// B. (only if A refuses) `WRITE_BUFFER 0x02/0x41 @0xA5AAAA` (0-byte knock)
|
||||
/// then `READ_BUFFER 0x02/0xB0 @0x500000` (164 B).
|
||||
///
|
||||
/// Success is by SCSI *status* alone (the 164-byte payload CONTENT is not
|
||||
/// validated — verified empirically: a real drive's table and an all-zero
|
||||
/// buffer are accepted identically as long as status is GOOD). We run the
|
||||
/// whole A→knock→B path rather than bailing after A, because the knock is a
|
||||
/// per-firmware fallback: some firmware revisions only answer the `0x500000`
|
||||
/// window *after* the knock (MakeMKV issues the knock exactly when A returns
|
||||
/// CHECK CONDITION). If NEITHER A nor B returns GOOD, the drive did not open →
|
||||
/// defer to the next unlocker.
|
||||
///
|
||||
/// (There may be more than one knock offset across firmware families; only
|
||||
/// the observed `0xA5AAAA` is issued here — add variants as they are proven.)
|
||||
///
|
||||
/// `Ok(true)` = opened (A or B GOOD). `Ok(false)` = neither opened (CHECK
|
||||
/// CONDITION). `Err(Transport)` = dead bus.
|
||||
fn vendor_open(scsi: &mut dyn ScsiTransport) -> std::result::Result<bool, UnlockError> {
|
||||
const RB_B0_04_CDB: [u8; 10] = [0x3C, 0x02, 0xB0, 0x00, 0x00, 0x04, 0x00, 0x00, 0xA4, 0x00];
|
||||
const KNOCK_A5AAAA_CDB: [u8; 10] =
|
||||
[0x3B, 0x02, 0x41, 0xA5, 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00];
|
||||
const RB_B0_500000_CDB: [u8; 10] =
|
||||
[0x3C, 0x02, 0xB0, 0x50, 0x00, 0x00, 0x00, 0x00, 0xA4, 0x00];
|
||||
|
||||
// A: primary open read.
|
||||
if read_is_good(scsi, &RB_B0_04_CDB)? {
|
||||
return Ok(true);
|
||||
}
|
||||
// B: MakeMKV's fallback — knock, then the second-window read. The knock is
|
||||
// fire-and-forget (payload-less); its own status is not the signal.
|
||||
match scsi.execute(&KNOCK_A5AAAA_CDB, DataDirection::None, &mut [], 5_000) {
|
||||
Ok(_) => {}
|
||||
Err(e)
|
||||
if e.status == crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE && e.sense.is_none() =>
|
||||
{
|
||||
return Err(UnlockError::Transport);
|
||||
}
|
||||
Err(_) => {} // a drive that refuses the knock still gets the B read tried
|
||||
}
|
||||
read_is_good(scsi, &RB_B0_500000_CDB)
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue a 164-byte vendor READ_BUFFER; `Ok(true)` on GOOD status, `Ok(false)`
|
||||
/// on CHECK CONDITION (drive refused), `Err(Transport)` on a senseless
|
||||
/// transport failure (dead bus).
|
||||
fn read_is_good(
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
cdb: &[u8; 10],
|
||||
) -> std::result::Result<bool, UnlockError> {
|
||||
let mut buf = [0u8; 164];
|
||||
match scsi.execute(cdb, DataDirection::FromDevice, &mut buf, 5_000) {
|
||||
Ok(r) => Ok(r.status == 0),
|
||||
Err(e) => {
|
||||
if e.status == crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE && e.sense.is_none() {
|
||||
return Err(UnlockError::Transport);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Unlocker for Renesis {
|
||||
@@ -76,10 +142,17 @@ impl Unlocker for Renesis {
|
||||
"Renesas"
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
/// Recognize and open a Renesas/Pioneer drive the way MakeMKV does:
|
||||
/// 1. `READ_BUFFER 0xF1` identity gate (the `SAT` marker) — else this is
|
||||
/// not our drive → `NotApplicable`.
|
||||
/// 2. the single vendor open read `READ_BUFFER 0xB0 @0x04` — GOOD status
|
||||
/// confirms the drive is rip-ready. A CHECK CONDITION here is exactly
|
||||
/// where MakeMKV gives up on the drive, so we defer to the next unlocker
|
||||
/// (`NotApplicable`) rather than claim a drive we could not open.
|
||||
///
|
||||
/// On success returns `drive_unlocked: false`: the vendor open makes the
|
||||
/// drive readable, but AACS bus decryption is still the host cert's job (the
|
||||
/// cert unlocker runs next), so we do not assert the bus is handled here.
|
||||
fn unlock_features(
|
||||
&self,
|
||||
scsi: &mut dyn ScsiTransport,
|
||||
@@ -88,10 +161,18 @@ impl Unlocker for Renesis {
|
||||
if !is_renesas(scsi)? {
|
||||
return Err(UnlockError::NotApplicable);
|
||||
}
|
||||
if !Self::vendor_open(scsi)? {
|
||||
tracing::debug!(
|
||||
target: "freemkv::disc",
|
||||
phase = "renesas_open_rejected",
|
||||
"Renesas drive recognized but RB 0xB0@0x04 refused; deferring to next unlocker"
|
||||
);
|
||||
return Err(UnlockError::NotApplicable);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: "freemkv::disc",
|
||||
phase = "renesas_recognized",
|
||||
"Renesas drive recognized; bus handled by cert"
|
||||
phase = "renesas_opened",
|
||||
"Renesas drive opened (RB 0xB0@0x04 GOOD); bus handled by cert"
|
||||
);
|
||||
Ok(Unlocked {
|
||||
vid: None,
|
||||
@@ -251,6 +332,54 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A genuine Renesas drive (passes the `0xF1` SAT gate) that REFUSES the
|
||||
/// vendor open read `RB 0xB0@0x04` with CHECK CONDITION — the exact point
|
||||
/// where MakeMKV abandons the drive. `unlock_features` must defer to the
|
||||
/// next unlocker (`NotApplicable`), not claim a drive it could not open.
|
||||
/// MUTATION: dropping the `vendor_open` gate makes this return `Ok` and
|
||||
/// wrongly claim the drive.
|
||||
#[test]
|
||||
fn open_rejection_defers_to_next_unlocker() {
|
||||
struct GateOkOpenRefused;
|
||||
impl ScsiTransport for GateOkOpenRefused {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
_dir: DataDirection,
|
||||
data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> Result<ScsiResult> {
|
||||
if cdb.get(2) == Some(&0xF1) {
|
||||
// Serve the SAT identity so is_renesas() matches.
|
||||
let p = renesas_payload();
|
||||
let n = p.len().min(data.len());
|
||||
data[..n].copy_from_slice(&p[..n]);
|
||||
Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: n,
|
||||
sense: [0u8; 32],
|
||||
})
|
||||
} else {
|
||||
// RB 0xB0@0x04 (the open read) → CHECK CONDITION.
|
||||
let mut sense = [0u8; 32];
|
||||
sense[2] = 0x05; // ILLEGAL REQUEST
|
||||
sense[12] = 0x20; // invalid command operation code
|
||||
Err(ScsiError {
|
||||
status: crate::scsi::SCSI_STATUS_CHECK_CONDITION,
|
||||
sense: Some(sense),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut t = GateOkOpenRefused;
|
||||
let id = crate::DriveId::default();
|
||||
let ctx = UnlockCtx::new(&id, DiscKind::Unknown, &[]);
|
||||
assert_eq!(
|
||||
Renesis::new().unlock_features(&mut t, &ctx).unwrap_err(),
|
||||
UnlockError::NotApplicable
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_provide_bus_removal() {
|
||||
// Renesas leaves bus encryption to the cert: unlock_bus is the default.
|
||||
|
||||
Reference in New Issue
Block a user