docs: neutralize external references and named techniques (opsec)

This commit is contained in:
Matthew Jackson
2026-08-25 22:18:31 -07:00
parent 9ad1eb9811
commit 7128fdf71f
8 changed files with 64 additions and 66 deletions
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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" {