mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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:
Generated
+5
-5
@@ -221,7 +221,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "freemkv-unlock"
|
||||
version = "1.7.1"
|
||||
version = "1.7.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -317,7 +317,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libfreemkv"
|
||||
version = "1.7.1"
|
||||
version = "1.7.4"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -839,12 +839,12 @@ dependencies = [
|
||||
|
||||
[[patch.unused]]
|
||||
name = "freemkv-engine"
|
||||
version = "1.7.1"
|
||||
version = "1.7.4"
|
||||
|
||||
[[patch.unused]]
|
||||
name = "freemkv-i18n"
|
||||
version = "1.7.1"
|
||||
version = "1.7.4"
|
||||
|
||||
[[patch.unused]]
|
||||
name = "freemkv-keysources"
|
||||
version = "1.7.1"
|
||||
version = "1.7.4"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "1.7.3"
|
||||
version = "1.7.4"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
license = "MIT"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)]));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -88,7 +88,6 @@ fn decrypt_keys_is_encrypted_variants() {
|
||||
|
||||
let aacs = DecryptKeys::Aacs {
|
||||
unit_keys: vec![],
|
||||
read_data_key: None,
|
||||
format: libfreemkv::disc::ContentFormat::BdTs,
|
||||
};
|
||||
assert!(aacs.is_encrypted());
|
||||
|
||||
Reference in New Issue
Block a user