mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
firmware: retire Ake+Bus into one Encryption lever (fw 0.9.0 ABI)
Firmware 0.9.0 consolidates the Ake (0x06) and Bus (0x07) features into a single Encryption (0x06) lever; wire id 0x07 no longer exists. The separate Bus lever was proven inert on BU40N/MT1959 — the one Encryption lever de-busses on its own — so the unlock recipe now sets one flag where it used to set two, and the trailing best-effort Set(Bus, off) is gone. Set(Encryption, STATE_OFF) is the load-bearing step: the drive acts pre-authenticated, so a bare 0xAD returns the VID with no cert and no AKE, and content comes back de-bussed. Failure still maps to VidUnavailable. Hardware-validated against fw 0.9.0 on BU40N 1.00 with a UHD disc: every sampled unit returns de-bussed and opens with the AACS unit key resolved from the live keyserver.
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.7.5] — UNRELEASED
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- firmware: mirror the fw 0.9.0 ABI — `Ake` (`0x06`) and `Bus` (`0x07`) are retired into a single `Encryption` (`0x06`) lever, so the unlock recipe now sets one flag instead of two and wire id `0x07` is gone. Required to talk to firmware 0.9.0.
|
||||||
|
|
||||||
## [1.7.4] — 2026-09-21
|
## [1.7.4] — 2026-09-21
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
|
|
||||||
- CI consolidated to the central reusable workflows; no functional changes.
|
- CI consolidated to the central reusable workflows; no functional changes.
|
||||||
|
|||||||
+13
-9
@@ -360,7 +360,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// A minimal freemkv drive: IDENTITY→magic, SET updates state, GET reads it.
|
// A minimal freemkv drive: IDENTITY→magic, SET updates state, GET reads it.
|
||||||
struct FwDrive {
|
struct FwDrive {
|
||||||
ake: u8,
|
encryption: u8,
|
||||||
hrl: u8,
|
hrl: u8,
|
||||||
cdbs: Vec<Vec<u8>>,
|
cdbs: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
@@ -380,17 +380,18 @@ mod tests {
|
|||||||
resp.extend(std::iter::repeat_n(STATE_PASSTHROUGH, ALL_FEATURES.len()));
|
resp.extend(std::iter::repeat_n(STATE_PASSTHROUGH, ALL_FEATURES.len()));
|
||||||
n = resp.len().min(data.len());
|
n = resp.len().min(data.len());
|
||||||
data[..n].copy_from_slice(&resp[..n]);
|
data[..n].copy_from_slice(&resp[..n]);
|
||||||
} else if cdb[CDB_VERB] == Verb::Set as u8 && cdb[CDB_FEATURE] == Feature::Ake as u8
|
} else if cdb[CDB_VERB] == Verb::Set as u8
|
||||||
|
&& cdb[CDB_FEATURE] == Feature::Encryption as u8
|
||||||
{
|
{
|
||||||
self.ake = cdb[CDB_STATE];
|
self.encryption = cdb[CDB_STATE];
|
||||||
} else if cdb[CDB_VERB] == Verb::Set as u8 && cdb[CDB_FEATURE] == Feature::Hrl as u8
|
} else if cdb[CDB_VERB] == Verb::Set as u8 && cdb[CDB_FEATURE] == Feature::Hrl as u8
|
||||||
{
|
{
|
||||||
self.hrl = cdb[CDB_STATE];
|
self.hrl = cdb[CDB_STATE];
|
||||||
} else if cdb[CDB_VERB] == Verb::Get as u8
|
} else if cdb[CDB_VERB] == Verb::Get as u8
|
||||||
&& cdb[CDB_FEATURE] == Feature::Ake as u8
|
&& cdb[CDB_FEATURE] == Feature::Encryption as u8
|
||||||
&& !data.is_empty()
|
&& !data.is_empty()
|
||||||
{
|
{
|
||||||
data[0] = self.ake;
|
data[0] = self.encryption;
|
||||||
n = 1;
|
n = 1;
|
||||||
} else if cdb[CDB_VERB] == Verb::Get as u8
|
} else if cdb[CDB_VERB] == Verb::Get as u8
|
||||||
&& cdb[CDB_FEATURE] == Feature::Hrl as u8
|
&& cdb[CDB_FEATURE] == Feature::Hrl as u8
|
||||||
@@ -407,19 +408,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut t = FwDrive {
|
let mut t = FwDrive {
|
||||||
ake: STATE_PASSTHROUGH,
|
encryption: STATE_PASSTHROUGH,
|
||||||
hrl: STATE_PASSTHROUGH,
|
hrl: STATE_PASSTHROUGH,
|
||||||
cdbs: Vec::new(),
|
cdbs: Vec::new(),
|
||||||
};
|
};
|
||||||
let u = AacsUnlocker::new(vec![host_cert()]).arm_before_unlock(ArmRecipe::BypassBd);
|
let u = AacsUnlocker::new(vec![host_cert()]).arm_before_unlock(ArmRecipe::BypassBd);
|
||||||
u.maybe_arm(&mut t).expect("armed");
|
u.maybe_arm(&mut t).expect("armed");
|
||||||
assert_eq!(t.hrl, STATE_OFF, "the recipe skipped revocation (Hrl=off)");
|
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)");
|
assert_eq!(
|
||||||
// IDENTITY, then Set(Hrl,off)+verify, then Set(Ake,off)+verify.
|
t.encryption, STATE_OFF,
|
||||||
|
"the recipe disabled encryption (Encryption=off)"
|
||||||
|
);
|
||||||
|
// IDENTITY, then Set(Hrl,off)+verify, then Set(Encryption,off)+verify.
|
||||||
assert_eq!(t.cdbs[0][CDB_VERB], Verb::Identity as u8);
|
assert_eq!(t.cdbs[0][CDB_VERB], Verb::Identity as u8);
|
||||||
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Hrl, 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[2][CDB_VERB], Verb::Get as u8);
|
||||||
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
|
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
assert_eq!(t.cdbs[4][CDB_VERB], Verb::Get as u8);
|
assert_eq!(t.cdbs[4][CDB_VERB], Verb::Get as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+128
-47
@@ -44,8 +44,13 @@ use crate::scsi::{DataDirection, ScsiTransport};
|
|||||||
pub const READ_BUFFER_OPCODE: u8 = 0x3C;
|
pub const READ_BUFFER_OPCODE: u8 = 0x3C;
|
||||||
/// The freemkv sub-command mode at `cdb[1]`; OEM rejects modes `>= 0x0E`.
|
/// The freemkv sub-command mode at `cdb[1]`; OEM rejects modes `>= 0x0E`.
|
||||||
pub const KNOCK_MODE: u8 = 0x0E;
|
pub const KNOCK_MODE: u8 = 0x0E;
|
||||||
/// Two-byte knock at `cdb[2..4]` ("C0DE").
|
/// Two-byte knock at `cdb[2..4]` ("C0DE") — the *safe* knock carried by every
|
||||||
|
/// durable verb (Identity/Set/Get/Reset/DumpAll/Save/FlashWrite).
|
||||||
pub const KNOCK: [u8; 2] = [0xC0, 0xDE];
|
pub const KNOCK: [u8; 2] = [0xC0, 0xDE];
|
||||||
|
/// Two-byte knock at `cdb[2..4]` — the *debug* knock, distinct from [`KNOCK`]
|
||||||
|
/// by construction. The only frame the fw honours for the debug-only verbs
|
||||||
|
/// ([`Verb::Call`], [`Verb::Poke`], [`Verb::Reboot`]).
|
||||||
|
pub const DEBUG_KNOCK: [u8; 2] = [0xDE, 0xB9];
|
||||||
/// Response-framing magic leading the [`Verb::Identity`] reply (`b"freemkv"`).
|
/// Response-framing magic leading the [`Verb::Identity`] reply (`b"freemkv"`).
|
||||||
pub const RESP_MAGIC: &[u8] = b"freemkv";
|
pub const RESP_MAGIC: &[u8] = b"freemkv";
|
||||||
|
|
||||||
@@ -153,12 +158,41 @@ pub enum Verb {
|
|||||||
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at the 32-bit address in
|
/// Diagnostic RAM peek: [`MEMREAD_LEN`] bytes at the 32-bit address in
|
||||||
/// `cdb[5..9]` (big-endian).
|
/// `cdb[5..9]` (big-endian).
|
||||||
DumpAll = 0x09,
|
DumpAll = 0x09,
|
||||||
|
/// **TEMPORARY** diagnostic probe: program a single byte to an allowlisted
|
||||||
|
/// flash offset via the OEM PROGRAM routine. `CDB[5..9]` = 32-bit flash offset
|
||||||
|
/// (big-endian); `CDB[9]` = value byte. The firmware refuses any offset outside
|
||||||
|
/// the compile-time allowlist window (`0x1EA000..0x1EB000` — the corpus-proven-
|
||||||
|
/// unlocked NV/SAVE block) with a distinct `"REFU"` status word (PROGRAM not
|
||||||
|
/// called). Reply: 4-byte offset echo (BE) + 4-byte PROGRAM/REFU status (BE).
|
||||||
|
/// Not part of the durable feature grammar; subsumed by [`Verb::Save`].
|
||||||
|
FlashWrite = 0x0A,
|
||||||
/// Persist the whole RAM feature-state table to the flash config block. This is
|
/// Persist the whole RAM feature-state table to the flash config block. This is
|
||||||
/// the ONLY verb that writes the config to flash: [`Verb::Set`] and
|
/// the ONLY verb that writes the config to flash: [`Verb::Set`] and
|
||||||
/// [`Verb::Reset`] touch RAM only, so a host's changes stay volatile until a
|
/// [`Verb::Reset`] touch RAM only, so a host's changes stay volatile until a
|
||||||
/// `Save` commits them (and survive a power cycle only once saved). Ignores
|
/// `Save` commits them (and survive a power cycle only once saved). Ignores
|
||||||
/// feature/state.
|
/// feature/state.
|
||||||
Save = 0x0B,
|
Save = 0x0B,
|
||||||
|
/// **Debug-knock only.** Interactive fw-exploration `blx target` primitive.
|
||||||
|
/// `CDB[5..9]` = 32-bit target VA (big-endian); `CDB[9]` = single u8 that
|
||||||
|
/// is loaded into r0 as the sole register argument. Refused under the safe
|
||||||
|
/// [`KNOCK`]; only executed under [`DEBUG_KNOCK`]. See [`build_call_cdb`].
|
||||||
|
Call = 0x0C,
|
||||||
|
/// **Debug-knock only.** Poke a single byte to an arbitrary address.
|
||||||
|
/// `CDB[5..9]` = 32-bit target address (big-endian, RAM or MMIO);
|
||||||
|
/// `CDB[9]` = byte value to store. Refused under the safe [`KNOCK`]; only
|
||||||
|
/// executed under [`DEBUG_KNOCK`]. See [`build_poke_cdb`].
|
||||||
|
Poke = 0x0D,
|
||||||
|
/// **Debug-knock only.** Invokes the firmware's boot function entry with
|
||||||
|
/// r0=4 to force the cold path (BSS clear + C-runtime data init + full
|
||||||
|
/// post-init sequence). Recovers a wedged drive without a power cycle
|
||||||
|
/// (SCSI target briefly returns Aborted Command mid-reboot, then comes
|
||||||
|
/// back ~5s later; the safe-knock verb chain is re-armed to its power-on
|
||||||
|
/// defaults). The target VA is baked into the emitted handler at build
|
||||||
|
/// time (resolved from the boot-init signature = `boot_init_site - 0x10`),
|
||||||
|
/// so no CDB arguments are carried beyond the verb byte. Refused under
|
||||||
|
/// the safe [`KNOCK`]; only executed under [`DEBUG_KNOCK`]. See
|
||||||
|
/// [`build_reboot_cdb`].
|
||||||
|
Reboot = 0x0F,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The feature selector in `cdb[5]` for [`Verb::Set`] / [`Verb::Get`]. These
|
/// The feature selector in `cdb[5]` for [`Verb::Set`] / [`Verb::Get`]. These
|
||||||
@@ -195,27 +229,25 @@ pub enum Feature {
|
|||||||
/// revoked certs accepted, non-destructive, the unlock direction); [`STATE_ON`]
|
/// revoked certs accepted, non-destructive, the unlock direction); [`STATE_ON`]
|
||||||
/// (`0x01`) = on (enforce the HRL).
|
/// (`0x01`) = on (enforce the HRL).
|
||||||
Hrl = 0x05,
|
Hrl = 0x05,
|
||||||
/// Drive-host AKE. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM real handshake;
|
/// The one master encryption/cert bypass. [`STATE_OFF`] (`0x00`) = every
|
||||||
/// [`STATE_OFF`] (`0x00`) = off (null/bypass — the drive acts pre-authenticated,
|
/// drive-side encryption/cert requirement gone (cert-AKE relaxed, bus-wrap
|
||||||
/// the unlock direction); [`STATE_ON`] (`0x01`) = on (require the real handshake).
|
/// off — what used to need a cert now doesn't). [`STATE_PASSTHROUGH`] (`0xFF`)
|
||||||
Ake = 0x06,
|
/// = OEM behavior (real handshake, bus-encrypted content). [`STATE_ON`]
|
||||||
/// In-transit AACS bus encryption. [`STATE_PASSTHROUGH`] (`0xFF`) = OEM (bus
|
/// (`0x01`) = on (require the real handshake). Wire id `0x07` is retired
|
||||||
/// encryption on); [`STATE_OFF`] (`0x00`) = off (de-bussed — content returned
|
/// (was `Bus`, empirically proved inert as a separate datapath lever on
|
||||||
/// with no bus encryption, the unlock direction); [`STATE_ON`] (`0x01`) = on
|
/// BU40N/MT1959 — the single Encryption lever de-busses on its own).
|
||||||
/// (bus encryption).
|
Encryption = 0x06,
|
||||||
Bus = 0x07,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The set of all features, in the canonical order the IDENTITY feature-state
|
/// The set of all features, in the canonical order the IDENTITY feature-state
|
||||||
/// table serialises them (and the order [`FirmwareControl::states`] probes).
|
/// table serialises them (and the order [`FirmwareControl::states`] probes).
|
||||||
pub const ALL_FEATURES: [Feature; 7] = [
|
pub const ALL_FEATURES: [Feature; 6] = [
|
||||||
Feature::Speed,
|
Feature::Speed,
|
||||||
Feature::Region,
|
Feature::Region,
|
||||||
Feature::Uhd,
|
Feature::Uhd,
|
||||||
Feature::Bd,
|
Feature::Bd,
|
||||||
Feature::Hrl,
|
Feature::Hrl,
|
||||||
Feature::Ake,
|
Feature::Encryption,
|
||||||
Feature::Bus,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// A Blu-ray region for [`FirmwareControl::force_region_bd`].
|
/// A Blu-ray region for [`FirmwareControl::force_region_bd`].
|
||||||
@@ -316,6 +348,63 @@ pub fn build_memread_cdb(addr: u32) -> [u8; CDB_LEN] {
|
|||||||
cdb
|
cdb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a 10-byte CDB for [`Verb::Call`]: `blx target(r0)` where `target` is
|
||||||
|
/// packed big-endian in `cdb[5..9]` and the u8 `r0` register argument rides in
|
||||||
|
/// `cdb[9]`. Carries the [`DEBUG_KNOCK`] at `cdb[2..4]` — the ONLY frame the
|
||||||
|
/// fw honours for Call. The handler ORs the thumb bit into the address at
|
||||||
|
/// runtime. r1..r3 are undefined at callee entry.
|
||||||
|
pub fn build_call_cdb(target: u32, r0: u8) -> [u8; CDB_LEN] {
|
||||||
|
let mut cdb = [0u8; CDB_LEN];
|
||||||
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
||||||
|
cdb[CDB_MODE] = KNOCK_MODE;
|
||||||
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
||||||
|
cdb[CDB_VERB] = Verb::Call as u8;
|
||||||
|
cdb[5] = (target >> 24) as u8;
|
||||||
|
cdb[6] = (target >> 16) as u8;
|
||||||
|
cdb[7] = (target >> 8) as u8;
|
||||||
|
cdb[8] = target as u8;
|
||||||
|
cdb[9] = r0;
|
||||||
|
cdb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a 10-byte CDB for [`Verb::Poke`]: write a single byte `val` to the
|
||||||
|
/// arbitrary 32-bit address `target` (RAM or MMIO). Target is packed
|
||||||
|
/// big-endian in `cdb[5..9]` and `val` rides in `cdb[9]`. Carries the
|
||||||
|
/// [`DEBUG_KNOCK`] at `cdb[2..4]`. NO bounds check on the address — this is a
|
||||||
|
/// diagnostic primitive for on-drive state discovery, not a durable verb.
|
||||||
|
pub fn build_poke_cdb(target: u32, val: u8) -> [u8; CDB_LEN] {
|
||||||
|
let mut cdb = [0u8; CDB_LEN];
|
||||||
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
||||||
|
cdb[CDB_MODE] = KNOCK_MODE;
|
||||||
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
||||||
|
cdb[CDB_VERB] = Verb::Poke as u8;
|
||||||
|
cdb[5] = (target >> 24) as u8;
|
||||||
|
cdb[6] = (target >> 16) as u8;
|
||||||
|
cdb[7] = (target >> 8) as u8;
|
||||||
|
cdb[8] = target as u8;
|
||||||
|
cdb[9] = val;
|
||||||
|
cdb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a 10-byte CDB for [`Verb::Reboot`]: force the firmware boot function's
|
||||||
|
/// cold path (soft-reboot the controller). Carries the [`DEBUG_KNOCK`] at
|
||||||
|
/// `cdb[2..4]`. NO arguments are transmitted on the wire — the target VA is
|
||||||
|
/// baked into the emitted handler at build time (per-image, resolved from the
|
||||||
|
/// boot-init signature). Requests a [`MIN_ALLOC_LEN`]-byte data-in like every
|
||||||
|
/// other durable-shape verb: the drive returns Aborted Command mid-reboot
|
||||||
|
/// anyway, so callers should tolerate a rejection on this send and re-probe
|
||||||
|
/// identity after a short delay.
|
||||||
|
pub fn build_reboot_cdb() -> [u8; CDB_LEN] {
|
||||||
|
let mut cdb = [0u8; CDB_LEN];
|
||||||
|
cdb[CDB_OPCODE] = READ_BUFFER_OPCODE;
|
||||||
|
cdb[CDB_MODE] = KNOCK_MODE;
|
||||||
|
cdb[CDB_KNOCK..CDB_KNOCK + 2].copy_from_slice(&DEBUG_KNOCK);
|
||||||
|
cdb[CDB_VERB] = Verb::Reboot as u8;
|
||||||
|
cdb[CDB_ALLOC_LEN] = (MIN_ALLOC_LEN >> 8) as u8;
|
||||||
|
cdb[CDB_ALLOC_LEN + 1] = MIN_ALLOC_LEN as u8;
|
||||||
|
cdb
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a device data response leads with [`RESP_MAGIC`].
|
/// Whether a device data response leads with [`RESP_MAGIC`].
|
||||||
pub fn verify_response(bytes: &[u8]) -> bool {
|
pub fn verify_response(bytes: &[u8]) -> bool {
|
||||||
bytes.starts_with(RESP_MAGIC)
|
bytes.starts_with(RESP_MAGIC)
|
||||||
@@ -365,10 +454,8 @@ pub struct FeatureStates {
|
|||||||
pub bd: u8,
|
pub bd: u8,
|
||||||
/// [`Feature::Hrl`] state.
|
/// [`Feature::Hrl`] state.
|
||||||
pub hrl: u8,
|
pub hrl: u8,
|
||||||
/// [`Feature::Ake`] state.
|
/// [`Feature::Encryption`] state (the consolidated cert/bus bypass).
|
||||||
pub ake: u8,
|
pub encryption: u8,
|
||||||
/// [`Feature::Bus`] state.
|
|
||||||
pub bus: u8,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeatureStates {
|
impl FeatureStates {
|
||||||
@@ -380,8 +467,7 @@ impl FeatureStates {
|
|||||||
uhd: STATE_PASSTHROUGH,
|
uhd: STATE_PASSTHROUGH,
|
||||||
bd: STATE_PASSTHROUGH,
|
bd: STATE_PASSTHROUGH,
|
||||||
hrl: STATE_PASSTHROUGH,
|
hrl: STATE_PASSTHROUGH,
|
||||||
ake: STATE_PASSTHROUGH,
|
encryption: STATE_PASSTHROUGH,
|
||||||
bus: STATE_PASSTHROUGH,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +479,7 @@ impl FeatureStates {
|
|||||||
Feature::Uhd => self.uhd,
|
Feature::Uhd => self.uhd,
|
||||||
Feature::Bd => self.bd,
|
Feature::Bd => self.bd,
|
||||||
Feature::Hrl => self.hrl,
|
Feature::Hrl => self.hrl,
|
||||||
Feature::Ake => self.ake,
|
Feature::Encryption => self.encryption,
|
||||||
Feature::Bus => self.bus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,8 +490,7 @@ impl FeatureStates {
|
|||||||
Feature::Uhd => self.uhd = state,
|
Feature::Uhd => self.uhd = state,
|
||||||
Feature::Bd => self.bd = state,
|
Feature::Bd => self.bd = state,
|
||||||
Feature::Hrl => self.hrl = state,
|
Feature::Hrl => self.hrl = state,
|
||||||
Feature::Ake => self.ake = state,
|
Feature::Encryption => self.encryption = state,
|
||||||
Feature::Bus => self.bus = state,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,7 +552,7 @@ const CMD_TIMEOUT_MS: u32 = 5_000;
|
|||||||
/// # fn demo(scsi: &mut dyn freemkv_unlock::scsi::ScsiTransport) {
|
/// # fn demo(scsi: &mut dyn freemkv_unlock::scsi::ScsiTransport) {
|
||||||
/// let mut fw = FirmwareControl::new(scsi);
|
/// let mut fw = FirmwareControl::new(scsi);
|
||||||
/// if fw.is_freemkv().unwrap_or(false) {
|
/// if fw.is_freemkv().unwrap_or(false) {
|
||||||
/// // Real AKE on a UHD disc, revocation ignored, bus de-encrypted:
|
/// // Encryption off on a UHD disc, revocation ignored:
|
||||||
/// let _ = fw.arm_oem_uhd();
|
/// let _ = fw.arm_oem_uhd();
|
||||||
/// }
|
/// }
|
||||||
/// # }
|
/// # }
|
||||||
@@ -639,15 +723,13 @@ impl<'a> FirmwareControl<'a> {
|
|||||||
pub fn skip_hrl(&mut self) -> Result<()> {
|
pub fn skip_hrl(&mut self) -> Result<()> {
|
||||||
self.set(Feature::Hrl, STATE_OFF)
|
self.set(Feature::Hrl, STATE_OFF)
|
||||||
}
|
}
|
||||||
/// Null the drive-host AKE (drive acts pre-authenticated). The unlock direction
|
/// Disable every drive-side encryption/cert requirement in one shot: the
|
||||||
/// is now [`STATE_OFF`] (`0x00` = handshake off/bypassed).
|
/// drive acts pre-authenticated (no handshake) AND content returns de-bussed
|
||||||
pub fn null_ake(&mut self) -> Result<()> {
|
/// (no in-transit bus encryption). The consolidated `Encryption` lever
|
||||||
self.set(Feature::Ake, STATE_OFF)
|
/// replaces the old separate `Ake`/`Bus` levers — wire id `0x07` (`Bus`) is
|
||||||
}
|
/// retired. The unlock direction is [`STATE_OFF`] (`0x00`).
|
||||||
/// Turn AACS in-transit bus encryption off (content returned de-bussed). The
|
pub fn disable_encryption(&mut self) -> Result<()> {
|
||||||
/// unlock direction is now [`STATE_OFF`] (`0x00` = bus encryption off).
|
self.set(Feature::Encryption, STATE_OFF)
|
||||||
pub fn bus_off(&mut self) -> Result<()> {
|
|
||||||
self.set(Feature::Bus, STATE_OFF)
|
|
||||||
}
|
}
|
||||||
/// Region-free (RPC-1). Sends [`REGION_FREE`] (`0x0F`) — under the migrated
|
/// Region-free (RPC-1). Sends [`REGION_FREE`] (`0x0F`) — under the migrated
|
||||||
/// region scheme `0x01` now means "force DVD region 1", not region-free.
|
/// region scheme `0x01` now means "force DVD region 1", not region-free.
|
||||||
@@ -684,38 +766,37 @@ impl<'a> FirmwareControl<'a> {
|
|||||||
self.set_verify(Feature::Hrl, STATE_OFF)
|
self.set_verify(Feature::Hrl, STATE_OFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **arm_oem_uhd** — OEM-style UHD (AACS 2.0) rip with a REAL AKE.
|
/// **arm_oem_uhd** — OEM-style UHD (AACS 2.0) rip.
|
||||||
///
|
///
|
||||||
/// Sets: `Uhd = on` (mode-gate neutralized so the drive engages the UHD
|
/// Sets: `Uhd = on` (mode-gate neutralized so the drive engages the UHD
|
||||||
/// disc), `Hrl = skip` (revocation off), `Bus = off` (content de-bussed).
|
/// disc), `Hrl = skip` (revocation off), `Encryption = off` (content
|
||||||
/// The host still runs the real AKE. Rips: UHD via the OEM cert path.
|
/// de-bussed — the consolidated cert/bus bypass). Rips: UHD via the OEM
|
||||||
|
/// path.
|
||||||
pub fn arm_oem_uhd(&mut self) -> Result<()> {
|
pub fn arm_oem_uhd(&mut self) -> Result<()> {
|
||||||
self.set_verify(Feature::Uhd, STATE_ON)?;
|
self.set_verify(Feature::Uhd, STATE_ON)?;
|
||||||
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
||||||
self.set_verify(Feature::Bus, STATE_OFF)
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **arm_bypass_bd** — full-bypass Blu-ray rip (NO host cert needed).
|
/// **arm_bypass_bd** — full-bypass Blu-ray rip (NO host cert needed).
|
||||||
///
|
///
|
||||||
/// Sets: `Ake = null` (drive acts pre-authenticated, so a bare VID read
|
/// Sets: `Encryption = off` (the consolidated cert/bus bypass — drive acts
|
||||||
/// returns the volume ID with no cert and no AKE) and `Hrl = skip`
|
/// pre-authenticated, no handshake, content de-bussed) and `Hrl = skip`
|
||||||
/// (revocation off, so a revoked/absent cert never trips the HRL check).
|
/// (revocation off). Rips: BD with no cert.
|
||||||
/// Rips: BD with no cert.
|
|
||||||
pub fn arm_bypass_bd(&mut self) -> Result<()> {
|
pub fn arm_bypass_bd(&mut self) -> Result<()> {
|
||||||
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
||||||
self.set_verify(Feature::Ake, STATE_OFF)
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **arm_bypass_uhd** — full-bypass UHD rip (NO host cert needed).
|
/// **arm_bypass_uhd** — full-bypass UHD rip (NO host cert needed).
|
||||||
///
|
///
|
||||||
/// Sets: `Uhd = on` (engage the UHD disc), `Ake = null` (skip the
|
/// Sets: `Uhd = on` (engage the UHD disc), `Encryption = off` (the
|
||||||
/// handshake), `Bus = off` (content de-bussed), and `Hrl = skip`
|
/// consolidated cert/bus bypass), and `Hrl = skip` (revocation off).
|
||||||
/// (revocation off). Rips: UHD with no cert.
|
/// Rips: UHD with no cert.
|
||||||
pub fn arm_bypass_uhd(&mut self) -> Result<()> {
|
pub fn arm_bypass_uhd(&mut self) -> Result<()> {
|
||||||
self.set_verify(Feature::Uhd, STATE_ON)?;
|
self.set_verify(Feature::Uhd, STATE_ON)?;
|
||||||
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
self.set_verify(Feature::Hrl, STATE_OFF)?;
|
||||||
self.set_verify(Feature::Ake, STATE_OFF)?;
|
self.set_verify(Feature::Encryption, STATE_OFF)
|
||||||
self.set_verify(Feature::Bus, STATE_OFF)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **arm_stealth_oem** — return the drive to byte-for-byte OEM behaviour.
|
/// **arm_stealth_oem** — return the drive to byte-for-byte OEM behaviour.
|
||||||
|
|||||||
+88
-52
@@ -13,7 +13,11 @@ fn verb_values_match_abi() {
|
|||||||
assert_eq!(Verb::Get as u8, 0x03);
|
assert_eq!(Verb::Get as u8, 0x03);
|
||||||
assert_eq!(Verb::Reset as u8, 0x04);
|
assert_eq!(Verb::Reset as u8, 0x04);
|
||||||
assert_eq!(Verb::DumpAll as u8, 0x09);
|
assert_eq!(Verb::DumpAll as u8, 0x09);
|
||||||
|
assert_eq!(Verb::FlashWrite as u8, 0x0A);
|
||||||
assert_eq!(Verb::Save as u8, 0x0B);
|
assert_eq!(Verb::Save as u8, 0x0B);
|
||||||
|
assert_eq!(Verb::Call as u8, 0x0C);
|
||||||
|
assert_eq!(Verb::Poke as u8, 0x0D);
|
||||||
|
assert_eq!(Verb::Reboot as u8, 0x0F);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -23,8 +27,17 @@ fn feature_values_match_abi() {
|
|||||||
assert_eq!(Feature::Uhd as u8, 0x03);
|
assert_eq!(Feature::Uhd as u8, 0x03);
|
||||||
assert_eq!(Feature::Bd as u8, 0x04);
|
assert_eq!(Feature::Bd as u8, 0x04);
|
||||||
assert_eq!(Feature::Hrl as u8, 0x05);
|
assert_eq!(Feature::Hrl as u8, 0x05);
|
||||||
assert_eq!(Feature::Ake as u8, 0x06);
|
assert_eq!(Feature::Encryption as u8, 0x06);
|
||||||
assert_eq!(Feature::Bus as u8, 0x07);
|
// Wire id 0x07 (was `Bus`) is retired — proven inert on BU40N/MT1959.
|
||||||
|
assert_eq!(ALL_FEATURES.len(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debug_knock_distinct_from_safe_knock() {
|
||||||
|
// The fw dispatches Call/Poke ONLY under DEBUG_KNOCK; a typo of a safe verb
|
||||||
|
// vs a debug verb under the wrong knock never crosses the line.
|
||||||
|
assert_ne!(KNOCK, DEBUG_KNOCK);
|
||||||
|
assert_eq!(DEBUG_KNOCK, [0xDE, 0xB9]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -52,21 +65,22 @@ fn state_and_frame_constants_match_abi() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_set_cdb_exact_bytes() {
|
fn build_set_cdb_exact_bytes() {
|
||||||
// SET Ake = ON: verb 02, feature 06, state 01, alloc MIN_ALLOC_LEN (0x0040)
|
// SET Encryption = ON: verb 02, feature 06, state 01, alloc MIN_ALLOC_LEN
|
||||||
// at cdb[7..9] big-endian — the drive aborts a sub-16-byte data-in.
|
// (0x0040) at cdb[7..9] big-endian — the drive aborts a sub-16-byte data-in.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
build_set_cdb(Feature::Ake, STATE_ON),
|
build_set_cdb(Feature::Encryption, STATE_ON),
|
||||||
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x01, 0x00, 0x40, 0x00]
|
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x01, 0x00, 0x40, 0x00]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_get_cdb_exact_bytes() {
|
fn build_get_cdb_exact_bytes() {
|
||||||
// GET Bus: verb 03, feature 07, alloc MIN_ALLOC_LEN (0x0040) at cdb[7..9]
|
// GET Encryption: verb 03, feature 06, alloc MIN_ALLOC_LEN (0x0040) at
|
||||||
// big-endian — the drive aborts a 1-byte data-in; state read from offset 0.
|
// cdb[7..9] big-endian — the drive aborts a 1-byte data-in; state read
|
||||||
|
// from offset 0.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
build_get_cdb(Feature::Bus),
|
build_get_cdb(Feature::Encryption),
|
||||||
[0x3C, 0x0E, 0xC0, 0xDE, 0x03, 0x07, 0x00, 0x00, 0x40, 0x00]
|
[0x3C, 0x0E, 0xC0, 0xDE, 0x03, 0x06, 0x00, 0x00, 0x40, 0x00]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +92,39 @@ fn vendor_commands_floor_alloc_len_at_min_for_hw() {
|
|||||||
assert_eq!(MIN_ALLOC_LEN, 64);
|
assert_eq!(MIN_ALLOC_LEN, 64);
|
||||||
let alloc =
|
let alloc =
|
||||||
|cdb: &[u8; CDB_LEN]| u16::from_be_bytes([cdb[CDB_ALLOC_LEN], cdb[CDB_ALLOC_LEN + 1]]);
|
|cdb: &[u8; CDB_LEN]| u16::from_be_bytes([cdb[CDB_ALLOC_LEN], cdb[CDB_ALLOC_LEN + 1]]);
|
||||||
assert_eq!(alloc(&build_set_cdb(Feature::Ake, STATE_ON)), MIN_ALLOC_LEN);
|
assert_eq!(
|
||||||
assert_eq!(alloc(&build_get_cdb(Feature::Ake)), MIN_ALLOC_LEN);
|
alloc(&build_set_cdb(Feature::Encryption, STATE_ON)),
|
||||||
|
MIN_ALLOC_LEN
|
||||||
|
);
|
||||||
|
assert_eq!(alloc(&build_get_cdb(Feature::Encryption)), MIN_ALLOC_LEN);
|
||||||
assert_eq!(alloc(&build_reset_cdb(RESET_TO_OEM)), MIN_ALLOC_LEN);
|
assert_eq!(alloc(&build_reset_cdb(RESET_TO_OEM)), MIN_ALLOC_LEN);
|
||||||
assert_eq!(alloc(&build_save_cdb()), MIN_ALLOC_LEN);
|
assert_eq!(alloc(&build_save_cdb()), MIN_ALLOC_LEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_call_and_poke_cdb_exact_bytes() {
|
||||||
|
// Call: verb 0x0C at cdb[4], DEBUG_KNOCK at cdb[2..4], target big-endian
|
||||||
|
// in cdb[5..9], r0 in cdb[9].
|
||||||
|
assert_eq!(
|
||||||
|
build_call_cdb(0x0102_0304, 0xAB),
|
||||||
|
[0x3C, 0x0E, 0xDE, 0xB9, 0x0C, 0x01, 0x02, 0x03, 0x04, 0xAB]
|
||||||
|
);
|
||||||
|
// Poke: verb 0x0D at cdb[4], DEBUG_KNOCK at cdb[2..4], target big-endian
|
||||||
|
// in cdb[5..9], value in cdb[9].
|
||||||
|
assert_eq!(
|
||||||
|
build_poke_cdb(0x0200_0E40, 0xAA),
|
||||||
|
[0x3C, 0x0E, 0xDE, 0xB9, 0x0D, 0x02, 0x00, 0x0E, 0x40, 0xAA]
|
||||||
|
);
|
||||||
|
// Reboot: verb 0x0F at cdb[4], DEBUG_KNOCK at cdb[2..4], no on-wire target/arg
|
||||||
|
// (the boot-function-entry VA is baked into the emitted handler at build time).
|
||||||
|
// Alloc floors at MIN_ALLOC_LEN (0x0040 big-endian at cdb[7..9]) so the drive
|
||||||
|
// does not abort the data-in before the verb executes.
|
||||||
|
assert_eq!(
|
||||||
|
build_reboot_cdb(),
|
||||||
|
[0x3C, 0x0E, 0xDE, 0xB9, 0x0F, 0x00, 0x00, 0x00, 0x40, 0x00]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_reset_and_identity_cdb_exact_bytes() {
|
fn build_reset_and_identity_cdb_exact_bytes() {
|
||||||
// RESET carries its mode in the state slot (cdb[6]) and floors its data-in at
|
// RESET carries its mode in the state slot (cdb[6]) and floors its data-in at
|
||||||
@@ -271,13 +312,13 @@ impl ScsiTransport for FwMock {
|
|||||||
#[test]
|
#[test]
|
||||||
fn identity_detects_freemkv_and_issues_identity_cdb() {
|
fn identity_detects_freemkv_and_issues_identity_cdb() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
m.states.ake = STATE_ON;
|
m.states.encryption = STATE_ON;
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
let id = fw.identity().expect("no fault").expect("is freemkv");
|
let id = fw.identity().expect("no fault").expect("is freemkv");
|
||||||
assert_eq!(id.version, "0.6.6");
|
assert_eq!(id.version, "0.6.6");
|
||||||
// States no longer ride in IDENTITY — read them via GET.
|
// States no longer ride in IDENTITY — read them via GET.
|
||||||
assert_eq!(fw.get(Feature::Ake).expect("get"), STATE_ON);
|
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_ON);
|
||||||
assert!(fw.is_freemkv().expect("no fault"));
|
assert!(fw.is_freemkv().expect("no fault"));
|
||||||
}
|
}
|
||||||
assert_eq!(m.cdbs[0], build_identity_cdb(MEMREAD_LEN as u16));
|
assert_eq!(m.cdbs[0], build_identity_cdb(MEMREAD_LEN as u16));
|
||||||
@@ -311,20 +352,19 @@ fn get_and_set_roundtrip_issue_the_right_cdbs() {
|
|||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_PASSTHROUGH);
|
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_PASSTHROUGH);
|
||||||
fw.set(Feature::Bus, STATE_ON).expect("set");
|
fw.set(Feature::Encryption, STATE_ON).expect("set");
|
||||||
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_ON);
|
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_ON);
|
||||||
}
|
}
|
||||||
assert_eq!(m.cdbs[0], build_get_cdb(Feature::Bus));
|
assert_eq!(m.cdbs[0], build_get_cdb(Feature::Encryption));
|
||||||
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bus, STATE_ON));
|
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Encryption, STATE_ON));
|
||||||
assert_eq!(m.cdbs[2], build_get_cdb(Feature::Bus));
|
assert_eq!(m.cdbs[2], build_get_cdb(Feature::Encryption));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reset_sends_reset_cdb_and_clears_state() {
|
fn reset_sends_reset_cdb_and_clears_state() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
m.states.ake = STATE_ON;
|
m.states.encryption = STATE_ON;
|
||||||
m.states.bus = STATE_ON;
|
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
fw.reset().expect("reset");
|
fw.reset().expect("reset");
|
||||||
@@ -350,12 +390,12 @@ fn save_then_reset_to_flash_restores_the_saved_value() {
|
|||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
fw.set(Feature::Bus, STATE_OFF).expect("set");
|
fw.set(Feature::Encryption, STATE_OFF).expect("set");
|
||||||
fw.save().expect("save");
|
fw.save().expect("save");
|
||||||
fw.reset().expect("reset to OEM");
|
fw.reset().expect("reset to OEM");
|
||||||
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_PASSTHROUGH);
|
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_PASSTHROUGH);
|
||||||
fw.reset_to_flash().expect("reset to flash");
|
fw.reset_to_flash().expect("reset to flash");
|
||||||
assert_eq!(fw.get(Feature::Bus).expect("get"), STATE_OFF);
|
assert_eq!(fw.get(Feature::Encryption).expect("get"), STATE_OFF);
|
||||||
}
|
}
|
||||||
// The verbs issued, in order: SET, SAVE, RESET, GET, RESET, GET.
|
// The verbs issued, in order: SET, SAVE, RESET, GET, RESET, GET.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -386,10 +426,10 @@ fn states_probes_every_feature_in_order() {
|
|||||||
}
|
}
|
||||||
assert_eq!(states.speed, SPEED_MAX);
|
assert_eq!(states.speed, SPEED_MAX);
|
||||||
assert_eq!(states.hrl, STATE_ON);
|
assert_eq!(states.hrl, STATE_ON);
|
||||||
// Seven GETs, one per feature, in ALL_FEATURES order.
|
// Six GETs, one per feature, in ALL_FEATURES order (Bus id 0x07 retired).
|
||||||
assert_eq!(m.cdbs.len(), 7);
|
assert_eq!(m.cdbs.len(), 6);
|
||||||
let features: Vec<u8> = m.cdbs.iter().map(|c| c[CDB_FEATURE]).collect();
|
let features: Vec<u8> = m.cdbs.iter().map(|c| c[CDB_FEATURE]).collect();
|
||||||
assert_eq!(features, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
|
assert_eq!(features, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
|
||||||
assert!(m.verbs().iter().all(|&v| v == Verb::Get as u8));
|
assert!(m.verbs().iter().all(|&v| v == Verb::Get as u8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,8 +456,7 @@ fn typed_setters_issue_expected_set_cdbs() {
|
|||||||
fw.enable_uhd().unwrap();
|
fw.enable_uhd().unwrap();
|
||||||
fw.disable_bd().unwrap();
|
fw.disable_bd().unwrap();
|
||||||
fw.skip_hrl().unwrap();
|
fw.skip_hrl().unwrap();
|
||||||
fw.null_ake().unwrap();
|
fw.disable_encryption().unwrap();
|
||||||
fw.bus_off().unwrap();
|
|
||||||
fw.region_free().unwrap();
|
fw.region_free().unwrap();
|
||||||
fw.force_region_bd(BdRegion::B).unwrap();
|
fw.force_region_bd(BdRegion::B).unwrap();
|
||||||
fw.force_region_dvd(2).unwrap();
|
fw.force_region_dvd(2).unwrap();
|
||||||
@@ -425,16 +464,15 @@ fn typed_setters_issue_expected_set_cdbs() {
|
|||||||
}
|
}
|
||||||
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
||||||
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bd, STATE_OFF));
|
assert_eq!(m.cdbs[1], build_set_cdb(Feature::Bd, STATE_OFF));
|
||||||
// The HRL/AKE/BUS unlock direction is now STATE_OFF (0x00), not STATE_ON.
|
// The HRL/Encryption unlock direction is STATE_OFF (0x00), not STATE_ON.
|
||||||
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
|
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
|
||||||
assert_eq!(m.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
|
assert_eq!(m.cdbs[3], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
|
|
||||||
// Region-free is REGION_FREE (0x0F) — 0x01 now forces DVD region 1.
|
// Region-free is REGION_FREE (0x0F) — 0x01 now forces DVD region 1.
|
||||||
assert_eq!(m.cdbs[5], build_set_cdb(Feature::Region, REGION_FREE));
|
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Region, REGION_FREE));
|
||||||
assert_eq!(m.cdbs[6], build_set_cdb(Feature::Region, REGION_BD_B));
|
assert_eq!(m.cdbs[5], build_set_cdb(Feature::Region, REGION_BD_B));
|
||||||
// force_region_dvd(2) = REGION_DVD_BASE + 2 = 0x02 under the new base.
|
// force_region_dvd(2) = REGION_DVD_BASE + 2 = 0x02 under the new base.
|
||||||
assert_eq!(m.cdbs[7], build_set_cdb(Feature::Region, 0x02));
|
assert_eq!(m.cdbs[6], build_set_cdb(Feature::Region, 0x02));
|
||||||
assert_eq!(m.cdbs[8], build_set_cdb(Feature::Speed, SPEED_MAX));
|
assert_eq!(m.cdbs[7], build_set_cdb(Feature::Speed, SPEED_MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -461,7 +499,7 @@ fn arm_oem_bd_sets_and_verifies_hrl_skip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arm_oem_uhd_sets_uhd_hrl_bus_each_verified() {
|
fn arm_oem_uhd_sets_uhd_hrl_encryption_each_verified() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
@@ -476,50 +514,48 @@ fn arm_oem_uhd_sets_uhd_hrl_bus_each_verified() {
|
|||||||
);
|
);
|
||||||
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
||||||
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
|
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, STATE_OFF));
|
||||||
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Bus, STATE_OFF));
|
assert_eq!(m.cdbs[4], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
assert_eq!(m.states.uhd, STATE_ON);
|
assert_eq!(m.states.uhd, STATE_ON);
|
||||||
assert_eq!(m.states.hrl, STATE_OFF);
|
assert_eq!(m.states.hrl, STATE_OFF);
|
||||||
assert_eq!(m.states.bus, STATE_OFF);
|
assert_eq!(m.states.encryption, STATE_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arm_bypass_bd_nulls_ake() {
|
fn arm_bypass_bd_disables_encryption() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
fw.arm_bypass_bd().expect("armed");
|
fw.arm_bypass_bd().expect("armed");
|
||||||
}
|
}
|
||||||
// Hrl=off (revocation skip) is set+verified FIRST, then Ake=null.
|
// Hrl=off (revocation skip) is set+verified FIRST, then Encryption=off.
|
||||||
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Hrl, STATE_OFF));
|
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[1], build_get_cdb(Feature::Hrl));
|
||||||
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Ake, STATE_OFF));
|
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
assert_eq!(m.cdbs[3], build_get_cdb(Feature::Ake));
|
assert_eq!(m.cdbs[3], build_get_cdb(Feature::Encryption));
|
||||||
assert_eq!(m.states.hrl, STATE_OFF);
|
assert_eq!(m.states.hrl, STATE_OFF);
|
||||||
assert_eq!(m.states.ake, STATE_OFF);
|
assert_eq!(m.states.encryption, STATE_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arm_bypass_uhd_sets_uhd_ake_bus() {
|
fn arm_bypass_uhd_sets_uhd_hrl_encryption() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
fw.arm_bypass_uhd().expect("armed");
|
fw.arm_bypass_uhd().expect("armed");
|
||||||
}
|
}
|
||||||
// Recipe order: Uhd=on, Hrl=off, Ake=null, Bus=off (each SET then verifying GET).
|
// Recipe order: Uhd=on, Hrl=off, Encryption=off (each SET then verifying GET).
|
||||||
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
assert_eq!(m.cdbs[0], build_set_cdb(Feature::Uhd, STATE_ON));
|
||||||
assert_eq!(m.cdbs[2], build_set_cdb(Feature::Hrl, 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[4], build_set_cdb(Feature::Encryption, 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.uhd, STATE_ON);
|
||||||
assert_eq!(m.states.hrl, STATE_OFF);
|
assert_eq!(m.states.hrl, STATE_OFF);
|
||||||
assert_eq!(m.states.ake, STATE_OFF);
|
assert_eq!(m.states.encryption, STATE_OFF);
|
||||||
assert_eq!(m.states.bus, STATE_OFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arm_stealth_oem_resets_then_verifies_all_passthrough() {
|
fn arm_stealth_oem_resets_then_verifies_all_passthrough() {
|
||||||
let mut m = FwMock::new();
|
let mut m = FwMock::new();
|
||||||
m.states.ake = STATE_ON;
|
m.states.encryption = STATE_ON;
|
||||||
m.states.uhd = STATE_ON;
|
m.states.uhd = STATE_ON;
|
||||||
{
|
{
|
||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
@@ -539,7 +575,7 @@ fn arm_by_recipe_enum_dispatches() {
|
|||||||
let mut fw = FirmwareControl::new(&mut m);
|
let mut fw = FirmwareControl::new(&mut m);
|
||||||
fw.arm(ArmRecipe::BypassBd).expect("armed");
|
fw.arm(ArmRecipe::BypassBd).expect("armed");
|
||||||
}
|
}
|
||||||
assert_eq!(m.states.ake, STATE_OFF);
|
assert_eq!(m.states.encryption, STATE_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+44
-55
@@ -11,11 +11,14 @@
|
|||||||
//! module's CDB builders so the wire framing never drifts.
|
//! module's CDB builders so the wire framing never drifts.
|
||||||
//!
|
//!
|
||||||
//! Unlock mapping onto the grammar: region-free = `Set(Region, free)`
|
//! Unlock mapping onto the grammar: region-free = `Set(Region, free)`
|
||||||
//! (`REGION_FREE`), riplock lift = `Set(Speed, max)` (`SPEED_MAX`), the
|
//! (`REGION_FREE`), riplock lift = `Set(Speed, max)` (`SPEED_MAX`), and the
|
||||||
//! load-bearing transport unlock = `Set(Ake, off)` (`STATE_OFF` — drive acts
|
//! load-bearing transport unlock = `Set(Encryption, off)` (`STATE_OFF` — the
|
||||||
//! pre-authenticated → a bare `0xAD` returns the VID with no cert and no AKE), and
|
//! consolidated cert/bus bypass: drive acts pre-authenticated → a bare `0xAD`
|
||||||
//! the trailing bus-off = `Set(Bus, off)` (`STATE_OFF`, content de-bussed). Under
|
//! returns the VID with no cert and no AKE, and content returns de-bussed).
|
||||||
//! the migrated spec the HRL/AKE/BUS unlock direction is `STATE_OFF`, not `0x01`.
|
//! The old separate `Bus` lever (wire id `0x07`) is retired — it was proven
|
||||||
|
//! inert on BU40N/MT1959, and the single `Encryption` lever de-busses on its
|
||||||
|
//! own. Under the migrated spec the HRL/Encryption unlock direction is
|
||||||
|
//! `STATE_OFF`, not `0x01`.
|
||||||
|
|
||||||
use crate::firmware::{
|
use crate::firmware::{
|
||||||
Feature, MEMREAD_LEN, MIN_ALLOC_LEN, REGION_FREE, RESP_MAGIC, SPEED_MAX, STATE_OFF,
|
Feature, MEMREAD_LEN, MIN_ALLOC_LEN, REGION_FREE, RESP_MAGIC, SPEED_MAX, STATE_OFF,
|
||||||
@@ -162,8 +165,8 @@ impl FreemkvUnlocker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Full freemkv unlock: IDENTITY (hard gate) → Set(Region,free) →
|
// Full freemkv unlock: IDENTITY (hard gate) → Set(Region,free) →
|
||||||
// Set(Speed,max) (best-effort) → Set(Ake,off) (LOAD-BEARING) → bare 0xAD VID
|
// Set(Speed,max) (best-effort) → Set(Encryption,off) (LOAD-BEARING —
|
||||||
// (best-effort) → Set(Bus,off) (trailing, best-effort).
|
// consolidated cert/bus bypass) → bare 0xAD VID (best-effort).
|
||||||
fn full_unlock(
|
fn full_unlock(
|
||||||
&self,
|
&self,
|
||||||
scsi: &mut dyn ScsiTransport,
|
scsi: &mut dyn ScsiTransport,
|
||||||
@@ -193,41 +196,31 @@ impl FreemkvUnlocker {
|
|||||||
// Region-free + riplock lift (best-effort features).
|
// Region-free + riplock lift (best-effort features).
|
||||||
best_effort(self.set(scsi, Feature::Region, REGION_FREE), "region")?;
|
best_effort(self.set(scsi, Feature::Region, REGION_FREE), "region")?;
|
||||||
best_effort(self.set(scsi, Feature::Speed, SPEED_MAX), "speed")?;
|
best_effort(self.set(scsi, Feature::Speed, SPEED_MAX), "speed")?;
|
||||||
// Ake = off/null (LOAD-BEARING): drive acts pre-authenticated, so a bare
|
// Encryption = off (LOAD-BEARING): the consolidated cert/bus bypass —
|
||||||
// 0xAD returns the VID with no cert/AKE. No fallback. The migrated spec
|
// drive acts pre-authenticated (so a bare 0xAD returns the VID with no
|
||||||
// puts the null/bypass direction on STATE_OFF (0x00), not 0x01.
|
// cert/AKE) AND content returns de-bussed. No fallback. The old
|
||||||
match self.set(scsi, Feature::Ake, STATE_OFF) {
|
// separate Bus lever (wire id 0x07) is retired.
|
||||||
|
match self.set(scsi, Feature::Encryption, STATE_OFF) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(UnlockError::Transport) => return Err(UnlockError::Transport),
|
Err(UnlockError::Transport) => return Err(UnlockError::Transport),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "freemkv_null_ake_rejected",
|
phase = "freemkv_disable_encryption_rejected",
|
||||||
"Set(Ake, null) rejected — cannot unlock this drive"
|
"Set(Encryption, off) rejected — cannot unlock this drive"
|
||||||
);
|
);
|
||||||
return Err(UnlockError::VidUnavailable);
|
return Err(UnlockError::VidUnavailable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Bare 0xAD VID read — the shared BEST-EFFORT reader (identical to the
|
// Bare 0xAD VID read — the shared BEST-EFFORT reader (identical to the
|
||||||
// LD/Renesas routes). The null AKE already unlocked the drive, so a VID
|
// LD/Renesas routes). Encryption=off already unlocked the drive, so a
|
||||||
// miss must not discard it: only a dead bus propagates (`?`), else `None`.
|
// VID miss must not discard it: only a dead bus propagates (`?`), else `None`.
|
||||||
let vid = crate::vid::read_aacs_vid(scsi)?;
|
let vid = crate::vid::read_aacs_vid(scsi)?;
|
||||||
// Trailing Set(Bus, off): remove in-transit bus encryption (STATE_OFF is
|
|
||||||
// the de-bussed direction). FULLY best-effort — inert without the lever,
|
|
||||||
// and a failure here never discards an already-obtained unlock.
|
|
||||||
if let Err(e) = self.set(scsi, Feature::Bus, STATE_OFF) {
|
|
||||||
tracing::debug!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "freemkv_bus_off_unavailable",
|
|
||||||
?e,
|
|
||||||
"bus-off (Set Bus) not applied; continuing"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "freemkv_unlocked",
|
phase = "freemkv_unlocked",
|
||||||
has_vid = vid.is_some(),
|
has_vid = vid.is_some(),
|
||||||
"freemkv drive unlocked (null AKE — Ake=off)"
|
"freemkv drive unlocked (Encryption=off — consolidated cert/bus bypass)"
|
||||||
);
|
);
|
||||||
Ok(Unlocked { vid, bus_key: None })
|
Ok(Unlocked { vid, bus_key: None })
|
||||||
}
|
}
|
||||||
@@ -370,15 +363,12 @@ mod tests {
|
|||||||
build_set_cdb(Feature::Speed, SPEED_MAX),
|
build_set_cdb(Feature::Speed, SPEED_MAX),
|
||||||
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x01, 0x00, 0x00, 0x40, 0x00]
|
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x01, 0x00, 0x00, 0x40, 0x00]
|
||||||
);
|
);
|
||||||
// Ake/Bus unlock direction is now STATE_OFF (0x00) at cdb[6], not 0x01.
|
// Encryption unlock direction is STATE_OFF (0x00) at cdb[6]; wire id
|
||||||
|
// 0x07 (was `Bus`) is retired.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
build_set_cdb(Feature::Ake, STATE_OFF),
|
build_set_cdb(Feature::Encryption, STATE_OFF),
|
||||||
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x00, 0x00, 0x40, 0x00]
|
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x06, 0x00, 0x00, 0x40, 0x00]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
build_set_cdb(Feature::Bus, STATE_OFF),
|
|
||||||
[0x3C, 0x0E, 0xC0, 0xDE, 0x02, 0x07, 0x00, 0x00, 0x40, 0x00]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Detection ────────────────────────────────────────────────────────
|
// ── Detection ────────────────────────────────────────────────────────
|
||||||
@@ -437,19 +427,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_ake_null_issues_the_set_ake_on_cdb() {
|
fn set_encryption_off_issues_the_set_encryption_cdb() {
|
||||||
let mut t = MockTransport::always(Reply::good(vec![]));
|
let mut t = MockTransport::always(Reply::good(vec![]));
|
||||||
FreemkvUnlocker::new()
|
FreemkvUnlocker::new()
|
||||||
.set(&mut t, Feature::Ake, STATE_OFF)
|
.set(&mut t, Feature::Encryption, STATE_OFF)
|
||||||
.expect("ok");
|
.expect("ok");
|
||||||
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Ake, STATE_OFF));
|
assert_eq!(t.cdbs[0], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_drive_rejection_is_not_applicable() {
|
fn set_drive_rejection_is_not_applicable() {
|
||||||
let mut t = MockTransport::always(Reply::illegal_request());
|
let mut t = MockTransport::always(Reply::illegal_request());
|
||||||
let err = FreemkvUnlocker::new()
|
let err = FreemkvUnlocker::new()
|
||||||
.set(&mut t, Feature::Ake, STATE_OFF)
|
.set(&mut t, Feature::Encryption, STATE_OFF)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(err, UnlockError::NotApplicable);
|
assert_eq!(err, UnlockError::NotApplicable);
|
||||||
}
|
}
|
||||||
@@ -465,19 +455,18 @@ mod tests {
|
|||||||
|
|
||||||
// ── full_unlock / unlock ─────────────────────────────────────────────
|
// ── full_unlock / unlock ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// The full unlock runs IDENTITY → Set(Region) → Set(Speed) → Set(Ake) →
|
/// The full unlock runs IDENTITY → Set(Region) → Set(Speed) →
|
||||||
/// bare VID → Set(Bus) in order.
|
/// Set(Encryption) → bare VID in order.
|
||||||
#[test]
|
#[test]
|
||||||
fn full_unlock_issues_identity_region_speed_ake_then_bare_vid_then_bus() {
|
fn full_unlock_issues_identity_region_speed_encryption_then_bare_vid() {
|
||||||
let vid = [0x7Cu8; 16];
|
let vid = [0x7Cu8; 16];
|
||||||
let mut t = MockTransport::scripted(
|
let mut t = MockTransport::scripted(
|
||||||
vec![
|
vec![
|
||||||
Reply::good(freemkv_identity_payload()), // IDENTITY
|
Reply::good(freemkv_identity_payload()), // IDENTITY
|
||||||
Reply::good(vec![]), // Set Region
|
Reply::good(vec![]), // Set Region
|
||||||
Reply::good(vec![]), // Set Speed
|
Reply::good(vec![]), // Set Speed
|
||||||
Reply::good(vec![]), // Set Ake (load-bearing)
|
Reply::good(vec![]), // Set Encryption (load-bearing)
|
||||||
Reply::good(vid_ds_response(vid)), // 0xAD bare VID
|
Reply::good(vid_ds_response(vid)), // 0xAD bare VID
|
||||||
Reply::good(vec![]), // Set Bus (trailing)
|
|
||||||
],
|
],
|
||||||
Reply::TransportFault,
|
Reply::TransportFault,
|
||||||
);
|
);
|
||||||
@@ -485,16 +474,15 @@ mod tests {
|
|||||||
let out = FreemkvUnlocker::new()
|
let out = FreemkvUnlocker::new()
|
||||||
.unlock(&mut t, &ctx(&id))
|
.unlock(&mut t, &ctx(&id))
|
||||||
.expect("no fault")
|
.expect("no fault")
|
||||||
.expect("null AKE ⇒ unlocked");
|
.expect("encryption off ⇒ unlocked");
|
||||||
assert_eq!(out.vid, Some(vid));
|
assert_eq!(out.vid, Some(vid));
|
||||||
assert_eq!(t.cdbs.len(), 6);
|
assert_eq!(t.cdbs.len(), 5);
|
||||||
// The four vendor SETs land on the right features, in order.
|
// The three vendor SETs land on the right features, in order.
|
||||||
assert_eq!(t.cdbs[0], build_identity_cdb(RESP_LEN as u16));
|
assert_eq!(t.cdbs[0], build_identity_cdb(RESP_LEN as u16));
|
||||||
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Region, REGION_FREE));
|
assert_eq!(t.cdbs[1], build_set_cdb(Feature::Region, REGION_FREE));
|
||||||
assert_eq!(t.cdbs[2], build_set_cdb(Feature::Speed, SPEED_MAX));
|
assert_eq!(t.cdbs[2], build_set_cdb(Feature::Speed, SPEED_MAX));
|
||||||
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Ake, STATE_OFF));
|
assert_eq!(t.cdbs[3], build_set_cdb(Feature::Encryption, STATE_OFF));
|
||||||
assert_eq!(t.cdbs[4][0], crate::scsi::SCSI_READ_DISC_STRUCTURE);
|
assert_eq!(t.cdbs[4][0], crate::scsi::SCSI_READ_DISC_STRUCTURE);
|
||||||
assert_eq!(t.cdbs[5], build_set_cdb(Feature::Bus, STATE_OFF));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A missing region/speed feature does not fail the unlock (best-effort).
|
/// A missing region/speed feature does not fail the unlock (best-effort).
|
||||||
@@ -519,17 +507,17 @@ mod tests {
|
|||||||
assert_eq!(out.vid, Some(vid));
|
assert_eq!(out.vid, Some(vid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The null AKE is LOAD-BEARING: if the drive rejects it, this isn't an
|
/// Set(Encryption, off) is LOAD-BEARING: if the drive rejects it, this
|
||||||
/// unlockable freemkv drive, so `unlock()` declines (`None`) and falls
|
/// isn't an unlockable freemkv drive, so `unlock()` declines (`None`) and
|
||||||
/// through — never a hard error.
|
/// falls through — never a hard error.
|
||||||
#[test]
|
#[test]
|
||||||
fn declines_when_null_ake_rejected() {
|
fn declines_when_disable_encryption_rejected() {
|
||||||
let mut t = MockTransport::scripted(
|
let mut t = MockTransport::scripted(
|
||||||
vec![
|
vec![
|
||||||
Reply::good(freemkv_identity_payload()), // identity
|
Reply::good(freemkv_identity_payload()), // identity
|
||||||
Reply::good(vec![]), // region
|
Reply::good(vec![]), // region
|
||||||
Reply::good(vec![]), // speed
|
Reply::good(vec![]), // speed
|
||||||
Reply::illegal_request(), // null ake REJECTED
|
Reply::illegal_request(), // Set(Encryption,off) REJECTED
|
||||||
],
|
],
|
||||||
Reply::TransportFault,
|
Reply::TransportFault,
|
||||||
);
|
);
|
||||||
@@ -542,8 +530,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort VID: the null AKE succeeded (drive unlocked), but the bare
|
/// Best-effort VID: Set(Encryption,off) succeeded (drive unlocked), but the
|
||||||
/// VID read was rejected — `unlock()` still returns `Some`, with `vid: None`.
|
/// bare VID read was rejected — `unlock()` still returns `Some`, with
|
||||||
|
/// `vid: None`.
|
||||||
#[test]
|
#[test]
|
||||||
fn unlocks_without_vid_when_bare_read_fails() {
|
fn unlocks_without_vid_when_bare_read_fails() {
|
||||||
let mut t = MockTransport::scripted(
|
let mut t = MockTransport::scripted(
|
||||||
@@ -551,7 +540,7 @@ mod tests {
|
|||||||
Reply::good(freemkv_identity_payload()), // identity
|
Reply::good(freemkv_identity_payload()), // identity
|
||||||
Reply::good(vec![]), // region
|
Reply::good(vec![]), // region
|
||||||
Reply::good(vec![]), // speed
|
Reply::good(vec![]), // speed
|
||||||
Reply::good(vec![]), // null ake
|
Reply::good(vec![]), // Set(Encryption,off)
|
||||||
Reply::illegal_request(), // bare VID: no medium
|
Reply::illegal_request(), // bare VID: no medium
|
||||||
],
|
],
|
||||||
Reply::TransportFault,
|
Reply::TransportFault,
|
||||||
|
|||||||
Reference in New Issue
Block a user