AacsKeyMap: a positive map — no key for a sector means pass through
The key map is now purely "these sectors use this key": entry_for returns Option and an LBA in no range is left untouched (no default-decrypt- everything fallback). resolve_mux_key_map builds explicit content ranges for every case — single-CPS keys each content extent, multi-CPS keys each extent with the key that opens it, and FMTS fills the non-segment content with the base Unit Key so a whole-disc read decrypts content and passes nav/filesystem through. Combined with the fail-loud resolve, a map can never silently apply a wrong key, and clear sectors are never scrambled. decrypt_sectors_mapped skips a unit with no map entry; read_plan keeps an unmapped unit (pass-through content) and drops only alternate-phase forensic units. Tests updated to the Option semantics.
This commit is contained in:
+15
-7
@@ -274,15 +274,23 @@ mod tests {
|
|||||||
// sectors 937..=946 → LBA 1937..1947, key index 7.
|
// sectors 937..=946 → LBA 1937..1947, key index 7.
|
||||||
assert_eq!(ranges[1], (1937, 1947, 7));
|
assert_eq!(ranges[1], (1937, 1947, 7));
|
||||||
|
|
||||||
// The ranges drive an AacsKeyMap with the Unit Key (index 0) as default.
|
// The ranges drive a positive AacsKeyMap: an LBA in no range has no key.
|
||||||
let map = crate::decrypt::AacsKeyMap::from_ranges(ranges, 0);
|
let map = crate::decrypt::AacsKeyMap::from_ranges(ranges);
|
||||||
assert_eq!(map.key_idx_for(500), 0, "outside any segment → Unit Key");
|
assert_eq!(map.key_idx_for(500), None, "outside any segment → no key");
|
||||||
assert_eq!(map.key_idx_for(1012), 5, "inside index-5 segment → key 5");
|
assert_eq!(
|
||||||
assert_eq!(map.key_idx_for(1940), 7, "inside index-7 segment → key 7");
|
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!(
|
assert_eq!(
|
||||||
map.key_idx_for(1019),
|
map.key_idx_for(1019),
|
||||||
0,
|
None,
|
||||||
"segment end is exclusive → Unit Key"
|
"segment end is exclusive → no key"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
-76
@@ -206,97 +206,67 @@ pub enum Phase {
|
|||||||
/// common disc pays zero lookup cost and needs no structural walk.
|
/// common disc pays zero lookup cost and needs no structural walk.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct AacsKeyMap {
|
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)>,
|
ranges: Vec<(u32, u32, usize, Phase)>,
|
||||||
default_idx: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AacsKeyMap {
|
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
|
/// Build from `[start_lba, end_lba) → key_idx` ranges that decrypt EVERY unit
|
||||||
/// (multi-CPS): each range is [`Phase::All`]. `default_idx` answers any
|
/// (single- or multi-CPS): each range is [`Phase::All`]. An LBA in no range is
|
||||||
/// uncovered LBA. Byte-for-byte identical behaviour to before phases existed.
|
/// passed through untouched.
|
||||||
pub fn from_ranges(ranges: Vec<(u32, u32, usize)>, default_idx: usize) -> Self {
|
pub fn from_ranges(ranges: Vec<(u32, u32, usize)>) -> Self {
|
||||||
let phased = ranges
|
let phased = ranges
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(s, e, i)| (s, e, i, Phase::All))
|
.map(|(s, e, i)| (s, e, i, Phase::All))
|
||||||
.collect();
|
.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
|
/// 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`]
|
/// opens ([`Phase::Even`]/[`Phase::Odd`] for a forensic segment, [`Phase::All`]
|
||||||
/// for base/CPS). Ranges are sorted; `default_idx` answers any uncovered LBA.
|
/// for base/CPS). Ranges are sorted; an LBA in no range is passed through.
|
||||||
pub fn from_ranges_phased(
|
pub fn from_ranges_phased(mut ranges: Vec<(u32, u32, usize, Phase)>) -> Self {
|
||||||
mut ranges: Vec<(u32, u32, usize, Phase)>,
|
|
||||||
default_idx: usize,
|
|
||||||
) -> Self {
|
|
||||||
ranges.sort_by_key(|&(start, _, _, _)| start);
|
ranges.sort_by_key(|&(start, _, _, _)| start);
|
||||||
Self {
|
Self { ranges }
|
||||||
ranges,
|
|
||||||
default_idx,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`.
|
/// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`, or
|
||||||
/// O(log n) — the last range whose start is `<= lba` and whose end is `> lba`,
|
/// `None` when no range covers it (not encrypted content this map keys — pass
|
||||||
/// else `(default_idx, All, 0)`. `range_start_lba` lets the mapped decrypt
|
/// the unit through untouched). O(log n). `range_start_lba` lets the mapped
|
||||||
/// compute a unit's parity WITHIN a forensic segment (for `Even`/`Odd`).
|
/// decrypt compute a unit's parity WITHIN a forensic segment (`Even`/`Odd`).
|
||||||
pub fn entry_for(&self, lba: u32) -> (usize, Phase, u32) {
|
pub fn entry_for(&self, lba: u32) -> Option<(usize, Phase, u32)> {
|
||||||
if self.ranges.is_empty() {
|
|
||||||
return (self.default_idx, Phase::All, 0);
|
|
||||||
}
|
|
||||||
match self
|
match self
|
||||||
.ranges
|
.ranges
|
||||||
.binary_search_by(|&(start, _, _, _)| start.cmp(&lba))
|
.binary_search_by(|&(start, _, _, _)| start.cmp(&lba))
|
||||||
{
|
{
|
||||||
Ok(i) => {
|
Ok(i) => {
|
||||||
let (start, _, idx, ph) = self.ranges[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) => {
|
Err(i) => {
|
||||||
let (start, end, idx, ph) = self.ranges[i - 1];
|
let (start, end, idx, ph) = self.ranges[i - 1];
|
||||||
if lba >= start && lba < end {
|
(lba >= start && lba < end).then_some((idx, ph, start))
|
||||||
(idx, ph, start)
|
|
||||||
} else {
|
|
||||||
(self.default_idx, Phase::All, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The unit-key index for the aligned unit at `lba` (phase-agnostic; see
|
/// The unit-key index for the aligned unit at `lba`, or `None` when no range
|
||||||
/// [`entry_for`](Self::entry_for) for the phase). Cheap per-unit hot-path call.
|
/// covers it (pass through). See [`entry_for`](Self::entry_for) for the phase.
|
||||||
pub fn key_idx_for(&self, lba: u32) -> usize {
|
pub fn key_idx_for(&self, lba: u32) -> Option<usize> {
|
||||||
self.entry_for(lba).0
|
self.entry_for(lba).map(|(idx, _, _)| idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `[start_lba, end_lba) → (key_idx, phase)` ranges (sorted, disjoint).
|
/// 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)] {
|
pub fn ranges(&self) -> &[(u32, u32, usize, Phase)] {
|
||||||
&self.ranges
|
&self.ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The key index for any LBA no explicit range claims (the single-CPS key).
|
/// The distinct key indices this map selects — the CPS units / segments the
|
||||||
pub fn default_idx(&self) -> usize {
|
/// title actually reaches. The resolver secures exactly these up front.
|
||||||
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.
|
|
||||||
pub fn key_indices(&self) -> Vec<usize> {
|
pub fn key_indices(&self) -> Vec<usize> {
|
||||||
let mut v: Vec<usize> = self.ranges.iter().map(|&(_, _, i, _)| i).collect();
|
let mut v: Vec<usize> = self.ranges.iter().map(|&(_, _, i, _)| i).collect();
|
||||||
v.push(self.default_idx);
|
|
||||||
v.sort_unstable();
|
v.sort_unstable();
|
||||||
v.dedup();
|
v.dedup();
|
||||||
v
|
v
|
||||||
@@ -367,10 +337,11 @@ impl AacsKeyMap {
|
|||||||
push(lba, remaining);
|
push(lba, remaining);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let (_, phase, range_start) = self.entry_for(lba);
|
// A unit in NO range is pass-through content (base/default) — read
|
||||||
let keep = match phase {
|
// it. Only an alternate-phase forensic unit is dropped from the plan.
|
||||||
Phase::All => true,
|
let keep = match self.entry_for(lba) {
|
||||||
Phase::Even | Phase::Odd => {
|
None | Some((_, Phase::All, _)) => true,
|
||||||
|
Some((_, phase, range_start)) => {
|
||||||
let unit_ix = (lba - range_start) / us;
|
let unit_ix = (lba - range_start) / us;
|
||||||
let is_odd = unit_ix % 2 == 1;
|
let is_odd = unit_ix % 2 == 1;
|
||||||
is_odd == matches!(phase, Phase::Odd)
|
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
|
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 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
|
// PHASE GATE (FMTS forensic segment): the segment interleaves two variants
|
||||||
// at the unit level. Decrypt ONLY our parity; leave the alternate half as
|
// at the unit level. Decrypt ONLY our parity; leave the alternate half as
|
||||||
// ciphertext (the muxer drops untouched ciphertext cleanly — no garble).
|
// ciphertext (the muxer drops untouched ciphertext cleanly — no garble).
|
||||||
@@ -1626,18 +1601,19 @@ mod tests {
|
|||||||
|
|
||||||
// ── FMTS phase-aware map ──────────────────────────────────────────────────
|
// ── FMTS phase-aware map ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/// `entry_for` returns (idx, phase, range_start); `from_ranges` is All,
|
/// `entry_for` returns Some((idx, phase, range_start)) inside a range;
|
||||||
/// `from_ranges_phased` carries the phase; uncovered → (default, All, 0).
|
/// `from_ranges` is All, `from_ranges_phased` carries the phase; an uncovered
|
||||||
|
/// LBA is `None` (pass through).
|
||||||
#[test]
|
#[test]
|
||||||
fn aacskeymap_phase_entry_for() {
|
fn aacskeymap_phase_entry_for() {
|
||||||
let all = AacsKeyMap::from_ranges(vec![(100, 200, 3)], 0);
|
let all = AacsKeyMap::from_ranges(vec![(100, 200, 3)]);
|
||||||
assert_eq!(all.entry_for(150), (3, Phase::All, 100));
|
assert_eq!(all.entry_for(150), Some((3, Phase::All, 100)));
|
||||||
assert_eq!(all.entry_for(50), (0, Phase::All, 0));
|
assert_eq!(all.entry_for(50), None);
|
||||||
|
|
||||||
let phased = AacsKeyMap::from_ranges_phased(vec![(100, 200, 3, Phase::Odd)], 7);
|
let phased = AacsKeyMap::from_ranges_phased(vec![(100, 200, 3, Phase::Odd)]);
|
||||||
assert_eq!(phased.entry_for(150), (3, Phase::Odd, 100));
|
assert_eq!(phased.entry_for(150), Some((3, Phase::Odd, 100)));
|
||||||
assert_eq!(phased.entry_for(250), (7, Phase::All, 0));
|
assert_eq!(phased.entry_for(250), None);
|
||||||
assert_eq!(phased.key_idx_for(150), 3);
|
assert_eq!(phased.key_idx_for(150), Some(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A map with no forensic (Even/Odd) range is the common disc: `read_plan`
|
/// A map with no forensic (Even/Odd) range is the common disc: `read_plan`
|
||||||
@@ -1656,9 +1632,9 @@ mod tests {
|
|||||||
sector_count: 60,
|
sector_count: 60,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// Single-CPS and multi-CPS (All) maps both leave the plan untouched.
|
// A non-forensic map (empty, or multi-CPS All) leaves the plan untouched.
|
||||||
assert_eq!(AacsKeyMap::single(0).read_plan(&ext, us), ext);
|
assert_eq!(AacsKeyMap::from_ranges(vec![]).read_plan(&ext, us), ext);
|
||||||
let multi = AacsKeyMap::from_ranges(vec![(1000, 1150, 2)], 0);
|
let multi = AacsKeyMap::from_ranges(vec![(1000, 1150, 2)]);
|
||||||
assert_eq!(multi.read_plan(&ext, us), ext);
|
assert_eq!(multi.read_plan(&ext, us), ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1677,7 +1653,7 @@ mod tests {
|
|||||||
start_lba: 1000,
|
start_lba: 1000,
|
||||||
sector_count: 300,
|
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 plan = map.read_plan(&ext, us);
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
Extent {
|
Extent {
|
||||||
@@ -1719,8 +1695,7 @@ mod tests {
|
|||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
while off < e.sector_count {
|
while off < e.sector_count {
|
||||||
let lba = e.start_lba + off;
|
let lba = e.start_lba + off;
|
||||||
let (_, phase, rs) = map.entry_for(lba);
|
if let Some((_, phase @ (Phase::Even | Phase::Odd), rs)) = map.entry_for(lba) {
|
||||||
if let Phase::Even | Phase::Odd = phase {
|
|
||||||
let is_odd = ((lba - rs) / us) % 2 == 1;
|
let is_odd = ((lba - rs) / us) % 2 == 1;
|
||||||
assert!(
|
assert!(
|
||||||
is_odd == matches!(phase, Phase::Odd),
|
is_odd == matches!(phase, Phase::Odd),
|
||||||
@@ -1756,7 +1731,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
format: ContentFormat::BdTs,
|
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");
|
decrypt_sectors_mapped(&mut buf, &keys, 0, &map).expect("even phase decrypts clean");
|
||||||
for i in 0..8 {
|
for i in 0..8 {
|
||||||
let u = &buf[i * ul..(i + 1) * ul];
|
let u = &buf[i * ul..(i + 1) * ul];
|
||||||
@@ -1791,7 +1766,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
format: ContentFormat::BdTs,
|
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!(
|
assert!(matches!(
|
||||||
decrypt_sectors_mapped(&mut buf, &keys, 0, &map),
|
decrypt_sectors_mapped(&mut buf, &keys, 0, &map),
|
||||||
Err(crate::error::Error::DecryptFailed)
|
Err(crate::error::Error::DecryptFailed)
|
||||||
@@ -1816,7 +1791,12 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
format: ContentFormat::BdTs,
|
format: ContentFormat::BdTs,
|
||||||
};
|
};
|
||||||
decrypt_sectors_mapped(&mut buf, &keys, 0, &AacsKeyMap::single(0))
|
decrypt_sectors_mapped(
|
||||||
|
&mut buf,
|
||||||
|
&keys,
|
||||||
|
0,
|
||||||
|
&AacsKeyMap::from_ranges(vec![(0, u32::MAX, 0)]),
|
||||||
|
)
|
||||||
.expect("all-phase decrypts");
|
.expect("all-phase decrypts");
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
+1
-1
@@ -1180,7 +1180,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// 100 units (300 sectors). A 10-unit Even forensic segment at LBA [30,60):
|
// 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.
|
// 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(
|
let stream = DiscStream::new(
|
||||||
Box::new(ZeroReader { capacity: 300 }),
|
Box::new(ZeroReader { capacity: 300 }),
|
||||||
synthetic_title(300),
|
synthetic_title(300),
|
||||||
|
|||||||
+49
-13
@@ -951,9 +951,33 @@ fn resolve_fmts_key_map(
|
|||||||
return Err(crate::error::Error::FmtsKeyMissing.into());
|
return Err(crate::error::Error::FmtsKeyMissing.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(crate::decrypt::AacsKeyMap::from_ranges_phased(
|
// Cover the NON-segment content with the base Unit Key: the forensic segments
|
||||||
ranges, base_idx,
|
// (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
|
/// 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
|
/// ciphertext samples, where the `is_clean` proof IS sound). The mux then just
|
||||||
/// decrypts each unit with its mapped key and trusts it.
|
/// decrypts each unit with its mapped key and trusts it.
|
||||||
///
|
///
|
||||||
/// Single-CPS (the overwhelming majority, incl. every single-key UHD) is the
|
/// Single-CPS (the overwhelming majority, incl. every single-key UHD) keys every
|
||||||
/// trivial map: one key everywhere, no sampling. Multi-CPS assigns each extent to
|
/// content extent with one index; multi-CPS keys each extent with the key that
|
||||||
/// the key that opens a real sample from it; a bad-content extent no sample can
|
/// opens a real sample from it; FMTS layers per-segment index keys on top. Any LBA
|
||||||
/// classify inherits its predecessor's key (contiguity). FMTS segment mapping
|
/// outside the title's content (nav/filesystem) is in no range and passes through.
|
||||||
/// layers onto the same structure.
|
///
|
||||||
|
/// 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(
|
pub fn resolve_mux_key_map(
|
||||||
reader: &mut dyn SectorSource,
|
reader: &mut dyn SectorSource,
|
||||||
title: &DiscTitle,
|
title: &DiscTitle,
|
||||||
@@ -994,8 +1029,9 @@ pub fn resolve_mux_key_map(
|
|||||||
// always >= 1 for the AACS map paths below.
|
// always >= 1 for the AACS map paths below.
|
||||||
let pool_len = match keys {
|
let pool_len = match keys {
|
||||||
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => unit_keys.len(),
|
crate::decrypt::DecryptKeys::Aacs { unit_keys, .. } => unit_keys.len(),
|
||||||
// CSS / clear: no AACS map (the decorator's map path is AACS-only).
|
// CSS / clear: the AACS map keys nothing here — an empty map passes every
|
||||||
_ => return Ok(crate::decrypt::AacsKeyMap::single(0)),
|
// 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
|
// 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
|
// 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);
|
return Ok(map);
|
||||||
}
|
}
|
||||||
if pool_len == 1 {
|
if pool_len == 1 {
|
||||||
// One CPS unit → one key everywhere. No structural walk, no sampling.
|
// One CPS unit → key 0 over every content extent; nav passes through.
|
||||||
return Ok(crate::decrypt::AacsKeyMap::single(0));
|
return Ok(content_map(title, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-CPS: read a spread of real encrypted units from each extent and pick
|
// 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,
|
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
|
/// Assemble the ISO mux pipeline (read+decrypt → demux → parse) for
|
||||||
|
|||||||
Reference in New Issue
Block a user