mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
f67582348881e22eaa330e623e7920ea40ad6dc1
Two failure CLASSES ran through this crate, and both had the same shape: an outcome that means "stop" was recorded and then not acted on. 1. TRANSPORT-FAULT MISCLASSIFICATION. `ScsiTransport::execute` is the load-bearing contract here: `Ok` even on a SCSI sense (inspect `status`), `Err` ONLY on a transport-layer fault. That split is the abort/fall-through boundary — a dead bus must abort the consumer, everything else may fall through to the next unlocker. Almost every call site got one half of it wrong. Discarding the `Err`: `Err(_) => false` in the Renesas identity probe (the FIRST command the unlocker issues, so a dead bus read as "not a Renesas drive"); `_ => false` with no `Err` arm at all in the DVD profile probe; `.map_err(|_| CssAuthFailed)` at every CSS bus-auth step, which made the `Transport` arm of css's own error conversion unreachable from any real call site; `Err(_)` in the MT1959 init retry loop, which re-uploaded firmware into a dead bus three times and then returned a generic `UnlockFailed` that classifies as "not applicable"; `continue` in the AACS cert loop, which ran three certs with a 1 s backoff against a dead bus and reported HandshakeRejected — a credentials problem — for what is a replug; and an unconditional `VidUnavailable` after a successful authentication. Ignoring the `Ok`: the AACS handshake's two SCSI helpers inspected neither `status` nor `bytes_transferred`, so a CHECK CONDITION — which per the contract arrives as `Ok` — handed each step back the caller's own ZERO-FILLED buffer to parse as drive data. That is latent only because the single transport implementor violates the contract in the opposite direction; a conforming transport would have turned the whole handshake into a zero-buffer success. Same hole in the MT1959 unlock and validate reads, the OEM Volume ID read, and the CSS steps. And once, the inverse: a merely SHORT probe response was fabricated into a transport-failure status, hard-aborting a rip that would have succeeded. 2. A COMPUTED-BUT-UNREAD UNLOCK FLAG. `Mt1959::do_unlock` computes `unlocked` from three conditions, with a comment explaining that requiring all of them "keeps any partial / corrupted response from steering us off the cert-auth fallback". Nothing outside the tests ever read it. `firmware_unlock` set `drive_unlocked: true` whenever `init()` returned `Ok` — and `init()` returning `Ok` only means the handshake completed. A half-unlocked drive was reported to the consumer as serving clear content, which suppressed the cert-auth fallback. Worse, each of `do_unlock`'s three checks was individually guarded by its own length test, so a drive delivering ZERO bytes skipped all three and returned `Ok` on a 64-byte buffer it had never written: a fully unlocked drive, out of nothing. Also fixed: `read_data_keys` returned AES-decrypted garbage as `Ok((key, key))` from an all-zero response with nothing to authenticate it; the probe's two passes walked the same 88 addresses twice per disc; a bad bundled profiles.json cached `None` forever unlogged, making every drive look unsupported; `DriveProfile` re-exported the MediaTek firmware blob and the vendor CDB templates to any downstream crate; firmware verify reads were discarded with `let _ =` despite being called verify steps; AGIDs leaked on error paths; the VID MAC compare short-circuited; `Copyright Information` labelled the Disc Key block; and two intra-doc links pointed at items in another crate. THE TEST GAP THAT HID ALL OF IT. No mock in this crate could return `Err`, a non-zero status, or a short transfer, and nothing drove the AACS or CSS handshakes through a transport at all — `aacs/handshake.rs`'s 41 tests were pure math. One test actively PINNED the bug (it fed a transport `Err` and asserted NotApplicable). A crate-wide mock transport that expresses all three contract outcomes now lives in `scsi.rs`; it is the red-before-green vehicle for every fix above. `lib.rs`, `scsi.rs` and `ld/error.rs` had zero tests and now cover the dispatch order, the sense layout and the abort/fall-through boundary itself; `ld/cdb.rs`'s tests compiled only under a non-default feature and now run in CI. Behaviour changes worth knowing: a half-unlocked drive now falls through to cert-auth instead of claiming the bus is clear; a dead bus now aborts from the Renesas probe, the DVD probe, CSS bus-auth, MT1959 init and the AACS cert loop instead of falling through; and drive prep issues 88 fewer SCSI commands per disc.
freemkv-unlock
The unlock layer for the freemkv toolchain.
An unlocker removes a drive-level bus-encryption barrier so the drive serves readable (de-bus'd / de-scrambled) sectors. Content-key decryption is a separate concern — the consumer's job.
This crate defines the Unlocker contract and a generic SCSI transport
contract, and holds the self-contained unlocker modules. The consumer
(libfreemkv) depends on this crate and
dispatches through all_unlockers(); it never names an individual unlocker, and
clients of libfreemkv are oblivious to unlockers entirely (as they are to the
SCSI layer).
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() {
match u.unlock_features(&mut scsi, &ctx) {
Ok(unlocked) => return Ok(unlocked),
Err(UnlockError::NotApplicable) => continue,
Err(e) => return Err(e),
}
}
To remove an unlocker, delete its module directory and its one line in
all_unlockers() — nothing else changes.
License: MIT.
Languages
Rust
100%