libfreemkv 1.7.4: single drive-owned AACS bus-removal point

Relocate AACS bus decryption to one spot at the bottom of the sector chain:
the Drive's SectorSource impl applies decrypt_bus_in_content (gated to
encrypted-content ranges), with the stage decided once from the unlock/
handshake result (BusStage::from_read_data_key). Remove read_data_key from
DecryptKeys::Aacs and AacsState and the two downstream decrypt_bus call sites;
every reader (key sampler, mux, unit-key validation) now sees already-de-bussed
content and never reasons about bus encryption. Add BusRemovalSectorSource
stream + tests; keep the AacsBusKeyUnavailable fail-loud gate.
This commit is contained in:
Matthew Jackson
2026-09-19 13:59:52 -07:00
parent 5696175520
commit 1894fa1a49
17 changed files with 719 additions and 158 deletions
+35
View File
@@ -280,6 +280,41 @@ pub fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) {
}
}
/// Remove bus encryption across a sector-aligned `buf`, gated to the disc's
/// encrypted-content ranges. `base_lba` is the LBA of `buf`'s first 2048-byte
/// sector (sector `i` = `base_lba + i`). A sector is de-bussed only when `ranges`
/// is `None` (content-only caller) or its LBA falls inside a sorted
/// `(start_lba, sector_count)` range — a clear UDF/nav sector outside content is
/// left untouched (de-bussing it would corrupt plaintext). Key schedule expanded
/// once. Single de-bus entry point (see the `sector::bus_removal` module doc).
pub(crate) fn decrypt_bus_in_content(
buf: &mut [u8],
read_data_key: &[u8; 16],
base_lba: u32,
ranges: Option<&[(u32, u32)]>,
) {
let in_content = |lba: u32| -> bool {
match ranges {
None => true,
Some(rs) => rs
.iter()
.any(|&(start, cnt)| lba >= start && (lba as u64) < start as u64 + cnt as u64),
}
};
let cipher = crate::aacs::crypto::new_cipher_for(read_data_key);
for (i, sector) in buf.chunks_mut(SECTOR_BYTES).enumerate() {
if sector.len() < SECTOR_BYTES {
break; // trailing partial sector (never on a whole-sector read)
}
let lba = base_lba.saturating_add(i as u32);
if !in_content(lba) {
continue;
}
// First 16 bytes of each sector are plaintext on the wire.
crate::aacs::crypto::cbc_decrypt_blocks(&cipher, &mut sector[16..SECTOR_BYTES]);
}
}
/// Test-only exact inverse of [`decrypt_bus`]: apply AACS 2.0 bus ENCRYPTION to
/// an aligned unit in place — per-sector AES-CBC-encrypt bytes 16..2048 of each
/// 2048-byte sector under `read_data_key`, leaving the first 16 bytes of every
-1
View File
@@ -1308,7 +1308,6 @@ mod tests {
}];
let mut keys = crate::decrypt::DecryptKeys::Aacs {
unit_keys: vec![(0, [0u8; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::MpegPs,
};
resolve_dvd_title_key(
+81 -56
View File
@@ -116,14 +116,15 @@ pub fn decrypt_threads() -> usize {
pub enum DecryptKeys {
/// No encryption on this disc.
None,
/// AACS (Blu-ray / UHD / HD-DVD). Unit keys + optional read data key. The
/// `format` is the disc's content container (BD/UHD/FMTS = Transport Stream,
/// HD-DVD `.evo` = Program Stream); it travels with the keys because both are
/// resolved once per disc, and the key SELECTOR (`is_clean`) needs it to prove
/// a key structurally against the right container.
/// AACS (Blu-ray / UHD / HD-DVD). Unit keys only — bus encryption is ALREADY
/// removed upstream by the drive's single de-bus point
/// ([`crate::sector::bus_removal::BusStage`]), so no read data key is threaded
/// here. The `format` is the disc's content container (BD/UHD/FMTS = Transport
/// Stream, HD-DVD `.evo` = Program Stream); it travels with the keys because
/// both are resolved once per disc, and the key SELECTOR (`is_clean`) needs it
/// to prove a key structurally against the right container.
Aacs {
unit_keys: Vec<(u32, [u8; 16])>,
read_data_key: Option<[u8; 16]>,
format: crate::disc::ContentFormat,
},
/// CSS (DVD). Title key for sector descrambling.
@@ -381,12 +382,8 @@ fn apply_aacs_map(
map: &AacsKeyMap,
content: Option<&[(u32, u32)]>,
) -> Result<(), crate::error::Error> {
let (unit_keys, rdk, format) = match keys {
DecryptKeys::Aacs {
unit_keys,
read_data_key,
format,
} => (unit_keys, *read_data_key, *format),
let (unit_keys, format) = match keys {
DecryptKeys::Aacs { unit_keys, format } => (unit_keys, *format),
// Clear / CSS: the mapped path is AACS-only. Leave the buffer untouched;
// CSS descrambles via `decrypt_sectors` and `None` is already clear.
_ => return Ok(()),
@@ -451,11 +448,10 @@ fn apply_aacs_map(
if !aacs::content::aacs_unit_encrypted(chunk, format) {
return;
}
// Bounds already proven above; index directly.
// Bounds already proven above; index directly. Bus encryption was already
// removed by the drive's single de-bus point before this buffer arrived —
// this path only applies the CPS unit key.
let key = &unit_keys[key_idx].1;
if let Some(ref rdk_key) = rdk {
aacs::content::decrypt_bus(chunk, rdk_key);
}
aacs::content::decrypt_unit(chunk, key);
// Correct-phase forensic verify (silent unless the map is wrong).
if matches!(phase, Phase::Even | Phase::Odd) && !aacs::content::is_clean(chunk, format) {
@@ -711,7 +707,6 @@ mod tests {
fn content_gate_aacs_keys_fail_loud_not_ok_zero() {
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(1, [0xAB; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
@@ -801,7 +796,6 @@ mod tests {
fn aacs_scrambled_trailing_partial_is_rejected() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
// One CLEAR leading unit (passes through) + a 4096-byte (two-sector) tail
@@ -828,7 +822,6 @@ mod tests {
fn aacs_clear_trailing_partial_passes_through() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
@@ -866,7 +859,6 @@ mod tests {
assert!(
DecryptKeys::Aacs {
unit_keys: vec![(0, [0; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
}
.is_encrypted()
@@ -1042,7 +1034,6 @@ mod tests {
fn aacs_mapped_out_of_range_key_idx_errors() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
@@ -1062,7 +1053,6 @@ mod tests {
fn aacs_mapped_empty_unit_keys_errors() {
let keys = DecryptKeys::Aacs {
unit_keys: vec![],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
@@ -1079,7 +1069,6 @@ mod tests {
fn aacs_via_unmapped_decrypt_sectors_fails_loud() {
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAB; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
@@ -1114,63 +1103,108 @@ mod tests {
unit
}
// AACS 2.0 / UHD "OEM route": a drive delivers bus_encrypt(encrypt_unit(clear))
// and the mapped decrypt must strip the bus layer with the read_data_key FIRST,
// then the unit-key layer — the `if let Some(rdk)` arm no other test reaches.
// A `SectorSource` that returns a fixed wire buffer for any read (the bytes a
// bus-encrypted drive would put on the transport), reporting the full span —
// the input to the drive-owned bus-removal stream in the ordering tests below.
struct WireSource {
bytes: Vec<u8>,
}
impl crate::sector::SectorSource for WireSource {
fn capacity_sectors(&self) -> u32 {
(self.bytes.len() / 2048) as u32
}
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> crate::error::Result<usize> {
let n = (count as usize * 2048).min(self.bytes.len());
buf[..n].copy_from_slice(&self.bytes[..n]);
Ok(n)
}
}
// AACS 2.0 bus-then-unit ordering, proven at the SINGLE de-bus point: the
// drive-owned bus-removal stream strips the bus layer AT READ TIME, then the
// downstream mapped decrypt strips the unit-key layer (no bus key threaded).
#[test]
fn mapped_decrypt_removes_bus_then_unit_encryption_end_to_end() {
fn bus_removal_then_mapped_unit_decrypt_recovers_plaintext_end_to_end() {
use crate::sector::SectorSource;
use crate::sector::bus_removal::{BusRemovalSectorSource, BusStage};
let unit_key = [0x5Au8; 16];
let rdk = [0x91u8; 16];
let mut clear = clear_ts_unit();
clear[0] |= 0xC0; // the encrypted flag lives in the clear seed
let mut buf = clear.clone();
// Inner layer: unit-key encryption. Outer layer: drive bus-encryption.
aacs_encrypt_unit_for_test(&mut buf, &unit_key);
aacs::content::encrypt_bus(&mut buf, &rdk);
// Inner layer: unit-key encryption. Outer layer: drive bus-encryption —
// exactly the bytes a bus-encrypted drive puts on the wire.
let mut wire = clear.clone();
aacs_encrypt_unit_for_test(&mut wire, &unit_key);
aacs::content::encrypt_bus(&mut wire, &rdk);
// Read the wire through the drive-owned bus-removal stream (host-key
// stage): the ONE place bus decryption is applied.
let mut src =
BusRemovalSectorSource::new(WireSource { bytes: wire }, BusStage::AacsHostKey(rdk));
let mut buf = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
let n = src.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(n, aacs::content::ALIGNED_UNIT_LEN);
// Bus removed, but the CPS unit-key layer is still in place — not yet clear.
assert!(
!aacs::content::is_clean(&buf, crate::disc::ContentFormat::BdTs),
"the delivered unit must still be scrambled before decrypt"
"bus removed at read, but the CPS unit-key layer must still be scrambled"
);
// The downstream mapped decrypt applies ONLY the unit key over the
// already-de-bussed content.
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: Some(rdk),
format: crate::disc::ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
decrypt_sectors_mapped(&mut buf, &keys, 0, &map)
.expect("bus + unit-key decrypt must succeed");
.expect("unit-key decrypt over de-bussed content must succeed");
assert_eq!(
buf, clear,
"read_data_key (bus) then unit-key must recover the plaintext exactly"
"bus removal (at read) then unit-key decrypt must recover the plaintext exactly"
);
}
// Necessity + ordering proof: the SAME delivered bytes with NO read_data_key
// run only the unit-key decrypt over still-bus-encrypted data and must NOT
// recover clear — so the rdk arm genuinely runs, and runs BEFORE the unit key.
// Necessity: the SAME wire bytes read through a PASSTHROUGH stage keep the bus
// layer, so unit-key decrypt alone runs over bus-encrypted data and cannot
// recover clear — the bus key is genuinely required, and de-bus precedes it.
#[test]
fn mapped_decrypt_without_read_data_key_cannot_clear_bus_encrypted_content() {
fn without_bus_removal_mapped_unit_decrypt_cannot_clear_bus_encrypted_content() {
use crate::sector::SectorSource;
use crate::sector::bus_removal::{BusRemovalSectorSource, BusStage};
let unit_key = [0x5Au8; 16];
let rdk = [0x91u8; 16];
let mut clear = clear_ts_unit();
clear[0] |= 0xC0;
let mut buf = clear.clone();
aacs_encrypt_unit_for_test(&mut buf, &unit_key);
aacs::content::encrypt_bus(&mut buf, &rdk);
let mut wire = clear.clone();
aacs_encrypt_unit_for_test(&mut wire, &unit_key);
aacs::content::encrypt_bus(&mut wire, &rdk);
let keys_no_rdk = DecryptKeys::Aacs {
// Passthrough: no bus key, so the wire is delivered still-bus-encrypted.
let mut src =
BusRemovalSectorSource::new(WireSource { bytes: wire }, BusStage::Passthrough);
let mut buf = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
src.read_sectors(0, 3, &mut buf, false).unwrap();
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
// Phase::All never runs the forensic verify, so this returns Ok either
// way; the point is the BYTES are not the plaintext without the bus key.
let _ = decrypt_sectors_mapped(&mut buf, &keys_no_rdk, 0, &map);
// way; the point is the BYTES are not the plaintext without bus removal.
let _ = decrypt_sectors_mapped(&mut buf, &keys, 0, &map);
assert_ne!(
buf, clear,
"without the read_data_key, unit-key decrypt alone cannot recover clear"
"without bus removal first, unit-key decrypt alone cannot recover clear"
);
}
@@ -1390,7 +1424,6 @@ mod tests {
// AACS, encrypted, no map installed at all.
let mut aacs_keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xAAu8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut buf = vec![0u8; ul];
@@ -1440,7 +1473,6 @@ mod tests {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: ContentFormat::BdTs,
};
// The map covers unit 0 only. Unit 1 is the orphan.
@@ -1500,7 +1532,6 @@ mod tests {
}
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges_phased(vec![(0, 8 * usz, 0, Phase::Even)]);
@@ -1535,7 +1566,6 @@ mod tests {
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]);
@@ -1579,7 +1609,6 @@ mod tests {
}
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges(vec![(0, (n as u32) * usz, 0)]);
@@ -1623,7 +1652,6 @@ mod tests {
let mut buf = build();
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b)],
read_data_key: None,
format: ContentFormat::BdTs,
};
decrypt_sectors_mapped(&mut buf, &keys, 0, &map)
@@ -1634,7 +1662,6 @@ mod tests {
let mut buf = build();
let swapped = DecryptKeys::Aacs {
unit_keys: vec![(1, key_b), (0, key_a)],
read_data_key: None,
format: ContentFormat::BdTs,
};
assert!(
@@ -1657,7 +1684,6 @@ mod tests {
buf[..ul].copy_from_slice(&u0);
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0xCCu8; 16])], // map slot points at the WRONG key
read_data_key: None,
format: ContentFormat::BdTs,
};
let map = AacsKeyMap::from_ranges_phased(vec![(0, 2 * usz, 0, Phase::Even)]);
@@ -1682,7 +1708,6 @@ mod tests {
}
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: ContentFormat::BdTs,
};
decrypt_sectors_mapped(
+6 -19
View File
@@ -372,7 +372,6 @@ impl Disc {
key_source: KeyOrigin::ExternalUk,
vuk: None,
unit_keys: vec![],
read_data_key: handshake.and_then(|h| h.read_data_key),
volume_id: handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]),
uk_ro: uk_ro_data,
mkb: mkb_bytes,
@@ -772,7 +771,8 @@ mod tests {
};
let st = Disc::resolve_vid_only(&udf, &mut disc, Some(&hs)).expect("state");
assert_eq!(st.volume_id, vid);
assert_eq!(st.read_data_key, Some(rdk));
// The bus key no longer propagates onto AacsState — it feeds the drive's
// single de-bus point via the handshake, not a decrypt-time field.
}
// OEM bus-key gate: a bus-encrypted disc on a LIVE drive with no
@@ -831,7 +831,6 @@ mod tests {
};
let st = Disc::resolve_vid_only(&udf, &mut disc, Some(&hs)).expect("bus key present → ok");
assert!(st.bus_encryption);
assert_eq!(st.read_data_key, Some([0x22u8; 16]));
}
/// Finding: `is_drive_unlocker(matched)` on the cert route was provably always
@@ -864,10 +863,6 @@ mod tests {
let st = Disc::resolve_vid_only(&udf, &mut disc, Some(&hs))
.expect("a drive-unlocked disc removes bus encryption even without a read_data_key");
assert!(st.bus_encryption);
assert_eq!(
st.read_data_key, None,
"the drive-unlock path carries no read_data_key"
);
}
/// ISO scan (handshake None) of a bus_encryption disc → Ok. Bus encryption
@@ -878,7 +873,6 @@ mod tests {
let (mut disc, udf) = disc_with_cert(0x01, true);
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("ISO bus disc → ok");
assert!(st.bus_encryption);
assert_eq!(st.read_data_key, None);
}
/// AACS 1.0 BD (V10 cert, bus_encryption off) on a live drive with NO
@@ -895,7 +889,6 @@ mod tests {
};
let st = Disc::resolve_vid_only(&udf, &mut disc, Some(&hs)).expect("AACS 1.0 → ok");
assert!(!st.bus_encryption);
assert_eq!(st.read_data_key, None);
}
/// With NO handshake, volume_id defaults to all-zero (encrypt.rs
@@ -914,7 +907,6 @@ mod tests {
);
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
assert_eq!(st.volume_id, [0u8; 16]);
assert_eq!(st.read_data_key, None);
}
/// `handshake_has_volume_id` treats an all-zero Volume ID as absent and
@@ -1291,11 +1283,10 @@ mod tests {
));
}
/// A handshake that carries a VID but `read_data_key: None` must SURFACE the
/// `None` onto the state — not silently drop it or synthesize a key. On a
/// non-bus-encrypted disc (so the bus-key gate does not fire) the VID still
/// propagates while `read_data_key` stays `None`. This is the AACS-2.0
/// "VID present, no RDK" signal the downstream gate keys off.
/// A handshake that carries a VID but `read_data_key: None` must still surface
/// the VID onto the state. On a non-bus-encrypted disc (so the bus-key gate
/// does not fire) the VID propagates; the bus key itself no longer lives on
/// AacsState — it feeds the drive's single de-bus point via the handshake.
#[test]
fn resolve_vid_only_surfaces_vid_with_absent_read_data_key() {
let (mut disc, udf) = disc_with_cert(0x00, false); // V10, bus off
@@ -1311,9 +1302,5 @@ mod tests {
st.volume_id, vid,
"the VID must propagate even without an RDK"
);
assert_eq!(
st.read_data_key, None,
"a handshake bus_key of None must surface as None, not be dropped or faked"
);
}
}
-1
View File
@@ -1226,7 +1226,6 @@ mod tests {
key_source: crate::disc::KeyOrigin::ExternalUk,
vuk: None,
unit_keys: vec![(0u32, [0u8; 16])],
read_data_key: None,
volume_id: [0u8; 16],
uk_ro: Vec::new(),
mkb: Vec::new(),
+24 -44
View File
@@ -1435,8 +1435,6 @@ pub struct AacsState {
pub vuk: Option<[u8; 16]>,
/// Decrypted unit keys (CPS unit number, key)
pub unit_keys: Vec<(u32, [u8; 16])>,
/// Read data key for AACS 2.0 bus decryption -- None for AACS 1.0
pub read_data_key: Option<[u8; 16]>,
/// Volume ID (16 bytes) -- from SCSI handshake
pub volume_id: [u8; 16],
/// Raw `Unit_Key_RO.inf` bytes (encrypted unit keys + CPS map). Stashed at
@@ -1461,7 +1459,6 @@ impl std::fmt::Debug for AacsState {
.field("key_source", &self.key_source)
.field("vuk", &self.vuk.map(|_| "<redacted>"))
.field("unit_keys_len", &self.unit_keys.len())
.field("read_data_key", &self.read_data_key.map(|_| "<redacted>"))
.field("volume_id", &"<redacted>")
.field("uk_ro_len", &self.uk_ro.len())
.field("mkb_len", &self.mkb.len())
@@ -1736,6 +1733,10 @@ impl Disc {
buffered.prefetch_ranges(&ranges);
}
// The AACS bus stage is decided ONCE from the handshake result, captured
// before `handshake` is moved into `scan_with` (see `BusStage`).
let bus_key = handshake.as_ref().and_then(|h| h.read_data_key);
tracing::info!(target: "freemkv::scan", "phase: parsing titles/streams");
let disc = Self::scan_with(
&mut buffered,
@@ -1747,6 +1748,17 @@ impl Disc {
)?;
tracing::info!(target: "freemkv::scan", titles = disc.titles.len(), format = ?disc.content_format, "phase: titles parsed");
// Wire the SINGLE de-bus point onto the drive from the handshake result,
// BEFORE the caller samples keys or muxes — gated to encrypted-content
// extents (the earlier UDF/metadata reads ran under default Passthrough).
session.set_bus_stage(crate::sector::bus_removal::BusStage::from_read_data_key(
bus_key,
));
let content_ranges = disc.encrypted_content_ranges();
if !content_ranges.is_empty() {
session.set_bus_content_ranges(std::sync::Arc::from(content_ranges.into_boxed_slice()));
}
// No CSS key recovery at scan time: DVD CSS keys are per-title and are
// re-cracked keylessly at read/decrypt time (`disc.css` stays unset),
// so log the format rather than a key state that is always `None`.
@@ -2499,13 +2511,10 @@ impl std::fmt::Debug for Key {
// CPS units. See docs/disc-mod.md for why "first sample decrypts" is unsafe.
fn aligned_unit_keys_validate(
unit_keys: &[(u32, [u8; 16])],
read_data_key: Option<&[u8; 16]>,
samples: &[Vec<u8>],
format: ContentFormat,
) -> bool {
use crate::aacs::content::{
ALIGNED_UNIT_LEN, aacs_unit_needs_decrypt, decrypt_bus, decrypt_unit, is_clean,
};
use crate::aacs::content::{ALIGNED_UNIT_LEN, aacs_unit_needs_decrypt, decrypt_unit, is_clean};
let scrambled: Vec<&[u8]> = samples
.iter()
.map(|s| s.as_slice())
@@ -2531,11 +2540,9 @@ fn aligned_unit_keys_validate(
hb.tick_cpu(tried, total);
tried += 1;
probe.copy_from_slice(&sample[..ALIGNED_UNIT_LEN]);
// bus layer (AACS 2.0) first, then the CPS unit key, then the structural
// proof — the composed form of the old `decrypt_unit_full`.
if let Some(rdk) = read_data_key {
decrypt_bus(&mut probe, rdk);
}
// Samples arrive ALREADY de-bussed (the drive's single de-bus point
// removed AACS 2.0 bus encryption at read time), so validation only
// applies the CPS unit key, then the structural proof.
decrypt_unit(&mut probe, k);
if is_clean(&probe, format) {
covered = true;
@@ -2577,7 +2584,6 @@ impl Disc {
}
crate::decrypt::DecryptKeys::Aacs {
unit_keys: aacs.unit_keys.clone(),
read_data_key: aacs.read_data_key,
format: self.content_format,
}
} else if let Some(ref css) = self.css {
@@ -2879,7 +2885,6 @@ impl Disc {
key_source: KeyOrigin::ExternalUk,
vuk: None,
unit_keys: keys,
read_data_key: None,
volume_id: [0u8; 16],
uk_ro: Vec::new(),
mkb: Vec::new(),
@@ -2927,14 +2932,9 @@ impl Disc {
/// rejected key (`Err(AacsKeyRejected)`) falls through to the caller's
/// next candidate. Pass `&[]` to skip validation (resume / mapfile cache).
pub fn decrypt_with(&mut self, key: Key, samples: &[Vec<u8>]) -> Result<()> {
// The AACS 2.x bus key, needed to de-scramble a sample for validation;
// captured before any mutable borrow. None for AACS 1.0 and file-backed
// ISO units (bus encryption was already removed at read time).
let read_data_key = self.aacs.as_ref().and_then(|a| a.read_data_key);
// Resolve the supplied key DOWN to candidate unit keys WITHOUT
// committing them, so a wrong higher-level key can be rejected before it
// poisons disc state.
// Samples arrive already de-bussed (drive's single bus-removal point), so
// validation needs no bus key. Resolve the key DOWN to candidate unit keys
// WITHOUT committing — a wrong higher key is rejected before it poisons state.
let (candidate_unit_keys, candidate_vuk) = if let Key::Unit(keys) = key {
// Terminal — the source / mapfile already holds the final UKs.
(keys, None)
@@ -3003,12 +3003,7 @@ impl Disc {
// VALIDATE against real ciphertext. Conservative: reject only when a
// supplied sample is AACS-scrambled and NO candidate key can de-scramble
// it; with no samples the key is accepted as-is (sample-less paths unchanged).
if !aligned_unit_keys_validate(
&candidate_unit_keys,
read_data_key.as_ref(),
samples,
self.content_format,
) {
if !aligned_unit_keys_validate(&candidate_unit_keys, samples, self.content_format) {
return Err(crate::error::Error::AacsKeyRejected);
}
@@ -3796,7 +3791,6 @@ mod tests {
key_source: KeyOrigin::ExternalUk,
vuk: Some([0xD5; 16]),
unit_keys: vec![(1, [0xD5; 16])],
read_data_key: Some([0xD5; 16]),
volume_id: [0xD5; 16],
uk_ro: vec![1, 2, 3],
mkb: vec![4, 5, 6],
@@ -5167,13 +5161,8 @@ mod tests {
disc.inject_unit_keys(uk.clone());
match disc.decrypt_keys() {
crate::decrypt::DecryptKeys::Aacs {
unit_keys,
read_data_key,
..
} => {
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => {
assert_eq!(unit_keys, uk, "injected UK must be the decrypt key");
assert_eq!(read_data_key, None, "ISO mux needs no bus key");
}
_ => panic!("expected Aacs decrypt keys after injecting a UK"),
}
@@ -5216,7 +5205,6 @@ mod tests {
key_source: KeyOrigin::DeviceKey,
vuk: None,
unit_keys,
read_data_key: None,
volume_id: [0u8; 16],
uk_ro: Vec::new(),
mkb: Vec::new(),
@@ -5847,7 +5835,6 @@ mod tests {
// like resume / mapfile must be unaffected).
assert!(super::aligned_unit_keys_validate(
&[(0, [0x11u8; 16])],
None,
&[],
ContentFormat::BdTs
));
@@ -5866,7 +5853,6 @@ mod tests {
));
assert!(super::aligned_unit_keys_validate(
&[(0, [0x11u8; 16])],
None,
&[clear.clone()],
ContentFormat::BdTs
));
@@ -5882,21 +5868,18 @@ mod tests {
// Right key -> de-scrambles -> accept (NO false reject of a good key).
assert!(super::aligned_unit_keys_validate(
&[(7, uk)],
None,
std::slice::from_ref(&enc),
ContentFormat::BdTs
));
// Wrong key -> cannot de-scramble a scrambled sample -> reject.
assert!(!super::aligned_unit_keys_validate(
&[(7, [0x00u8; 16])],
None,
std::slice::from_ref(&enc),
ContentFormat::BdTs
));
// Empty key set against a scrambled sample -> reject.
assert!(!super::aligned_unit_keys_validate(
&[],
None,
&[enc],
ContentFormat::BdTs
));
@@ -5935,7 +5918,6 @@ mod tests {
// reject. This is the bug fix: previously this returned true.
assert!(!super::aligned_unit_keys_validate(
&[(0, uk0)],
None,
&samples,
ContentFormat::BdTs
));
@@ -5943,7 +5925,6 @@ mod tests {
// Complete key set (both CPS units) -> accept.
assert!(super::aligned_unit_keys_validate(
&[(0, uk0), (1, uk1)],
None,
&samples,
ContentFormat::BdTs
));
@@ -5951,7 +5932,6 @@ mod tests {
// Order-independent: covering key present anywhere in the set is fine.
assert!(super::aligned_unit_keys_validate(
&[(1, uk1), (0, uk0)],
None,
&samples,
ContentFormat::BdTs
));
+193 -2
View File
@@ -116,6 +116,19 @@ pub struct Drive {
halt: Arc<AtomicBool>,
/// Event handler — fires for read errors and library-level state changes.
event_fn: Option<Box<dyn Fn(Event) + Send>>,
/// The SINGLE AACS bus-encryption removal point. Decided ONCE from the
/// unlock/handshake result during [`crate::disc::Disc::scan`] and applied on
/// every read below this drive, so every reader above (sampler, mux, sweep,
/// validation) sees already-de-bussed content and never threads a bus key.
/// Defaults to [`BusStage::Passthrough`] (firmware/vendor unlock de-busses at
/// the drive, or the disc carries no bus encryption).
bus_stage: crate::sector::bus_removal::BusStage,
/// Encrypted-content extent map (sorted `(start_lba, sector_count)`) that
/// gates the host-key de-bus to content sectors — clear UDF/nav sectors read
/// during a whole-disc pass are left untouched. `None` = de-bus every read
/// sector (a content-only reader). Ignored entirely under
/// [`BusStage::Passthrough`]. See docs/drive-mod.md — bus-removal wiring.
bus_content_ranges: Option<Arc<[(u32, u32)]>>,
/// Linux only: raw fd for the corresponding block device (`/dev/sr*`)
/// used as a recovery fallback when SCSI READ via `/dev/sg*` returns
/// an error. The kernel `sr_mod` driver auto-retries failed reads
@@ -157,11 +170,46 @@ impl Drive {
device_path: device.to_string_lossy().to_string(),
halt: Arc::new(AtomicBool::new(false)),
event_fn: None,
bus_stage: crate::sector::bus_removal::BusStage::Passthrough,
bus_content_ranges: None,
#[cfg(target_os = "linux")]
block_dev_fd,
})
}
/// Set the SINGLE AACS bus-removal stage for this drive, decided once from
/// the unlock/handshake result. `AacsHostKey(rdk)` de-busses each content
/// read in software; `Passthrough` (a firmware/vendor unlock, or a non-bus
/// disc) is a no-op. Every reader above the drive inherits the result.
pub fn set_bus_stage(&mut self, stage: crate::sector::bus_removal::BusStage) {
self.bus_stage = stage;
}
/// Gate host-key de-bussing to the disc's encrypted-content extents (sorted
/// `(start_lba, sector_count)`), so clear UDF/nav sectors read during a
/// whole-disc pass are left untouched. Set after the disc structure is parsed
/// (see [`crate::disc::Disc::encrypted_content_ranges`]); unset means every
/// read sector is treated as content.
pub fn set_bus_content_ranges(&mut self, ranges: Arc<[(u32, u32)]>) {
self.bus_content_ranges = Some(ranges);
}
/// The SOLE application of AACS bus decryption in the read path: under a
/// host-key stage, de-bus the `data` that a raw read landed at `lba`, gated
/// to the encrypted-content ranges. `Passthrough` is a no-op, so the raw
/// `read`/`read_fua` used by internal callers stay bus-encrypted while
/// `SectorSource` readers above see plaintext content.
fn remove_bus_encryption(&self, lba: u32, data: &mut [u8]) {
if let crate::sector::bus_removal::BusStage::AacsHostKey(rdk) = &self.bus_stage {
crate::aacs::content::decrypt_bus_in_content(
data,
rdk,
lba,
self.bus_content_ranges.as_deref(),
);
}
}
// Test-only constructor: build a `Drive` over an arbitrary `ScsiTransport`
// (no profile, no platform driver, no block-device fallback) so
// command-builder/response-parser logic can be exercised against a mock.
@@ -186,6 +234,8 @@ impl Drive {
device_path: "test".to_string(),
halt: Arc::new(AtomicBool::new(false)),
event_fn: None,
bus_stage: crate::sector::bus_removal::BusStage::Passthrough,
bus_content_ranges: None,
#[cfg(target_os = "linux")]
block_dev_fd: None,
}
@@ -1169,7 +1219,9 @@ impl SectorSource for Drive {
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
self.read(lba, count, buf, recovery)
let n = self.read(lba, count, buf, recovery)?;
self.remove_bus_encryption(lba, &mut buf[..n]);
Ok(n)
}
fn read_sectors_fua(
@@ -1180,7 +1232,9 @@ impl SectorSource for Drive {
recovery: bool,
fua: bool,
) -> Result<usize> {
self.read_fua(lba, count, buf, recovery, fua)
let n = self.read_fua(lba, count, buf, recovery, fua)?;
self.remove_bus_encryption(lba, &mut buf[..n]);
Ok(n)
}
fn set_speed(&mut self, kbs: u16) {
@@ -1519,6 +1573,143 @@ mod command_tests {
Drive::from_transport_for_test(Box::new(FixedTransport { payload }))
}
// AACS bus-removal tests (the drive's single de-bus point): a clear "content"
// buffer with plaintext first-16-of-each-sector (bus enc leaves those clear on
// the wire) and a known 16..2048 pattern; `sectors` exercises cross-sector gating.
fn clear_bus_content(sectors: usize) -> Vec<u8> {
let mut v = vec![0u8; sectors * 2048];
for (i, b) in v.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(31).wrapping_add(7);
}
v
}
// Passthrough (the default) is a no-op: every read byte comes back verbatim,
// even when the wire happens to be clear content.
#[test]
fn bus_passthrough_read_returns_bytes_unchanged() {
use crate::sector::SectorSource;
let clear = clear_bus_content(3);
let mut d = drive_with(clear.clone());
let mut got = vec![0u8; clear.len()];
let n = d.read_sectors(0, 3, &mut got, false).unwrap();
assert_eq!(n, clear.len());
assert_eq!(got, clear, "default Passthrough must not alter any byte");
}
// Host-key stage de-busses at read time: a bus-ENCRYPTED wire comes back as
// the known plaintext. MUTATION: leaving `bus_stage` Passthrough (dropping the
// de-bus call) returns ciphertext, so this goes red.
#[test]
fn bus_host_key_removes_bus_encryption_at_read() {
use crate::sector::SectorSource;
let rdk = [0x5Au8; 16];
let clear = clear_bus_content(3);
let mut wire = clear.clone();
crate::aacs::content::encrypt_bus(&mut wire, &rdk); // model the drive's forward bus transform
assert_ne!(wire, clear, "fixture must actually be bus-encrypted");
let mut d = drive_with(wire);
d.set_bus_stage(crate::sector::bus_removal::BusStage::AacsHostKey(rdk));
let mut got = vec![0u8; clear.len()];
let n = d.read_sectors(0, 3, &mut got, false).unwrap();
assert_eq!(n, clear.len());
assert_eq!(
got, clear,
"AacsHostKey must recover the plaintext byte-for-byte"
);
}
// The FUA read path applies the same single de-bus step (the per-sector
// recovery lever must not ship ciphertext).
#[test]
fn bus_host_key_removes_bus_encryption_on_fua_read() {
use crate::sector::SectorSource;
let rdk = [0x5Au8; 16];
let clear = clear_bus_content(3);
let mut wire = clear.clone();
crate::aacs::content::encrypt_bus(&mut wire, &rdk);
let mut d = drive_with(wire);
d.set_bus_stage(crate::sector::bus_removal::BusStage::AacsHostKey(rdk));
let mut got = vec![0u8; clear.len()];
let n = d.read_sectors_fua(0, 3, &mut got, false, true).unwrap();
assert_eq!(n, clear.len());
assert_eq!(got, clear, "the FUA path must de-bus too");
}
// Content-range gating: a sector OUTSIDE the encrypted-content extents is
// clear filesystem/nav and must pass through untouched — de-bussing it would
// corrupt plaintext. LBA 0 is outside content [300,303).
#[test]
fn bus_content_ranges_pass_through_sectors_outside_content() {
use crate::sector::SectorSource;
let rdk = [0x5Au8; 16];
let clear = clear_bus_content(1); // clear filesystem sector on the wire
let mut d = drive_with(clear.clone());
d.set_bus_stage(crate::sector::bus_removal::BusStage::AacsHostKey(rdk));
d.set_bus_content_ranges(std::sync::Arc::from(
vec![(300u32, 3u32)].into_boxed_slice(),
));
let mut got = vec![0u8; clear.len()];
// LBA 0 is outside content → no de-bus → bytes unchanged.
let n = d.read_sectors(0, 1, &mut got, false).unwrap();
assert_eq!(n, clear.len());
assert_eq!(
got, clear,
"a sector outside content must pass through untouched"
);
}
// The complement: a bus-encrypted sector INSIDE the content map is de-bussed.
#[test]
fn bus_content_ranges_debus_sectors_inside_content() {
use crate::sector::SectorSource;
let rdk = [0x33u8; 16];
let clear = clear_bus_content(3);
let mut wire = clear.clone();
crate::aacs::content::encrypt_bus(&mut wire, &rdk);
let mut d = drive_with(wire);
d.set_bus_stage(crate::sector::bus_removal::BusStage::AacsHostKey(rdk));
d.set_bus_content_ranges(std::sync::Arc::from(
vec![(300u32, 3u32)].into_boxed_slice(),
));
let mut got = vec![0u8; clear.len()];
// Read the 3 content sectors at LBA 300 → all inside → de-bussed to plaintext.
let n = d.read_sectors(300, 3, &mut got, false).unwrap();
assert_eq!(n, clear.len());
assert_eq!(got, clear, "content sectors must be de-bussed to plaintext");
}
// Partial gating within one read: content [301,302) covers only the middle
// sector of a 3-sector read at LBA 300. Only that sector is de-bussed; the
// clear neighbours (300, 302) pass through untouched.
#[test]
fn bus_content_ranges_gate_per_sector_within_a_read() {
use crate::sector::SectorSource;
let rdk = [0x44u8; 16];
// Sector 1 is bus-encrypted content; sectors 0 and 2 are clear on the wire.
let clear = clear_bus_content(3);
let mut wire = clear.clone();
let mut mid = clear[2048..4096].to_vec();
crate::aacs::content::encrypt_bus(&mut mid, &rdk); // encrypt only the middle sector's body
wire[2048..4096].copy_from_slice(&mid);
let mut d = drive_with(wire);
d.set_bus_stage(crate::sector::bus_removal::BusStage::AacsHostKey(rdk));
d.set_bus_content_ranges(std::sync::Arc::from(
vec![(301u32, 1u32)].into_boxed_slice(),
));
let mut got = vec![0u8; clear.len()];
d.read_sectors(300, 3, &mut got, false).unwrap();
// All three sectors come back as the known plaintext: 0 and 2 were clear
// and untouched; 1 was de-bussed back to clear.
assert_eq!(
got, clear,
"only the in-range sector is de-bussed; clear neighbours untouched"
);
}
#[test]
fn read_capacity_normal_adds_one() {
// last_lba = 0x0000_0063 (99) → capacity 100 sectors.
-3
View File
@@ -1383,7 +1383,6 @@ mod tests {
// engages. Key contents are irrelevant to the read plan.
let aacs = DecryptKeys::Aacs {
unit_keys: vec![(0, [0u8; 16]), (1, [1u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
// 100 units (300 sectors). A 10-unit Even forensic segment at LBA [30,60):
@@ -1859,7 +1858,6 @@ mod tests {
let title = synthetic_title(COUNT);
let keys = crate::decrypt::DecryptKeys::Aacs {
unit_keys: vec![(0, [0u8; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut stream = DiscStream::new(
@@ -2385,7 +2383,6 @@ mod tests {
synthetic_title(8),
crate::decrypt::DecryptKeys::Aacs {
unit_keys: vec![(0, [0x11u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
},
3,
-4
View File
@@ -1584,7 +1584,6 @@ mod tests {
}];
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
@@ -1644,7 +1643,6 @@ mod tests {
key_source: crate::disc::KeyOrigin::KeyDb,
vuk: None,
unit_keys: vec![(0, unit_key)],
read_data_key: None,
volume_id: [0u8; 16],
uk_ro: Vec::new(),
mkb: Vec::new(),
@@ -1852,7 +1850,6 @@ mod tests {
let mut r = mk_reader();
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = resolve_inline_base_map(
@@ -1871,7 +1868,6 @@ mod tests {
let mut r = mk_reader();
let mut keys_raw = DecryptKeys::Aacs {
unit_keys: vec![(0, unit_key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map_raw = resolve_inline_base_map(
-15
View File
@@ -2359,7 +2359,6 @@ mod tests {
// (pool_len == 1 would short-circuit to content_map before any read).
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0x11u8; 16]), (1, [0x22u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut reader = HaltCountSource {
@@ -2397,7 +2396,6 @@ mod tests {
}];
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0x11u8; 16]), (1, [0x22u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut reader = HaltCountSource {
@@ -3170,7 +3168,6 @@ mod tests {
};
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b), (2, key_c)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let title = multi_cps_title(start, sectors);
@@ -3205,7 +3202,6 @@ mod tests {
};
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let title = multi_cps_title(start, sectors);
@@ -3237,7 +3233,6 @@ mod tests {
};
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let title = multi_cps_title(start, sectors);
@@ -3337,7 +3332,6 @@ mod tests {
]);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b), (2, key_c)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut cache = super::DiscKeyCache::new();
@@ -3430,7 +3424,6 @@ mod tests {
];
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b), (2, key_c)],
read_data_key: None,
format: ContentFormat::BdTs,
};
@@ -3499,7 +3492,6 @@ mod tests {
]);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b), (2, key_c)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut cache = super::DiscKeyCache::new();
@@ -3575,7 +3567,6 @@ mod tests {
// to index 0 without sampling); neither opens the extent → fail loud.
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, [0x02u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
super::resolve_mux_key_map_cached(
@@ -3643,7 +3634,6 @@ mod tests {
let title = multi_cps_title(1000, 30);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0x01u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
let got = super::resolve_fmts_key_map(
@@ -3680,7 +3670,6 @@ mod tests {
let title = multi_cps_title(1000, 30);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0x01u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
let got = super::resolve_fmts_key_map(
@@ -4016,7 +4005,6 @@ mod tests {
fn fmts_keys() -> DecryptKeys {
DecryptKeys::Aacs {
unit_keys: vec![(0, FMTS_BASE_KEY)],
read_data_key: None,
format: ContentFormat::BdTs,
}
}
@@ -4042,7 +4030,6 @@ mod tests {
fn fmts_two_cps_keys() -> DecryptKeys {
DecryptKeys::Aacs {
unit_keys: vec![(1, FMTS_BASE_KEY), (2, FMTS_CPS2_KEY)],
read_data_key: None,
format: ContentFormat::BdTs,
}
}
@@ -4407,7 +4394,6 @@ mod tests {
]);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut cache = super::DiscKeyCache::new();
@@ -5012,7 +4998,6 @@ mod tests {
)]);
let mut keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key_a), (1, key_b), (2, key_c)],
read_data_key: None,
format: ContentFormat::BdTs,
};
let mut cache = super::DiscKeyCache::new();
+373
View File
@@ -0,0 +1,373 @@
//! `BusRemovalSectorSource` — a stream form of AACS bus-encryption removal.
//!
//! # Where bus removal actually happens
//! AACS 2.0 "bus encryption" (BEE) wraps content on the drive↔host transport,
//! and removing it is the UNLOCKER's job. The single de-bus TRANSFORM is
//! [`decrypt_bus_in_content`]; in the PRODUCTION path it is applied once, at the
//! very bottom of the read chain, inside the live drive's own `SectorSource`
//! impl (`Drive::remove_bus_encryption`, gated by the drive's `BusStage` +
//! content ranges). Every reader above it (key sampler, mux, unit-key
//! validation, whole-disc sweep) reads the `Drive` as `&mut dyn SectorSource`
//! and so transparently sees bus-removed sectors and never reasons about bus
//! encryption.
//!
//! This decorator is an equivalent, independently-tested STREAM form of that
//! same transform (it also calls [`decrypt_bus_in_content`]), kept for tests and
//! for any future non-`Drive` source that needs de-bussing. It is NOT wired into
//! the production chain today. DANGER: never wrap the live `Drive` in this
//! decorator — the `Drive` already de-busses, so a second pass would DOUBLE-
//! de-bus and corrupt content. There must only ever be ONE de-bus in a chain.
//!
//! # How the stage is chosen
//! [`BusStage`] is computed ONCE from the unlock result:
//! * a firmware/vendor unlocker (freemkv / LibreDrive / Renesas) removes bus
//! encryption AT THE DRIVE (`Bus=off`), and a non-bus disc (DVD / clear BD)
//! has none → [`BusStage::Passthrough`] (reads pass through untouched);
//! * the AACS cert route yields the Read Data Key from the AKE, so the host
//! must de-bus each content sector → [`BusStage::AacsHostKey`].
//!
//! # Content gating
//! De-bussing a clear UDF/nav sector would corrupt plaintext, so the host-key
//! path is gated to the disc's encrypted-content extents via
//! [`with_content_ranges`](BusRemovalSectorSource::with_content_ranges) — the
//! same `(start_lba, sector_count)` map [`DecryptingSectorSource`] uses. Absent
//! a map, the caller reads content-only (title extents) and every sector is
//! treated as content — matching the pre-overhaul behaviour where `decrypt_bus`
//! ran on content units only.
//!
//! [`decrypt_bus_in_content`]: crate::aacs::content::decrypt_bus_in_content
//! [`DecryptingSectorSource`]: crate::sector::decrypting::DecryptingSectorSource
use std::sync::Arc;
use crate::error::Result;
use super::SectorSource;
/// How this drive's AACS bus encryption is removed — decided once from the
/// unlock result, never re-examined downstream.
#[derive(Clone)]
pub enum BusStage {
/// Bus encryption was removed at the drive by a firmware/vendor unlocker
/// (`Bus=off`), or the disc carries none (DVD / clear BD). Reads pass
/// through untouched.
Passthrough,
/// AACS cert route: the host holds the Read Data Key from the AKE and
/// de-busses each content sector in software.
AacsHostKey([u8; 16]),
}
impl BusStage {
/// Map an unlock/handshake result to the bus stage: a cert-route Read Data
/// Key (`Some`) means the host must de-bus content in software
/// ([`BusStage::AacsHostKey`]); its absence (`None`) means bus encryption was
/// removed at the drive by a firmware/vendor unlock, or the disc never had it
/// ([`BusStage::Passthrough`]). This is the SINGLE decision point that wires
/// [`crate::disc::Disc::scan`]'s handshake to the drive's de-bus.
pub fn from_read_data_key(read_data_key: Option<[u8; 16]>) -> Self {
match read_data_key {
Some(rdk) => BusStage::AacsHostKey(rdk),
None => BusStage::Passthrough,
}
}
}
// `BusStage` wraps a raw key; keep it out of logs.
impl std::fmt::Debug for BusStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BusStage::Passthrough => write!(f, "BusStage::Passthrough"),
BusStage::AacsHostKey(_) => write!(f, "BusStage::AacsHostKey([redacted])"),
}
}
}
/// Decorator: read from `inner`, then remove AACS bus encryption from the bytes
/// that landed in `buf` when the [`BusStage`] carries a host key. A
/// [`BusStage::Passthrough`] stream is a zero-cost pass-through.
pub struct BusRemovalSectorSource<S: SectorSource> {
inner: S,
stage: BusStage,
/// Encrypted-content extent map (sorted/merged `(start_lba, sector_count)`).
/// `None` = caller reads content-only, so every sector is de-bussed;
/// `Some` = a whole-disc reader, so clear sectors outside these ranges pass
/// through untouched. Ignored entirely by [`BusStage::Passthrough`].
content_ranges: Option<Arc<[(u32, u32)]>>,
}
impl<S: SectorSource> BusRemovalSectorSource<S> {
/// Wrap `inner` with the bus stage decided by the unlock result.
pub fn new(inner: S, stage: BusStage) -> Self {
Self {
inner,
stage,
content_ranges: None,
}
}
/// Restrict host-key de-bussing to the disc's encrypted-content extents;
/// clear sectors outside them pass through untouched. Whole-disc readers
/// (sweep / patch) set this; content-only readers (mux title extents,
/// sampler) leave it unset.
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
self.content_ranges = Some(ranges);
self
}
/// `&mut` counterpart of [`with_content_ranges`](Self::with_content_ranges),
/// for readers that build the stream before the content map is known.
pub fn set_content_ranges(&mut self, ranges: Arc<[(u32, u32)]>) {
self.content_ranges = Some(ranges);
}
/// Borrow the inner source (tests / introspection).
pub fn inner(&self) -> &S {
&self.inner
}
/// Mutable borrow of the inner source.
pub fn inner_mut(&mut self) -> &mut S {
&mut self.inner
}
/// Consume the decorator and return the underlying source.
pub fn into_inner(self) -> S {
self.inner
}
}
impl<S: SectorSource> SectorSource for BusRemovalSectorSource<S> {
fn capacity_sectors(&self) -> u32 {
self.inner.capacity_sectors()
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
self.read_sectors_fua(lba, count, buf, recovery, false)
}
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
let n = self
.inner
.read_sectors_fua(lba, count, buf, recovery, fua)?;
if let BusStage::AacsHostKey(rdk) = &self.stage {
crate::aacs::content::decrypt_bus_in_content(
&mut buf[..n],
rdk,
lba,
self.content_ranges.as_deref(),
);
}
Ok(n)
}
fn set_speed(&mut self, kbs: u16) {
self.inner.set_speed(kbs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aacs::content::encrypt_bus;
use crate::consts::SECTOR_BYTES;
/// A source returning a fixed buffer for any read (starting at whatever the
/// fixture built), reporting the full span.
struct FixedSource {
bytes: Vec<u8>,
}
impl SectorSource for FixedSource {
fn capacity_sectors(&self) -> u32 {
(self.bytes.len() / SECTOR_BYTES) as u32
}
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let n = (count as usize * SECTOR_BYTES).min(self.bytes.len());
buf[..n].copy_from_slice(&self.bytes[..n]);
Ok(n)
}
}
// A recognisable clear "content" unit: plaintext first 16 bytes of each
// sector (bus enc leaves those clear on the wire), a known pattern in
// 16..2048. Two sectors so cross-sector gating is exercised.
fn clear_content(sectors: usize) -> Vec<u8> {
let mut v = vec![0u8; sectors * SECTOR_BYTES];
for (i, b) in v.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(31).wrapping_add(7);
}
v
}
// The single wiring decision: a cert-route Read Data Key becomes a host-key
// stage; its absence (firmware/vendor unlock, or a non-bus disc) is Passthrough.
#[test]
fn from_read_data_key_maps_handshake_to_stage() {
let rdk = [0xABu8; 16];
match BusStage::from_read_data_key(Some(rdk)) {
BusStage::AacsHostKey(k) => {
assert_eq!(k, rdk, "cert RDK carries into the host-key stage")
}
BusStage::Passthrough => panic!("a Some(read_data_key) must map to AacsHostKey"),
}
assert!(
matches!(BusStage::from_read_data_key(None), BusStage::Passthrough),
"no read_data_key (firmware/vendor unlock or non-bus disc) must map to Passthrough"
);
}
// Passthrough must hand bytes back byte-identical — even a stream fed
// already-clear content never touches it.
#[test]
fn passthrough_returns_bytes_unchanged() {
let clear = clear_content(2);
let src = FixedSource {
bytes: clear.clone(),
};
let mut s = BusRemovalSectorSource::new(src, BusStage::Passthrough);
let mut got = vec![0u8; 2 * SECTOR_BYTES];
let n = s.read_sectors(100, 2, &mut got, false).unwrap();
assert_eq!(n, 2 * SECTOR_BYTES);
assert_eq!(got, clear, "Passthrough must not alter any byte");
}
// The core contract: a bus-ENCRYPTED sector read through an AacsHostKey
// stream comes back as the known plaintext. MUTATION: dropping the de-bus
// call (or using Passthrough) leaves the ciphertext, so this goes red.
#[test]
fn host_key_removes_bus_encryption() {
let rdk = [0x5Au8; 16];
let clear = clear_content(2);
let mut wire = clear.clone();
encrypt_bus(&mut wire, &rdk); // model the drive's forward bus transform
assert_ne!(wire, clear, "fixture must actually be bus-encrypted");
let src = FixedSource { bytes: wire };
let mut s = BusRemovalSectorSource::new(src, BusStage::AacsHostKey(rdk));
let mut got = vec![0u8; 2 * SECTOR_BYTES];
let n = s.read_sectors(0, 2, &mut got, false).unwrap();
assert_eq!(n, 2 * SECTOR_BYTES);
assert_eq!(
got, clear,
"AacsHostKey must recover the plaintext byte-for-byte"
);
}
// A sector OUTSIDE the content map is clear filesystem and must pass through
// untouched (de-bussing it would corrupt plaintext). LBA 0 is outside content
// [300,303), so the unencrypted bytes come back verbatim.
#[test]
fn host_key_passes_through_sectors_outside_content() {
let rdk = [0x5Au8; 16];
let clear = clear_content(1); // clear filesystem sector on the wire
let src = FixedSource {
bytes: clear.clone(),
};
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(300u32, 3u32)].into_boxed_slice());
let mut s = BusRemovalSectorSource::new(src, BusStage::AacsHostKey(rdk))
.with_content_ranges(ranges);
let mut got = vec![0u8; SECTOR_BYTES];
// LBA 0 is outside content → no de-bus → bytes unchanged.
let n = s.read_sectors(0, 1, &mut got, false).unwrap();
assert_eq!(n, SECTOR_BYTES);
assert_eq!(
got, clear,
"a sector outside content must pass through untouched"
);
}
// The complement: a bus-encrypted sector INSIDE the content map is de-bussed.
#[test]
fn host_key_debusses_sectors_inside_content() {
let rdk = [0x33u8; 16];
let clear = clear_content(1);
let mut wire = clear.clone();
encrypt_bus(&mut wire, &rdk);
let src = FixedSource { bytes: wire };
let ranges: Arc<[(u32, u32)]> = Arc::from(vec![(300u32, 3u32)].into_boxed_slice());
let mut s = BusRemovalSectorSource::new(src, BusStage::AacsHostKey(rdk))
.with_content_ranges(ranges);
let mut got = vec![0u8; SECTOR_BYTES];
// LBA 300 is inside content → de-bus → plaintext recovered.
let n = s.read_sectors(300, 1, &mut got, false).unwrap();
assert_eq!(n, SECTOR_BYTES);
assert_eq!(
got, clear,
"a content sector must be de-bussed to plaintext"
);
}
// Only the reported `n` bytes are de-bussed; a short read must not touch
// bytes beyond `n`. Also proves capacity delegates.
#[test]
fn debus_bounded_by_reported_n_and_capacity_delegates() {
let rdk = [0x77u8; 16];
// Two IDENTICAL bus-encrypted sectors on the wire, but the source reports
// only the FIRST was read. Sector 0 must be de-bussed to plaintext; sector
// 1 (beyond the reported n) must be left as ciphertext, untouched.
let clear = clear_content(1);
let mut enc = clear.clone();
encrypt_bus(&mut enc, &rdk);
let mut bytes = enc.clone();
bytes.extend_from_slice(&enc); // two encrypted sectors
struct ShortSource {
bytes: Vec<u8>,
report: usize,
}
impl SectorSource for ShortSource {
fn capacity_sectors(&self) -> u32 {
42
}
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
buf[..self.bytes.len()].copy_from_slice(&self.bytes);
Ok(self.report)
}
}
let src = ShortSource {
bytes,
report: SECTOR_BYTES, // only sector 0 "read"
};
let mut s = BusRemovalSectorSource::new(src, BusStage::AacsHostKey(rdk));
assert_eq!(s.capacity_sectors(), 42, "capacity must delegate to inner");
let mut got = vec![0u8; 2 * SECTOR_BYTES];
let n = s.read_sectors(0, 2, &mut got, false).unwrap();
assert_eq!(n, SECTOR_BYTES);
// Sector 0 was de-bussed to plaintext...
assert_eq!(
&got[..SECTOR_BYTES],
&clear[..],
"reported sector de-bussed"
);
// ...and sector 1 (beyond n) was NOT touched — still ciphertext.
assert_eq!(
&got[SECTOR_BYTES..],
&enc[..],
"bytes beyond the reported n must stay ciphertext"
);
}
}
-5
View File
@@ -566,7 +566,6 @@ mod tests {
src,
DecryptKeys::Aacs {
unit_keys: Vec::new(),
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
},
);
@@ -757,7 +756,6 @@ mod tests {
let src = FixedUnit { unit };
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = std::sync::Arc::new(crate::decrypt::AacsKeyMap::from_ranges(vec![(
@@ -789,7 +787,6 @@ mod tests {
let src = FixedUnit { unit };
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let mut dec = DecryptingSectorSource::new(src, keys); // no with_key_map
@@ -811,7 +808,6 @@ mod tests {
let src = FixedUnit { unit };
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, key)],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = std::sync::Arc::new(crate::decrypt::AacsKeyMap::from_ranges(vec![(
@@ -841,7 +837,6 @@ mod tests {
let src = PatternedSource { capacity: 16 };
let keys = DecryptKeys::Aacs {
unit_keys: vec![(0, [0u8; 16])],
read_data_key: None,
format: crate::disc::ContentFormat::BdTs,
};
let map = std::sync::Arc::new(crate::decrypt::AacsKeyMap::from_ranges(vec![(300, 303, 0)]));
+1
View File
@@ -8,6 +8,7 @@
//! `SectorSource` and applies AACS / CSS in-place decrypt to
//! yield plaintext sectors.
pub mod bus_removal;
pub mod decrypting;
pub mod prefetched;
-1
View File
@@ -692,7 +692,6 @@ mod tests {
key_source: crate::disc::KeyOrigin::KeyDb,
vuk: None,
unit_keys: Vec::new(),
read_data_key: None,
volume_id: [0u8; 16],
uk_ro: Vec::new(),
mkb: Vec::new(),