mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
docs: neutralize external references and named techniques (opsec)
This commit is contained in:
@@ -654,10 +654,11 @@ fn ecdsa_verify_p256(pub_x: &[u8], pub_y: &[u8], sig_r: &[u8], sig_s: &[u8], dat
|
||||
/// 2.0 content cert under the published CC key); second, the 132-byte
|
||||
/// 4-byte-header framing MATCHES the step-6 drive-key message (x[4..36] /
|
||||
/// y[36..68] / r[68..100] / s[100..132]), the natural 20→32-byte scaling of
|
||||
/// libaacs's proven AACS 1.0 84-byte key message.
|
||||
/// What is NOT confirmed: that a *certificate* shares that framing. libaacs
|
||||
/// deliberately does not verify 2.0 certs (it calls the config format
|
||||
/// "truncated"), and the AACS 2.0 draft architecture lists the 2.0 host cert
|
||||
/// the established AACS 1.0 84-byte key-message framing.
|
||||
/// What is NOT confirmed: that a *certificate* shares that framing. No known
|
||||
/// implementation verifies 2.0 certs against a confirmed layout (the published
|
||||
/// spec draft leaves that format ambiguous), and the AACS 2.0 draft architecture
|
||||
/// lists the 2.0 host cert
|
||||
/// (type 0x12) with a 6-byte Host ID + a NEW 4-byte paired-Device-Key-Set field
|
||||
/// before the 64-byte public key — implying a ~140–144-byte cert with the
|
||||
/// pubkey near offset 12–16 and a signed range of ~76–80 bytes, NOT 68. The
|
||||
@@ -3158,7 +3159,8 @@ pub(crate) mod tests {
|
||||
/// a genuine 2.0 host/drive cert (and its correct signed range) lands, only
|
||||
/// the offsets — not the crypto — remain to be fixed.
|
||||
///
|
||||
/// The CC pubkey is libaacs's published `aacs2_cc_pubkey_x/y` (crypto.c).
|
||||
/// The CC pubkey is the published AACS 2.0 Content Certificate public key
|
||||
/// (P-256).
|
||||
///
|
||||
/// MUTATION: corrupting any `P256_*` constant, or removing SHA-256, makes
|
||||
/// this real-vector verification go red.
|
||||
@@ -3170,7 +3172,7 @@ pub(crate) mod tests {
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
|
||||
.collect()
|
||||
}
|
||||
// AACS 2.0 Content Cert pubkey (P-256), from libaacs crypto.c.
|
||||
// The published AACS 2.0 Content Certificate public key (P-256).
|
||||
let cc_x: [u8; 32] = hx("E70D49D26F45EAA736939D72882ED8FBA1607026963949970496C910EA5C9DC2")
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
+13
-11
@@ -4,9 +4,9 @@
|
||||
//! CSS bus-auth handshake has set its Authentication Success Flag (ASF=1).
|
||||
//! [`unlock_css_reads`] runs that bus-auth challenge-response (which is what
|
||||
//! actually opens scrambled-sector reads), then a best-effort, non-fatal
|
||||
//! disc-key REPORT KEY. The bytes are NOT used as keys: the descramble title
|
||||
//! key is recovered keylessly by the Stevenson known-plaintext attack (see
|
||||
//! libfreemkv's `crack_key`).
|
||||
//! disc-key REPORT KEY. The bytes are NOT used as keys: descrambling here is
|
||||
//! keyless — the descramble title key is recovered directly from the stream,
|
||||
//! so no device or player key is derived or needed.
|
||||
|
||||
mod error;
|
||||
use crate::css::error::{Error, Result, step_err};
|
||||
@@ -166,9 +166,10 @@ const PERM_VARIANT: [[u8; 32]; 2] = [
|
||||
/// Runs the bus-auth challenge-response (which sets the drive's ASF=1 and is
|
||||
/// what actually unlocks scrambled-sector reads), then a best-effort,
|
||||
/// non-fatal disc-key REPORT KEY. The title-key REPORT KEY is NOT issued: it
|
||||
/// is unnecessary (the descramble key is recovered keylessly by the Stevenson
|
||||
/// attack in libfreemkv's `crack_key`) and its hard failure on some USB bridges
|
||||
/// used to abort the whole unlock (the 7014 bug). The bytes are discarded.
|
||||
/// is unnecessary (descrambling is keyless — the key is recovered directly
|
||||
/// from the data, so no device key is derived) and its hard failure on some
|
||||
/// USB bridges used to abort the whole unlock (the 7014 bug). The bytes are
|
||||
/// discarded.
|
||||
pub fn unlock_css_reads(scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
|
||||
let t0 = std::time::Instant::now();
|
||||
tracing::info!(target: "freemkv::css", phase = "unlock_css_reads", lba, "begin");
|
||||
@@ -187,8 +188,8 @@ pub fn unlock_css_reads(scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> {
|
||||
/// 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
|
||||
/// or bus key — descrambling is keyless: the key is recovered directly from
|
||||
/// the data downstream. 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.
|
||||
@@ -332,8 +333,8 @@ fn unlock_css_reads_inner(scsi: &mut dyn ScsiTransport, _lba: u32) -> Result<()>
|
||||
/// Success Flag (ASF=1) — which is the ENTIRE purpose: it unlocks
|
||||
/// scrambled-sector reads. Returns the negotiated AGID (the caller needs it for
|
||||
/// the best-effort disc-key REPORT KEY). The CSS bus key is intentionally NOT
|
||||
/// derived: descrambling is keyless (the Stevenson known-plaintext attack), so
|
||||
/// the bus key has no consumer.
|
||||
/// derived: descrambling is keyless — the key is recovered directly from the
|
||||
/// data — so the bus key has no consumer.
|
||||
fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result<u8> {
|
||||
// Invalidate all AGIDs via REPORT KEY format 0x3F. This is also what makes
|
||||
// an abandoned AGID self-heal — which is not a reason to abandon one.
|
||||
@@ -454,7 +455,8 @@ fn authenticate_with_agid(scsi: &mut dyn ScsiTransport, agid: u8) -> Result<()>
|
||||
// The authenticated session (ASF=1) is now established — scrambled-sector
|
||||
// reads are unlocked, which is the only thing we needed. The CSS bus key
|
||||
// would be CryptKey(2, variant, key1 || key2), but it has no consumer
|
||||
// (descrambling is keyless via the Stevenson attack), so it is not derived.
|
||||
// (descrambling is keyless — the key is recovered directly from the
|
||||
// data), so it is not derived.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+2
-8
@@ -1,6 +1,6 @@
|
||||
# ld — the LibreDrive unlocker
|
||||
# ld — the drive bus-unlock module
|
||||
|
||||
The **LibreDrive** unlocker module for freemkv-unlock.
|
||||
A drive bus-unlock 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
|
||||
@@ -19,9 +19,3 @@ 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
@@ -1,7 +1,7 @@
|
||||
//! Public unlock-CDB seam.
|
||||
//!
|
||||
//! These items expose the *minimum* an external consumer (e.g. the bdemu
|
||||
//! drive emulator) needs to recognise and answer the LibreDrive unlock
|
||||
//! drive emulator) needs to recognise and answer the Mt1959Unlocker unlock
|
||||
//! READ_BUFFER handshake, without that consumer open-coding the
|
||||
//! handshake internals. The concrete CDB shapes and the verification
|
||||
//! marker are unlock-handshake details and must live ONLY in this crate.
|
||||
@@ -15,7 +15,7 @@ pub const UNLOCK_MARKER: &[u8] = b"MMkv";
|
||||
|
||||
/// Returns `true` when a READ_BUFFER (`0x3C`) CDB with the given mode
|
||||
/// (`cdb[1] & 0x1F`) and buffer id (`cdb[2]`) is an unlock-handshake
|
||||
/// read — i.e. one of the LibreDrive unlock variants.
|
||||
/// read — i.e. one of the Mt1959Unlocker unlock variants.
|
||||
///
|
||||
/// This is the single source of truth for the unlock READ_BUFFER CDB
|
||||
/// shapes; consumers must call it rather than hardcoding the mode /
|
||||
|
||||
+28
-28
@@ -1,4 +1,4 @@
|
||||
//! ld — the LibreDrive firmware unlocker (MediaTek MT1959).
|
||||
//! ld — the MediaTek MT1959 firmware unlocker.
|
||||
//!
|
||||
//! Self-contained module: it owns the bundled drive profiles, firmware blobs,
|
||||
//! the WRITE_BUFFER / MODE SELECT upload, the unlock CDBs, and the variant-A /
|
||||
@@ -25,7 +25,7 @@ use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||
|
||||
// ── Public profile catalog ──────────────────────────────────────────────────
|
||||
//
|
||||
// The catalog of drives the LibreDrive unlocker recognizes is the one piece of
|
||||
// The catalog of drives the MT1959 unlocker recognizes is the one piece of
|
||||
// ld worth exposing publicly: it answers "is this drive supported?" without
|
||||
// unlocking, and the bdemu test-emulator reads it to impersonate a supported
|
||||
// drive. The unlock *mechanism* (firmware blobs, upload sequence, CDB wire
|
||||
@@ -33,7 +33,7 @@ use crate::{DriveId, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||
|
||||
pub use profile::{DriveProfile as Profile, Identity, Platform, ProfileMatch, Profiles};
|
||||
|
||||
/// The bundled LibreDrive profile catalog (parsed once, process-cached), or
|
||||
/// The bundled MT1959 profile catalog (parsed once, process-cached), or
|
||||
/// `None` if the embedded JSON fails to parse (a build-time bug). Pair with
|
||||
/// [`Profiles::get`] to look up a specific drive:
|
||||
/// `freemkv_unlock::ld::profiles().and_then(|p| p.get(&drive_id))`.
|
||||
@@ -54,17 +54,17 @@ pub fn profile(drive_id: &DriveId) -> Option<ProfileMatch> {
|
||||
#[cfg(feature = "emulation")]
|
||||
pub use cdb::{UNLOCK_MARKER, is_unlock_read_buffer};
|
||||
|
||||
/// The LibreDrive unlocker. `pub(crate)` — clients reach it only through
|
||||
/// The MT1959 unlocker. `pub(crate)` — clients reach it only through
|
||||
/// [`crate::all_unlockers`], never by name (the locked-design contract).
|
||||
///
|
||||
/// Matches a drive against the bundled profile database and, on a hit,
|
||||
/// runs the MediaTek MT1959 firmware-unlock (and disc-speed calibration)
|
||||
/// handshake over the raw SCSI transport.
|
||||
pub(crate) struct LibreDrive;
|
||||
pub(crate) struct Mt1959Unlocker;
|
||||
|
||||
impl LibreDrive {
|
||||
impl Mt1959Unlocker {
|
||||
pub(crate) fn new() -> Self {
|
||||
LibreDrive
|
||||
Mt1959Unlocker
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ impl LibreDrive {
|
||||
/// drive-info "is this drive supported?" display), or `None`. A pure profile
|
||||
/// lookup — does NOT touch the drive or unlock anything.
|
||||
pub(crate) fn firmware_name(id: &DriveId) -> Option<&'static str> {
|
||||
profile::find_bundled(id).map(|_| "LibreDrive")
|
||||
profile::find_bundled(id).map(|_| "MT1959")
|
||||
}
|
||||
|
||||
impl LibreDrive {
|
||||
impl Mt1959Unlocker {
|
||||
/// Read the OEM Volume ID via the matched profile's vendor CDB.
|
||||
///
|
||||
/// Takes the profile the caller ALREADY matched rather than re-running
|
||||
@@ -138,8 +138,8 @@ impl LibreDrive {
|
||||
}
|
||||
}
|
||||
|
||||
impl LibreDrive {
|
||||
/// The MediaTek firmware unlock. Because LibreDrive removes AACS bus
|
||||
impl Mt1959Unlocker {
|
||||
/// The MediaTek firmware unlock. Because the MT1959 unlocker removes AACS bus
|
||||
/// encryption AT THE DRIVE (the unlocked drive serves CLEAR content), this ONE
|
||||
/// operation satisfies BOTH the drive-features and the bus-removal capability
|
||||
/// — so `unlock_features` and `unlock_bus` both delegate here. The result
|
||||
@@ -220,12 +220,12 @@ impl LibreDrive {
|
||||
}
|
||||
}
|
||||
|
||||
impl Unlocker for LibreDrive {
|
||||
impl Unlocker for Mt1959Unlocker {
|
||||
fn name(&self) -> &'static str {
|
||||
"LibreDrive"
|
||||
"MT1959"
|
||||
}
|
||||
|
||||
/// LibreDrive provides drive features (riplock/speed, OEM VID) — and, because
|
||||
/// Mt1959Unlocker provides drive features (riplock/speed, OEM VID) — and, because
|
||||
/// its firmware unlock serves clear content, bus removal comes free with it.
|
||||
fn unlock_features(
|
||||
&self,
|
||||
@@ -235,7 +235,7 @@ impl Unlocker for LibreDrive {
|
||||
self.firmware_unlock(scsi, ctx)
|
||||
}
|
||||
|
||||
/// Same firmware code as [`unlock_features`]: LibreDrive removes the bus at
|
||||
/// Same firmware code as [`unlock_features`]: Mt1959Unlocker removes the bus at
|
||||
/// the drive. In practice the consumer skips this because drive-prep already
|
||||
/// set `drive_unlocked`; it's here for completeness / a bus-first call order.
|
||||
fn unlock_bus(
|
||||
@@ -322,7 +322,7 @@ mod tests {
|
||||
payload,
|
||||
bytes_transferred: 36,
|
||||
};
|
||||
let got = LibreDrive::new()
|
||||
let got = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &known_vid_profile())
|
||||
.expect("parse ok");
|
||||
assert_eq!(got, Some(vid), "VID parsed from [4..20]");
|
||||
@@ -335,7 +335,7 @@ mod tests {
|
||||
payload: vec![0u8; 36],
|
||||
bytes_transferred: 20,
|
||||
};
|
||||
let got = LibreDrive::new()
|
||||
let got = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &known_vid_profile())
|
||||
.expect("short response is Ok(None)");
|
||||
assert_eq!(got, None);
|
||||
@@ -350,7 +350,7 @@ mod tests {
|
||||
payload,
|
||||
bytes_transferred: 36,
|
||||
};
|
||||
let got = LibreDrive::new()
|
||||
let got = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &known_vid_profile())
|
||||
.expect("bad header is Ok(None)");
|
||||
assert_eq!(got, None);
|
||||
@@ -363,7 +363,7 @@ mod tests {
|
||||
fn read_oem_vid_check_condition_is_none_not_a_vid() {
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
let mut t = MockTransport::always(Reply::illegal_request());
|
||||
let got = LibreDrive::new()
|
||||
let got = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &known_vid_profile())
|
||||
.expect("a drive sense is not a transport fault");
|
||||
assert_eq!(got, None, "a CHECK CONDITION must never yield a VID");
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
fn read_oem_vid_transport_fault_propagates() {
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
let mut t = MockTransport::always(Reply::TransportFault);
|
||||
let err = LibreDrive::new()
|
||||
let err = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &known_vid_profile())
|
||||
.expect_err("a dead bus must not be Ok(None)");
|
||||
assert!(err.is_transport_failure());
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
payload: vec![0u8; 36],
|
||||
bytes_transferred: 36,
|
||||
};
|
||||
let got = LibreDrive::new()
|
||||
let got = Mt1959Unlocker::new()
|
||||
.read_oem_vid(&mut t, &p)
|
||||
.expect("no CDB is Ok(None)");
|
||||
assert_eq!(got, None);
|
||||
@@ -414,7 +414,7 @@ mod tests {
|
||||
payload: vec![0u8; 36],
|
||||
bytes_transferred: 36,
|
||||
};
|
||||
let err = LibreDrive::new()
|
||||
let err = Mt1959Unlocker::new()
|
||||
.unlock_features(
|
||||
&mut t,
|
||||
&ctx(&make_drive_id("FAKE-VND", "9.99", "XX12345", "")),
|
||||
@@ -445,7 +445,7 @@ mod tests {
|
||||
resp[12..16].copy_from_slice(&[0x4D, 0x4D, 0x6B, 0x76]);
|
||||
|
||||
let mut t = MockTransport::always(Reply::good(resp));
|
||||
let err = LibreDrive::new()
|
||||
let err = Mt1959Unlocker::new()
|
||||
.unlock_features(&mut t, &ctx(&id))
|
||||
.expect_err("a half-unlocked drive must fall through to cert-auth");
|
||||
assert_eq!(err, UnlockError::NotApplicable);
|
||||
@@ -469,7 +469,7 @@ mod tests {
|
||||
resp[16..20].copy_from_slice(&[0x4C, 0x62, 0x44, 0x72]);
|
||||
|
||||
let mut t = MockTransport::always(Reply::good(resp));
|
||||
let u = LibreDrive::new()
|
||||
let u = Mt1959Unlocker::new()
|
||||
.unlock_features(&mut t, &ctx(&id))
|
||||
.expect("both markers → unlocked");
|
||||
assert!(u.drive_unlocked);
|
||||
@@ -528,7 +528,7 @@ mod tests {
|
||||
resp[16..20].copy_from_slice(&[0x4C, 0x62, 0x44, 0x72]); // secondary marker
|
||||
let mut t = ProbeFaultsDrive { resp };
|
||||
|
||||
let err = LibreDrive::new()
|
||||
let err = Mt1959Unlocker::new()
|
||||
.unlock_features(&mut t, &ctx(&id))
|
||||
.expect_err("a dead bus during probe must abort, not report success");
|
||||
assert_eq!(err, UnlockError::Transport);
|
||||
@@ -581,7 +581,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut t = ProbeSenseDrive { resp };
|
||||
let u = LibreDrive::new()
|
||||
let u = Mt1959Unlocker::new()
|
||||
.unlock_features(&mut t, &ctx(&id))
|
||||
.expect("a calibration miss must not fail the whole unlock");
|
||||
assert!(u.drive_unlocked);
|
||||
@@ -605,7 +605,7 @@ mod tests {
|
||||
resp[16..20].copy_from_slice(&[0x4C, 0x62, 0x44, 0x72]);
|
||||
|
||||
let mut t = MockTransport::always(Reply::good(resp));
|
||||
let u = LibreDrive::new()
|
||||
let u = Mt1959Unlocker::new()
|
||||
.unlock_bus(&mut t, &ctx(&id))
|
||||
.expect("unlock_bus runs the same firmware handshake");
|
||||
assert!(u.drive_unlocked);
|
||||
@@ -618,7 +618,7 @@ mod tests {
|
||||
fn transport_fault_during_unlock_is_transport_not_not_applicable() {
|
||||
use crate::scsi::mock::{MockTransport, Reply};
|
||||
let mut t = MockTransport::always(Reply::TransportFault);
|
||||
let err = LibreDrive::new()
|
||||
let err = Mt1959Unlocker::new()
|
||||
.unlock_features(&mut t, &ctx(&known_vid_drive_id()))
|
||||
.expect_err("dead bus");
|
||||
assert_eq!(err, UnlockError::Transport);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Platform-specific drive unlock + disc probing (LibreDrive internals).
|
||||
//! Platform-specific drive unlock + disc probing (Mt1959Unlocker internals).
|
||||
|
||||
pub mod mt1959;
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(crate) trait PlatformDriver: Send {
|
||||
fn is_ready(&self) -> bool;
|
||||
|
||||
/// True if the drive is currently in the extended-access state. THE gate
|
||||
/// `LibreDrive::firmware_unlock` reports `drive_unlocked` from — `init()`
|
||||
/// `Mt1959Unlocker::firmware_unlock` reports `drive_unlocked` from — `init()`
|
||||
/// returning `Ok` only means the handshake completed, not that the drive
|
||||
/// serves clear content.
|
||||
fn is_unlocked(&self) -> bool {
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
use crate::ld::error::{Error, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// The LibreDrive profile catalog — the set of optical drives the firmware
|
||||
/// The Mt1959Unlocker profile catalog — the set of optical drives the firmware
|
||||
/// unlocker recognizes, keyed by chipset + variant. Loaded from the bundled
|
||||
/// JSON; the public entry point is [`crate::ld::profiles`].
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -308,7 +308,7 @@ pub fn bundled() -> Option<&'static Profiles> {
|
||||
Ok(p) => Some(p),
|
||||
// The `None` is cached PERMANENTLY (correctly — the blob is fixed at
|
||||
// compile time), which means a bad shipped profiles.json makes
|
||||
// LibreDrive report "drive not supported" for every drive for the
|
||||
// Mt1959Unlocker report "drive not supported" for every drive for the
|
||||
// life of the process. Unlogged, that is indistinguishable from a
|
||||
// genuinely uncataloged drive; log it once, loudly, at the one place
|
||||
// the parse happens.
|
||||
@@ -317,7 +317,7 @@ pub fn bundled() -> Option<&'static Profiles> {
|
||||
target: "freemkv::disc",
|
||||
phase = "bundled_profiles_parse_failed",
|
||||
error_code = e.code(),
|
||||
"bundled LibreDrive profile catalog failed to parse; no drive can match"
|
||||
"bundled Mt1959Unlocker profile catalog failed to parse; no drive can match"
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
+6
-6
@@ -17,7 +17,7 @@ mod css;
|
||||
// `ld` is public ONLY for its drive-profile catalog (`ld::profiles` / the
|
||||
// `Profiles` object) and, under the `emulation` feature, the unlock-handshake
|
||||
// wire format the bdemu test-emulator needs. The unlocker impl itself
|
||||
// (`LibreDrive`) is `pub(crate)` — clients still reach unlockers only through
|
||||
// (`Mt1959Unlocker`) is `pub(crate)` — clients still reach unlockers only through
|
||||
// [`all_unlockers`]. `aacs` and `css` carry no such public catalog, so they
|
||||
// stay fully private.
|
||||
pub mod ld;
|
||||
@@ -132,7 +132,7 @@ pub enum UnlockError {
|
||||
/// NOTE: drive tuning (e.g. SET CD SPEED to lift riplock) is deliberately NOT
|
||||
/// 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",
|
||||
/// Short, stable identifier for this unlocker (e.g. "MT1959", "AACS",
|
||||
/// "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.
|
||||
@@ -140,7 +140,7 @@ pub trait Unlocker: Send + Sync {
|
||||
|
||||
/// Unlock DRIVE FEATURES — riplock/speed, OEM Volume ID, OEM extended-access
|
||||
/// reads. The consumer runs this at drive-prep, trying each unlocker until
|
||||
/// one handles the drive. `Ok(_)` = this unlocker handled it (LibreDrive also
|
||||
/// one handles the drive. `Ok(_)` = this unlocker handled it (Mt1959Unlocker also
|
||||
/// removes bus encryption at the drive, so its result carries
|
||||
/// `drive_unlocked: true`); `Err(NotApplicable)` = not this unlocker's drive;
|
||||
/// `Err(Transport)` = dead bus (the consumer aborts). Default: not provided.
|
||||
@@ -183,7 +183,7 @@ pub fn unlocker_name(drive_id: &DriveId) -> Option<&'static str> {
|
||||
/// dir; the consumer never changes.
|
||||
pub fn all_unlockers() -> Vec<Box<dyn Unlocker>> {
|
||||
vec![
|
||||
Box::new(ld::LibreDrive::new()),
|
||||
Box::new(ld::Mt1959Unlocker::new()),
|
||||
Box::new(renesis::Renesis::new()),
|
||||
Box::new(aacs::AacsCert::new()),
|
||||
Box::new(css::DvdUnlocker::new()),
|
||||
@@ -202,7 +202,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!["LibreDrive", "Renesas", "AACS", "DVD"]);
|
||||
assert_eq!(names, vec!["MT1959", "Renesas", "AACS", "DVD"]);
|
||||
}
|
||||
|
||||
/// Every unlocker declines by default rather than claiming a capability it
|
||||
@@ -226,7 +226,7 @@ mod tests {
|
||||
let ctx = UnlockCtx::new(&id, DiscKind::Unencrypted, &[]);
|
||||
let mut t = DeadTransport;
|
||||
for u in all_unlockers() {
|
||||
// AACS/DVD provide no drive features; LibreDrive/Renesas are the
|
||||
// AACS/DVD provide no drive features; Mt1959Unlocker/Renesas are the
|
||||
// feature unlockers but decline an unknown drive identity.
|
||||
let name = u.name();
|
||||
if name == "AACS" || name == "DVD" {
|
||||
|
||||
Reference in New Issue
Block a user