freemkv-unlock 1.7.4: Hrl=off in arm_bypass recipes + version

arm_bypass_bd/arm_bypass_uhd now also Set(Hrl, off) (revocation skip) so a
revoked/absent cert never trips the HRL check on a keyless bypass rip; tests
updated for the new SET/verify order.
This commit is contained in:
Matthew Jackson
2026-09-19 14:00:25 -07:00
parent 98ef26f78c
commit 58c667314c
5 changed files with 42 additions and 15 deletions
Generated
+5 -5
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "freemkv-unlock"
version = "1.7.1"
version = "1.7.4"
dependencies = [
"aes",
"base64",
@@ -406,16 +406,16 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[patch.unused]]
name = "freemkv-engine"
version = "1.7.1"
version = "1.7.4"
[[patch.unused]]
name = "freemkv-i18n"
version = "1.7.1"
version = "1.7.4"
[[patch.unused]]
name = "freemkv-keysources"
version = "1.7.1"
version = "1.7.4"
[[patch.unused]]
name = "libfreemkv"
version = "1.7.1"
version = "1.7.4"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "freemkv-unlock"
version = "1.7.3"
version = "1.7.4"
edition = "2024"
rust-version = "1.88"
license = "MIT"
+16 -2
View File
@@ -361,6 +361,7 @@ mod tests {
// A minimal freemkv drive: IDENTITY→magic, SET updates state, GET reads it.
struct FwDrive {
ake: u8,
hrl: u8,
cdbs: Vec<Vec<u8>>,
}
impl ScsiTransport for FwDrive {
@@ -382,12 +383,21 @@ mod tests {
} else if cdb[CDB_VERB] == Verb::Set as u8 && cdb[CDB_FEATURE] == Feature::Ake as u8
{
self.ake = cdb[CDB_STATE];
} else if cdb[CDB_VERB] == Verb::Set as u8 && cdb[CDB_FEATURE] == Feature::Hrl as u8
{
self.hrl = cdb[CDB_STATE];
} else if cdb[CDB_VERB] == Verb::Get as u8
&& cdb[CDB_FEATURE] == Feature::Ake as u8
&& !data.is_empty()
{
data[0] = self.ake;
n = 1;
} else if cdb[CDB_VERB] == Verb::Get as u8
&& cdb[CDB_FEATURE] == Feature::Hrl as u8
&& !data.is_empty()
{
data[0] = self.hrl;
n = 1;
}
Ok(crate::scsi::ScsiResult {
status: 0,
@@ -398,15 +408,19 @@ mod tests {
}
let mut t = FwDrive {
ake: STATE_PASSTHROUGH,
hrl: STATE_PASSTHROUGH,
cdbs: Vec::new(),
};
let u = AacsUnlocker::new(vec![host_cert()]).arm_before_unlock(ArmRecipe::BypassBd);
u.maybe_arm(&mut t).expect("armed");
assert_eq!(t.hrl, STATE_OFF, "the recipe skipped revocation (Hrl=off)");
assert_eq!(t.ake, STATE_OFF, "the recipe nulled the AKE (Ake=off)");
// IDENTITY, then Set(Ake,off), then a verifying Get(Ake).
// IDENTITY, then Set(Hrl,off)+verify, then Set(Ake,off)+verify.
assert_eq!(t.cdbs[0][CDB_VERB], Verb::Identity as u8);
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(t.cdbs[2][CDB_VERB], Verb::Get as u8);
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(t.cdbs[4][CDB_VERB], Verb::Get as u8);
}
/// With no host certs there is nothing to authenticate with → `Ok(false)`,
+7 -2
View File
@@ -698,17 +698,22 @@ impl<'a> FirmwareControl<'a> {
/// **arm_bypass_bd** — full-bypass Blu-ray rip (NO host cert needed).
///
/// Sets: `Ake = null` (drive acts pre-authenticated, so a bare VID read
/// returns the volume ID with no cert and no AKE). Rips: BD with no cert.
/// returns the volume ID with no cert and no AKE) and `Hrl = skip`
/// (revocation off, so a revoked/absent cert never trips the HRL check).
/// Rips: BD with no cert.
pub fn arm_bypass_bd(&mut self) -> Result<()> {
self.set_verify(Feature::Hrl, STATE_OFF)?;
self.set_verify(Feature::Ake, STATE_OFF)
}
/// **arm_bypass_uhd** — full-bypass UHD rip (NO host cert needed).
///
/// Sets: `Uhd = on` (engage the UHD disc), `Ake = null` (skip the
/// handshake), `Bus = off` (content de-bussed). Rips: UHD with no cert.
/// handshake), `Bus = off` (content de-bussed), and `Hrl = skip`
/// (revocation off). Rips: UHD with no cert.
pub fn arm_bypass_uhd(&mut self) -> Result<()> {
self.set_verify(Feature::Uhd, STATE_ON)?;
self.set_verify(Feature::Hrl, STATE_OFF)?;
self.set_verify(Feature::Ake, STATE_OFF)?;
self.set_verify(Feature::Bus, STATE_OFF)
}
+13 -5
View File
@@ -489,8 +489,12 @@ fn arm_bypass_bd_nulls_ake() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm_bypass_bd().expect("armed");
}
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Ake));
// Hrl=off (revocation skip) is set+verified FIRST, then Ake=null.
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[1], build_get_cdb(Feature::Hrl));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[3], build_get_cdb(Feature::Ake));
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.ake, STATE_OFF);
}
@@ -501,10 +505,13 @@ fn arm_bypass_uhd_sets_uhd_ake_bus() {
let mut fw = FirmwareControl::new(&mut m);
fw.arm_bypass_uhd().expect("armed");
}
// Recipe order: Uhd=on, Hrl=off, Ake=null, Bus=off (each SET then verifying GET).
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Ake, STATE_OFF));
assert_eq!(m.cdbs[6], build_set_cdb(Feature::Bus, STATE_OFF));
assert_eq!(m.states.uhd, STATE_ON);
assert_eq!(m.states.hrl, STATE_OFF);
assert_eq!(m.states.ake, STATE_OFF);
assert_eq!(m.states.bus, STATE_OFF);
}
@@ -563,10 +570,11 @@ fn set_verify_reports_mismatch() {
let mut d = StuckDrive;
let mut fw = FirmwareControl::new(&mut d);
let err = fw.arm_bypass_bd().unwrap_err();
// arm_bypass_bd now verifies Hrl=off FIRST, so the stuck drive trips on Hrl.
assert_eq!(
err,
FirmwareError::VerifyFailed {
feature: Feature::Ake,
feature: Feature::Hrl,
wanted: STATE_OFF,
got: STATE_PASSTHROUGH,
}