diff --git a/Cargo.lock b/Cargo.lock index 0e11993d..a9df4365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 3764a878..7b0cdea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "1.7.3" +version = "1.7.4" edition = "2024" rust-version = "1.88" license = "MIT" diff --git a/src/aacs/content.rs b/src/aacs/content.rs index 9d3f269b..6e6f5dd5 100644 --- a/src/aacs/content.rs +++ b/src/aacs/content.rs @@ -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 diff --git a/src/css/mod.rs b/src/css/mod.rs index 9290bd12..4cd0008e 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -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( diff --git a/src/decrypt.rs b/src/decrypt.rs index 1a400b02..8a5a6c33 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -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, + } + 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 { + 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( diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index 4e42ee64..5ebacca7 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -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" - ); } } diff --git a/src/disc/extract.rs b/src/disc/extract.rs index 8956091e..a023be66 100644 --- a/src/disc/extract.rs +++ b/src/disc/extract.rs @@ -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(), diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 73451318..d8a27469 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -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(|_| "")) .field("unit_keys_len", &self.unit_keys.len()) - .field("read_data_key", &self.read_data_key.map(|_| "")) .field("volume_id", &"") .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], 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]) -> 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 )); diff --git a/src/drive/mod.rs b/src/drive/mod.rs index c908e0e5..bbc8ec71 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -116,6 +116,19 @@ pub struct Drive { halt: Arc, /// Event handler — fires for read errors and library-level state changes. event_fn: Option>, + /// 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>, /// 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 { - 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 { - 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 { + 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. diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 75b88f00..1ec3206d 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -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, diff --git a/src/mux/driver.rs b/src/mux/driver.rs index 9a884bd3..82cc6efd 100644 --- a/src/mux/driver.rs +++ b/src/mux/driver.rs @@ -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( diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 18b0d875..d8f599e4 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -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(); diff --git a/src/sector/bus_removal.rs b/src/sector/bus_removal.rs new file mode 100644 index 00000000..e76bd9e6 --- /dev/null +++ b/src/sector/bus_removal.rs @@ -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 { + 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>, +} + +impl BusRemovalSectorSource { + /// 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 SectorSource for BusRemovalSectorSource { + fn capacity_sectors(&self) -> u32 { + self.inner.capacity_sectors() + } + + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + recovery: bool, + ) -> Result { + 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 { + 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, + } + 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 { + 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 { + 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, + 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 { + 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" + ); + } +} diff --git a/src/sector/decrypting.rs b/src/sector/decrypting.rs index 9b3dac88..3185776a 100644 --- a/src/sector/decrypting.rs +++ b/src/sector/decrypting.rs @@ -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)])); diff --git a/src/sector/mod.rs b/src/sector/mod.rs index ec122034..b2f54ecc 100644 --- a/src/sector/mod.rs +++ b/src/sector/mod.rs @@ -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; diff --git a/src/session.rs b/src/session.rs index 44368462..dfb68202 100644 --- a/src/session.rs +++ b/src/session.rs @@ -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(), diff --git a/tests/pass_n_patch_fix.rs b/tests/pass_n_patch_fix.rs index dc677978..4a3c4d25 100644 --- a/tests/pass_n_patch_fix.rs +++ b/tests/pass_n_patch_fix.rs @@ -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());