diff --git a/src/aacs/segment.rs b/src/aacs/segment.rs index 454b7fa..88eed1b 100644 --- a/src/aacs/segment.rs +++ b/src/aacs/segment.rs @@ -274,15 +274,23 @@ mod tests { // sectors 937..=946 → LBA 1937..1947, key index 7. assert_eq!(ranges[1], (1937, 1947, 7)); - // The ranges drive an AacsKeyMap with the Unit Key (index 0) as default. - let map = crate::decrypt::AacsKeyMap::from_ranges(ranges, 0); - assert_eq!(map.key_idx_for(500), 0, "outside any segment → Unit Key"); - assert_eq!(map.key_idx_for(1012), 5, "inside index-5 segment → key 5"); - assert_eq!(map.key_idx_for(1940), 7, "inside index-7 segment → key 7"); + // The ranges drive a positive AacsKeyMap: an LBA in no range has no key. + let map = crate::decrypt::AacsKeyMap::from_ranges(ranges); + assert_eq!(map.key_idx_for(500), None, "outside any segment → no key"); + assert_eq!( + map.key_idx_for(1012), + Some(5), + "inside index-5 segment → key 5" + ); + assert_eq!( + map.key_idx_for(1940), + Some(7), + "inside index-7 segment → key 7" + ); assert_eq!( map.key_idx_for(1019), - 0, - "segment end is exclusive → Unit Key" + None, + "segment end is exclusive → no key" ); } diff --git a/src/decrypt.rs b/src/decrypt.rs index 8093046..7995d1d 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -206,97 +206,67 @@ pub enum Phase { /// common disc pays zero lookup cost and needs no structural walk. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AacsKeyMap { - // (start_lba, end_lba, key_idx, phase) + // (start_lba, end_lba, key_idx, phase). An LBA in NO range is passed through + // untouched — the map is a positive list of "this key here", nothing more. ranges: Vec<(u32, u32, usize, Phase)>, - default_idx: usize, } impl AacsKeyMap { - /// The whole title is one CPS unit → one key (`idx`) everywhere. This is the - /// overwhelmingly common disc (incl. every single-CPS UHD); no LBA walk. - pub fn single(idx: usize) -> Self { - Self { - ranges: Vec::new(), - default_idx: idx, - } - } - /// Build from `[start_lba, end_lba) → key_idx` ranges that decrypt EVERY unit - /// (multi-CPS): each range is [`Phase::All`]. `default_idx` answers any - /// uncovered LBA. Byte-for-byte identical behaviour to before phases existed. - pub fn from_ranges(ranges: Vec<(u32, u32, usize)>, default_idx: usize) -> Self { + /// (single- or multi-CPS): each range is [`Phase::All`]. An LBA in no range is + /// passed through untouched. + pub fn from_ranges(ranges: Vec<(u32, u32, usize)>) -> Self { let phased = ranges .into_iter() .map(|(s, e, i)| (s, e, i, Phase::All)) .collect(); - Self::from_ranges_phased(phased, default_idx) + Self::from_ranges_phased(phased) } /// Build a PHASE-AWARE map (FMTS): each range carries which unit-parity its key /// opens ([`Phase::Even`]/[`Phase::Odd`] for a forensic segment, [`Phase::All`] - /// for base/CPS). Ranges are sorted; `default_idx` answers any uncovered LBA. - pub fn from_ranges_phased( - mut ranges: Vec<(u32, u32, usize, Phase)>, - default_idx: usize, - ) -> Self { + /// for base/CPS). Ranges are sorted; an LBA in no range is passed through. + pub fn from_ranges_phased(mut ranges: Vec<(u32, u32, usize, Phase)>) -> Self { ranges.sort_by_key(|&(start, _, _, _)| start); - Self { - ranges, - default_idx, - } + Self { ranges } } - /// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`. - /// O(log n) — the last range whose start is `<= lba` and whose end is `> lba`, - /// else `(default_idx, All, 0)`. `range_start_lba` lets the mapped decrypt - /// compute a unit's parity WITHIN a forensic segment (for `Even`/`Odd`). - pub fn entry_for(&self, lba: u32) -> (usize, Phase, u32) { - if self.ranges.is_empty() { - return (self.default_idx, Phase::All, 0); - } + /// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`, or + /// `None` when no range covers it (not encrypted content this map keys — pass + /// the unit through untouched). O(log n). `range_start_lba` lets the mapped + /// decrypt compute a unit's parity WITHIN a forensic segment (`Even`/`Odd`). + pub fn entry_for(&self, lba: u32) -> Option<(usize, Phase, u32)> { match self .ranges .binary_search_by(|&(start, _, _, _)| start.cmp(&lba)) { Ok(i) => { let (start, _, idx, ph) = self.ranges[i]; - (idx, ph, start) + Some((idx, ph, start)) } - Err(0) => (self.default_idx, Phase::All, 0), + Err(0) => None, Err(i) => { let (start, end, idx, ph) = self.ranges[i - 1]; - if lba >= start && lba < end { - (idx, ph, start) - } else { - (self.default_idx, Phase::All, 0) - } + (lba >= start && lba < end).then_some((idx, ph, start)) } } } - /// The unit-key index for the aligned unit at `lba` (phase-agnostic; see - /// [`entry_for`](Self::entry_for) for the phase). Cheap per-unit hot-path call. - pub fn key_idx_for(&self, lba: u32) -> usize { - self.entry_for(lba).0 + /// The unit-key index for the aligned unit at `lba`, or `None` when no range + /// covers it (pass through). See [`entry_for`](Self::entry_for) for the phase. + pub fn key_idx_for(&self, lba: u32) -> Option { + self.entry_for(lba).map(|(idx, _, _)| idx) } /// The `[start_lba, end_lba) → (key_idx, phase)` ranges (sorted, disjoint). - /// Empty for a single-CPS map (everything uses [`default_idx`](Self::default_idx)). pub fn ranges(&self) -> &[(u32, u32, usize, Phase)] { &self.ranges } - /// The key index for any LBA no explicit range claims (the single-CPS key). - pub fn default_idx(&self) -> usize { - self.default_idx - } - - /// The distinct key indices this map can select — the CPS units / segments a - /// title actually reaches. Used by the resolver to know which keys to secure - /// up front. + /// The distinct key indices this map selects — the CPS units / segments the + /// title actually reaches. The resolver secures exactly these up front. pub fn key_indices(&self) -> Vec { let mut v: Vec = self.ranges.iter().map(|&(_, _, i, _)| i).collect(); - v.push(self.default_idx); v.sort_unstable(); v.dedup(); v @@ -367,10 +337,11 @@ impl AacsKeyMap { push(lba, remaining); break; } - let (_, phase, range_start) = self.entry_for(lba); - let keep = match phase { - Phase::All => true, - Phase::Even | Phase::Odd => { + // A unit in NO range is pass-through content (base/default) — read + // it. Only an alternate-phase forensic unit is dropped from the plan. + let keep = match self.entry_for(lba) { + None | Some((_, Phase::All, _)) => true, + Some((_, phase, range_start)) => { let unit_ix = (lba - range_start) / us; let is_odd = unit_ix % 2 == 1; is_odd == matches!(phase, Phase::Odd) @@ -440,7 +411,11 @@ pub fn decrypt_sectors_mapped( return; // trailing partial unit: clear tail on disc, leave as-is } let unit_lba = base_lba.saturating_add((idx_in_buf as u32) * unit_sectors); - let (key_idx, phase, range_start) = map.entry_for(unit_lba); + // No range covers this LBA → the map keys no content here, so pass the + // unit through untouched (clear filesystem / nav on a whole-disc read). + let Some((key_idx, phase, range_start)) = map.entry_for(unit_lba) else { + return; + }; // PHASE GATE (FMTS forensic segment): the segment interleaves two variants // at the unit level. Decrypt ONLY our parity; leave the alternate half as // ciphertext (the muxer drops untouched ciphertext cleanly — no garble). @@ -1626,18 +1601,19 @@ mod tests { // ── FMTS phase-aware map ────────────────────────────────────────────────── - /// `entry_for` returns (idx, phase, range_start); `from_ranges` is All, - /// `from_ranges_phased` carries the phase; uncovered → (default, All, 0). + /// `entry_for` returns Some((idx, phase, range_start)) inside a range; + /// `from_ranges` is All, `from_ranges_phased` carries the phase; an uncovered + /// LBA is `None` (pass through). #[test] fn aacskeymap_phase_entry_for() { - let all = AacsKeyMap::from_ranges(vec![(100, 200, 3)], 0); - assert_eq!(all.entry_for(150), (3, Phase::All, 100)); - assert_eq!(all.entry_for(50), (0, Phase::All, 0)); + let all = AacsKeyMap::from_ranges(vec![(100, 200, 3)]); + assert_eq!(all.entry_for(150), Some((3, Phase::All, 100))); + assert_eq!(all.entry_for(50), None); - let phased = AacsKeyMap::from_ranges_phased(vec![(100, 200, 3, Phase::Odd)], 7); - assert_eq!(phased.entry_for(150), (3, Phase::Odd, 100)); - assert_eq!(phased.entry_for(250), (7, Phase::All, 0)); - assert_eq!(phased.key_idx_for(150), 3); + let phased = AacsKeyMap::from_ranges_phased(vec![(100, 200, 3, Phase::Odd)]); + assert_eq!(phased.entry_for(150), Some((3, Phase::Odd, 100))); + assert_eq!(phased.entry_for(250), None); + assert_eq!(phased.key_idx_for(150), Some(3)); } /// A map with no forensic (Even/Odd) range is the common disc: `read_plan` @@ -1656,9 +1632,9 @@ mod tests { sector_count: 60, }, ]; - // Single-CPS and multi-CPS (All) maps both leave the plan untouched. - assert_eq!(AacsKeyMap::single(0).read_plan(&ext, us), ext); - let multi = AacsKeyMap::from_ranges(vec![(1000, 1150, 2)], 0); + // A non-forensic map (empty, or multi-CPS All) leaves the plan untouched. + assert_eq!(AacsKeyMap::from_ranges(vec![]).read_plan(&ext, us), ext); + let multi = AacsKeyMap::from_ranges(vec![(1000, 1150, 2)]); assert_eq!(multi.read_plan(&ext, us), ext); } @@ -1677,7 +1653,7 @@ mod tests { start_lba: 1000, sector_count: 300, }]; - let map = AacsKeyMap::from_ranges_phased(vec![(1030, 1060, 5, Phase::Even)], 0); + let map = AacsKeyMap::from_ranges_phased(vec![(1030, 1060, 5, Phase::Even)]); let plan = map.read_plan(&ext, us); let expected = vec![ Extent { @@ -1719,8 +1695,7 @@ mod tests { let mut off = 0; while off < e.sector_count { let lba = e.start_lba + off; - let (_, phase, rs) = map.entry_for(lba); - if let Phase::Even | Phase::Odd = phase { + if let Some((_, phase @ (Phase::Even | Phase::Odd), rs)) = map.entry_for(lba) { let is_odd = ((lba - rs) / us) % 2 == 1; assert!( is_odd == matches!(phase, Phase::Odd), @@ -1756,7 +1731,7 @@ mod tests { read_data_key: None, format: ContentFormat::BdTs, }; - let map = AacsKeyMap::from_ranges_phased(vec![(0, 8 * usz, 0, Phase::Even)], 0); + let map = AacsKeyMap::from_ranges_phased(vec![(0, 8 * usz, 0, Phase::Even)]); decrypt_sectors_mapped(&mut buf, &keys, 0, &map).expect("even phase decrypts clean"); for i in 0..8 { let u = &buf[i * ul..(i + 1) * ul]; @@ -1791,7 +1766,7 @@ mod tests { read_data_key: None, format: ContentFormat::BdTs, }; - let map = AacsKeyMap::from_ranges_phased(vec![(0, 2 * usz, 0, Phase::Even)], 0); + let map = AacsKeyMap::from_ranges_phased(vec![(0, 2 * usz, 0, Phase::Even)]); assert!(matches!( decrypt_sectors_mapped(&mut buf, &keys, 0, &map), Err(crate::error::Error::DecryptFailed) @@ -1816,8 +1791,13 @@ mod tests { read_data_key: None, format: ContentFormat::BdTs, }; - decrypt_sectors_mapped(&mut buf, &keys, 0, &AacsKeyMap::single(0)) - .expect("all-phase decrypts"); + decrypt_sectors_mapped( + &mut buf, + &keys, + 0, + &AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]), + ) + .expect("all-phase decrypts"); for i in 0..4 { assert!( aacs::content::is_clean(&buf[i * ul..(i + 1) * ul], ContentFormat::BdTs), diff --git a/src/mux/disc.rs b/src/mux/disc.rs index a555898..884ce08 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -1180,7 +1180,7 @@ mod tests { }; // 100 units (300 sectors). A 10-unit Even forensic segment at LBA [30,60): // even units (30,36,42,48,54) are ours; odd (33,39,45,51,57) are dropped. - let map = AacsKeyMap::from_ranges_phased(vec![(30, 60, 1, Phase::Even)], 0); + let map = AacsKeyMap::from_ranges_phased(vec![(30, 60, 1, Phase::Even)]); let stream = DiscStream::new( Box::new(ZeroReader { capacity: 300 }), synthetic_title(300), diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index f70834b..b339e30 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -951,9 +951,33 @@ fn resolve_fmts_key_map( return Err(crate::error::Error::FmtsKeyMissing.into()); } - Ok(Some(crate::decrypt::AacsKeyMap::from_ranges_phased( - ranges, base_idx, - ))) + // Cover the NON-segment content with the base Unit Key: the forensic segments + // (added above with their index keys) carve holes out of the title's content + // extents; every other content unit uses the base UK. Fill the gaps so the map + // is a complete positive list — an LBA in no range is nav and passes through. + let cuts: Vec<(u32, u32)> = { + let mut c: Vec<(u32, u32)> = ranges.iter().map(|&(s, e, _, _)| (s, e)).collect(); + c.sort_unstable(); + c + }; + for ext in &title.extents { + let end = ext.start_lba.saturating_add(ext.sector_count); + let mut cur = ext.start_lba; + for &(cs, ce) in &cuts { + if ce <= cur || cs >= end { + continue; // cut outside this extent + } + if cs > cur { + ranges.push((cur, cs, base_idx, crate::decrypt::Phase::All)); + } + cur = cur.max(ce); + } + if cur < end { + ranges.push((cur, end, base_idx, crate::decrypt::Phase::All)); + } + } + + Ok(Some(crate::decrypt::AacsKeyMap::from_ranges_phased(ranges))) } /// Resolve the proactive [`AacsKeyMap`](crate::decrypt::AacsKeyMap) for a title @@ -971,11 +995,22 @@ fn resolve_fmts_key_map( /// ciphertext samples, where the `is_clean` proof IS sound). The mux then just /// decrypts each unit with its mapped key and trusts it. /// -/// Single-CPS (the overwhelming majority, incl. every single-key UHD) is the -/// trivial map: one key everywhere, no sampling. Multi-CPS assigns each extent to -/// the key that opens a real sample from it; a bad-content extent no sample can -/// classify inherits its predecessor's key (contiguity). FMTS segment mapping -/// layers onto the same structure. +/// Single-CPS (the overwhelming majority, incl. every single-key UHD) keys every +/// content extent with one index; multi-CPS keys each extent with the key that +/// opens a real sample from it; FMTS layers per-segment index keys on top. Any LBA +/// outside the title's content (nav/filesystem) is in no range and passes through. +/// +/// A single-key content map: every content extent → `idx`; everything else passes +/// through. The positive-map replacement for the old "one key everywhere" default. +fn content_map(title: &DiscTitle, idx: usize) -> crate::decrypt::AacsKeyMap { + let ranges = title + .extents + .iter() + .map(|e| (e.start_lba, e.start_lba.saturating_add(e.sector_count), idx)) + .collect(); + crate::decrypt::AacsKeyMap::from_ranges(ranges) +} + pub fn resolve_mux_key_map( reader: &mut dyn SectorSource, title: &DiscTitle, @@ -994,8 +1029,9 @@ pub fn resolve_mux_key_map( // always >= 1 for the AACS map paths below. let pool_len = match keys { crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => unit_keys.len(), - // CSS / clear: no AACS map (the decorator's map path is AACS-only). - _ => return Ok(crate::decrypt::AacsKeyMap::single(0)), + // CSS / clear: the AACS map keys nothing here — an empty map passes every + // unit through (CSS self-descrambles on its own path). + _ => return Ok(crate::decrypt::AacsKeyMap::from_ranges(Vec::new())), }; // FMTS (AACS 2.1): if the disc carries `IndividualSegment.tbl`, the forensic // segments need per-index keys the base Unit Key can't open. Resolve them up @@ -1006,8 +1042,8 @@ pub fn resolve_mux_key_map( return Ok(map); } if pool_len == 1 { - // One CPS unit → one key everywhere. No structural walk, no sampling. - return Ok(crate::decrypt::AacsKeyMap::single(0)); + // One CPS unit → key 0 over every content extent; nav passes through. + return Ok(content_map(title, 0)); } // Multi-CPS: read a spread of real encrypted units from each extent and pick @@ -1095,7 +1131,7 @@ pub fn resolve_mux_key_map( idx, )); } - Ok(crate::decrypt::AacsKeyMap::from_ranges(ranges, 0)) + Ok(crate::decrypt::AacsKeyMap::from_ranges(ranges)) } /// Assemble the ISO mux pipeline (read+decrypt → demux → parse) for