mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc87962e50 | ||
|
|
4221cd6a86 | ||
|
|
dda4e7482b | ||
|
|
f80551f278 |
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 0.31.8 (2026-06-17)
|
||||
|
||||
### Added
|
||||
- `recover_dk_position(mkb, key) -> Option<DeviceKey>`: recover an unpositioned
|
||||
device key's subset-difference position (`node`/`uv`/`u_mask_shift`) by
|
||||
scanning a disc MKB — the zero-descent slot probe and ancestor walk-up, with
|
||||
the verify reduced to the hoisted terminal Processing Key. Lets a caller boil
|
||||
a position-less device key against a real disc.
|
||||
|
||||
### Changed
|
||||
- Consolidate the SD walk surface: remove the research-only
|
||||
`probe::walk_pk_against_tables` (the single PK→MK engine is reached via
|
||||
`derive_media_key_from_pk`); make `derive_media_key_from_pk_walked` internal.
|
||||
|
||||
|
||||
## 0.31.7 (2026-06-17)
|
||||
|
||||
### Added
|
||||
- `derive_media_key_and_pk_from_dk(mkb, dks) -> Option<(media_key, processing_key)>`:
|
||||
expose the intermediate Processing Key the subset-difference walk already
|
||||
computes, so callers can record the full key lineage on a Device-Key walk.
|
||||
|
||||
### Changed
|
||||
- Unify the subset-difference walk: the duplicate `aesg3`/`calc_v_mask`/
|
||||
`calc_pk_from_dk` copies are consolidated to one definition. No behavior change.
|
||||
|
||||
|
||||
## 0.31.6 (2026-06-09)
|
||||
|
||||
### Fixed
|
||||
|
||||
- UDF `read_file`: the AACS Media Key Block (`MKB_RO.inf`) is allocated to a
|
||||
fixed ~128 MiB on UHD discs and zero-padded, but the real record stream is
|
||||
only a few MiB. 0.31.0 added a 64 MiB `MAX_FILE_BYTES` cap on `read_file`,
|
||||
which rejected the padded MKB outright — so `read_aacs_inputs` failed and the
|
||||
online key-resolve path reported "could not read this disc's key files" and
|
||||
**never contacted the keyserver** (a regression on every disc whose MKB
|
||||
exceeds 64 MiB). The MKB is now read length-aware: a bounded prefix is read,
|
||||
the real record length is found via the MKB header, and exactly that is
|
||||
returned — never the padding, and never tripping the cap. Validated
|
||||
end-to-end against a real UHD ISO (MKB reads and trims to its record length).
|
||||
- UDF `read_file`: honor inline/embedded allocation descriptors (ICB Tag flags
|
||||
low bits == 3). Tiny files (some AACS `*.inf` key files) store their data
|
||||
embedded directly in the ICB with no out-of-line extents; these are now read
|
||||
from the ICB payload instead of misparsing the embedded bytes as allocation
|
||||
descriptors (which could hard-error since 0.31.0).
|
||||
|
||||
## 0.31.5 (2026-06-08)
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "0.31.5"
|
||||
version = "0.31.8"
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
+226
-41
@@ -240,7 +240,7 @@ const PK_WALK_MAX_DEPTH_CAP: u8 = 5;
|
||||
///
|
||||
/// The BFS frontier grows as `2^max_depth`; `max_depth` is clamped to
|
||||
/// [`PK_WALK_MAX_DEPTH_CAP`] so a large value cannot exhaust memory.
|
||||
pub fn derive_media_key_from_pk_walked(
|
||||
pub(crate) fn derive_media_key_from_pk_walked(
|
||||
mkb: &[u8],
|
||||
processing_keys: &[[u8; 16]],
|
||||
max_depth: u8,
|
||||
@@ -251,9 +251,8 @@ pub fn derive_media_key_from_pk_walked(
|
||||
walk_pk_against_tables_impl(processing_keys, &uvs, &cvalues, &mk_dv, max_depth)
|
||||
}
|
||||
|
||||
/// Core Subset-Difference PK walk over explicit record bodies. Shared by
|
||||
/// [`derive_media_key_from_pk_walked`] (production, records auto-selected) and
|
||||
/// [`probe::walk_pk_against_tables`] (harness, records caller-pinned).
|
||||
/// Core Subset-Difference PK walk over explicit record bodies. The single PK→MK
|
||||
/// walk engine, reached in production via [`derive_media_key_from_pk`].
|
||||
fn walk_pk_against_tables_impl(
|
||||
processing_keys: &[[u8; 16]],
|
||||
uvs: &[u8],
|
||||
@@ -414,30 +413,6 @@ pub mod probe {
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the exact production Subset-Difference PK walk
|
||||
/// ([`super::derive_media_key_from_pk_walked`]) but against
|
||||
/// CALLER-SUPPLIED record bodies — so a harness can pin a specific
|
||||
/// Media-Key-Data table (record `0x05` on AACS 2.x UHD MKBs, which the
|
||||
/// production `mkb_find_cvalues` now selects) and the matching `0x04`
|
||||
/// Subset-Difference Index, across ALL entries.
|
||||
///
|
||||
/// `subdiff` is the type-0x04 body (5-byte entries
|
||||
/// `[u_mask_shift][uv:be32]`); `cvalues` is the chosen cvalue table
|
||||
/// (16-byte entries); `mk_dv` is from the verify record. Each entry in
|
||||
/// `keys` is tried as a terminal PK and as an SD node-key descending via
|
||||
/// `AES-G3(K, 0|1|2)` for `max_depth` levels — identical logic to the
|
||||
/// production walk (`max_depth` is clamped to the same internal cap to
|
||||
/// bound the `2^depth` frontier). Returns the verified Media Key, if any.
|
||||
pub fn walk_pk_against_tables(
|
||||
keys: &[[u8; 16]],
|
||||
subdiff: &[u8],
|
||||
cvalues: &[u8],
|
||||
mk_dv: &[u8; 16],
|
||||
max_depth: u8,
|
||||
) -> Option<[u8; 16]> {
|
||||
super::walk_pk_against_tables_impl(keys, subdiff, cvalues, mk_dv, max_depth)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB.
|
||||
@@ -611,7 +586,10 @@ const AESG3_SEED: [u8; 16] = [
|
||||
|
||||
/// AACS-G3: derive a subkey from a parent key.
|
||||
/// seed[15] += inc, then AES-DEC(key, seed) XOR seed.
|
||||
fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
///
|
||||
/// Shared with [`super::variants`] (its variant chain runs the same SD
|
||||
/// tree); a single definition keeps the two walks byte-identical.
|
||||
pub(super) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
let mut seed = AESG3_SEED;
|
||||
seed[15] = seed[15].wrapping_add(inc);
|
||||
let mut out = aes_ecb_decrypt(key, &seed);
|
||||
@@ -621,8 +599,8 @@ fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
out
|
||||
}
|
||||
|
||||
/// Compute v_mask from a UV value.
|
||||
fn calc_v_mask(uv: u32) -> u32 {
|
||||
/// Compute v_mask from a UV value. Shared with [`super::variants`].
|
||||
pub(super) fn calc_v_mask(uv: u32) -> u32 {
|
||||
let mut v_mask: u32 = 0xFFFF_FFFF;
|
||||
while (uv & !v_mask) == 0 && v_mask != 0 {
|
||||
v_mask <<= 1;
|
||||
@@ -631,7 +609,13 @@ fn calc_v_mask(uv: u32) -> u32 {
|
||||
}
|
||||
|
||||
/// Derive processing key from device key using subset-difference tree traversal.
|
||||
fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] {
|
||||
/// Shared with [`super::variants`].
|
||||
pub(super) fn calc_pk_from_dk(
|
||||
dk: &[u8; 16],
|
||||
uv: u32,
|
||||
v_mask: u32,
|
||||
dev_key_v_mask: u32,
|
||||
) -> [u8; 16] {
|
||||
// Initial derivation: left_child = aesg3(dk, 0), pk = aesg3(dk, 1), right_child = aesg3(dk, 2)
|
||||
let mut left_child = aesg3(dk, 0);
|
||||
let mut pk = aesg3(dk, 1);
|
||||
@@ -675,7 +659,26 @@ fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) ->
|
||||
}
|
||||
|
||||
/// Derive Media Key from MKB using device keys (subset-difference tree).
|
||||
///
|
||||
/// Thin wrapper over [`derive_media_key_and_pk_from_dk`] that drops the
|
||||
/// intermediate Processing Key. Callers that need the PK lineage (e.g.
|
||||
/// the key service banking DK·PK·MK) should call the `_and_pk_` form.
|
||||
pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> {
|
||||
derive_media_key_and_pk_from_dk(mkb, device_keys).map(|(mk, _pk)| mk)
|
||||
}
|
||||
|
||||
/// Derive both the Media Key and the intermediate Processing Key from an
|
||||
/// MKB using device keys (subset-difference tree).
|
||||
///
|
||||
/// Identical walk to [`derive_media_key_from_dk`]; this form additionally
|
||||
/// returns the Processing Key `Kp` derived at the matching subset-difference
|
||||
/// node — the value `calc_pk_from_dk` produces immediately before it
|
||||
/// validates into the Media Key. Returns `Some((mk, pk))` for the first DK
|
||||
/// that walks a uv slot whose Processing Key validates against the MKB.
|
||||
pub fn derive_media_key_and_pk_from_dk(
|
||||
mkb: &[u8],
|
||||
device_keys: &[DeviceKey],
|
||||
) -> Option<([u8; 16], [u8; 16])> {
|
||||
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||
let cvalues = mkb_find_cvalues(mkb)?;
|
||||
@@ -735,7 +738,7 @@ pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option
|
||||
if let Some(mk) =
|
||||
validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv)
|
||||
{
|
||||
return Some(mk);
|
||||
return Some((mk, pk));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,6 +748,108 @@ pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option
|
||||
None
|
||||
}
|
||||
|
||||
/// Recover the subset-difference position (`node`, `uv`, `u_mask_shift`) of an
|
||||
/// UNPOSITIONED device key by scanning a disc MKB. A device key alone (just the
|
||||
/// 16 bytes) cannot be walked — the walk needs its tree node. This finds that
|
||||
/// node empirically: for each MKB subset-difference record, it tries the device
|
||||
/// at the record's node AND at every ancestor v-position (the device may sit one
|
||||
/// or more levels ABOVE the record, descending via AES-G to reach it), deriving
|
||||
/// the candidate Processing Key DIRECTLY (one [`calc_pk_from_dk`] per candidate,
|
||||
/// no full re-walk) and checking it validates against that record's cvalue.
|
||||
///
|
||||
/// On the first verifying candidate it pins `(uv, u_mask_shift)` — invariant for
|
||||
/// the key across all discs — and resolves a gate-passing `node` (a one-time
|
||||
/// ≤32-try search at the single hit). Returns a [`DeviceKey`] ready to bank and
|
||||
/// reuse on every future disc via [`derive_media_key_from_dk`]. `None` if the
|
||||
/// key does not apply to this MKB.
|
||||
///
|
||||
/// Cost is `O(slots × tree_depth)` — linear in the MKB's subset-difference
|
||||
/// index, not the quartic cost of re-deriving per candidate.
|
||||
pub fn recover_dk_position(mkb: &[u8], key: &[u8; 16]) -> Option<DeviceKey> {
|
||||
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||
let cvalues = mkb_find_cvalues(mkb)?;
|
||||
let num_uvs = uvs
|
||||
.chunks(5)
|
||||
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
||||
.count();
|
||||
let n_cv = cvalues.len() / 16;
|
||||
|
||||
// Hoisted ONCE for the whole scan: the Processing Key the device produces if
|
||||
// it sits EXACTLY at a record (zero descent) is `AES-G3(key, 1)` — it does
|
||||
// not depend on the record, so the zero-descent probe of every slot reuses
|
||||
// this single value instead of re-deriving it per slot.
|
||||
let pk_zero_descent = aesg3(key, 1);
|
||||
|
||||
for i in 0..num_uvs {
|
||||
if i >= n_cv {
|
||||
break;
|
||||
}
|
||||
let u_mask_shift = uvs[5 * i];
|
||||
if u_mask_shift >= 32 {
|
||||
continue;
|
||||
}
|
||||
let p_uv = &uvs[1 + 5 * i..];
|
||||
let uv_r = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
||||
if uv_r == 0 {
|
||||
continue;
|
||||
}
|
||||
let v_mask = calc_v_mask(uv_r);
|
||||
let cv = &cvalues[i * 16..(i + 1) * 16];
|
||||
let uv_bytes = &uvs[1 + i * 5..];
|
||||
|
||||
// Zero descent (device sits at this slot's node): the cheapest, most
|
||||
// common case — one verify against the hoisted PK, no descent.
|
||||
if validate_processing_key(&pk_zero_descent, cv, uv_bytes, &mk_dv).is_some() {
|
||||
return resolve_dk_node(mkb, key, uv_r, u_mask_shift);
|
||||
}
|
||||
|
||||
// Descent: the device is an ANCESTOR of the slot. Walk the depth bit up
|
||||
// from the slot's lowest set bit; each level descends from the device's
|
||||
// node to the slot's node. (k = p is the zero-descent case handled
|
||||
// above, so start one above it.)
|
||||
let p = uv_r.trailing_zeros();
|
||||
for k in (p + 1)..32 {
|
||||
let uv_d = if k + 1 >= 32 {
|
||||
1u32 << k
|
||||
} else {
|
||||
(uv_r & (0xFFFF_FFFFu32 << (k + 1))) | (1u32 << k)
|
||||
};
|
||||
let pk = calc_pk_from_dk(key, uv_r, v_mask, calc_v_mask(uv_d));
|
||||
if validate_processing_key(&pk, cv, uv_bytes, &mk_dv).is_some() {
|
||||
return resolve_dk_node(mkb, key, uv_d, u_mask_shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve a positioned [`DeviceKey`] for an orphan `key` known to sit at
|
||||
/// `(uv, u_mask_shift)`: find a `device_number` (node) that passes the walk's
|
||||
/// subset-difference gate on `mkb`. The derived key is independent of the exact
|
||||
/// node (it only gates), so any gating node yields the same Media Key — a
|
||||
/// one-time ≤32-try search, run only once at the recovered position.
|
||||
fn resolve_dk_node(mkb: &[u8], key: &[u8; 16], uv: u32, u_mask_shift: u8) -> Option<DeviceKey> {
|
||||
for b in 0..u_mask_shift {
|
||||
let dk = DeviceKey {
|
||||
key: *key,
|
||||
node: ((uv ^ (1u32 << b)) & 0xFFFF) as u16,
|
||||
uv,
|
||||
u_mask_shift,
|
||||
};
|
||||
if derive_media_key_from_dk(mkb, std::slice::from_ref(&dk)).is_some() {
|
||||
return Some(dk);
|
||||
}
|
||||
}
|
||||
// Degenerate MKB (no gating bit): fall back to the node itself.
|
||||
Some(DeviceKey {
|
||||
key: *key,
|
||||
node: (uv & 0xFFFF) as u16,
|
||||
uv,
|
||||
u_mask_shift,
|
||||
})
|
||||
}
|
||||
|
||||
/// MKB disc structure format code.
|
||||
const MKB_DISC_STRUCTURE_FORMAT: u8 = 0x83;
|
||||
/// MKB pack buffer size.
|
||||
@@ -1602,12 +1707,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn probe_walk_pk_against_tables_accepts_planted_pk_rejects_corrupt() {
|
||||
// Lock in the shared SD walk used by both production
|
||||
// (`derive_media_key_from_pk_walked`) and the independent-reproduction
|
||||
// harness (`probe::walk_pk_against_tables`). Plant a terminal PK whose
|
||||
// derived Media Key satisfies a synthetic verify record; confirm the
|
||||
// walk ACCEPTS it against caller-supplied SD/cvalue tables and REJECTS a
|
||||
// 1-byte corruption.
|
||||
// Lock in the shared SD walk (`walk_pk_against_tables_impl`) used by the
|
||||
// production PK path (`derive_media_key_from_pk`). Plant a terminal PK
|
||||
// whose derived Media Key satisfies a synthetic verify record; confirm
|
||||
// the walk ACCEPTS it against caller-supplied SD/cvalue tables and
|
||||
// REJECTS a 1-byte corruption.
|
||||
use super::super::decrypt::aes_ecb_encrypt as enc;
|
||||
|
||||
let pk: [u8; 16] = [
|
||||
@@ -1634,14 +1738,14 @@ mod tests {
|
||||
subdiff.extend_from_slice(&uv);
|
||||
|
||||
assert_eq!(
|
||||
probe::walk_pk_against_tables(std::slice::from_ref(&pk), &subdiff, &cv, &mk_dv, 1),
|
||||
walk_pk_against_tables_impl(std::slice::from_ref(&pk), &subdiff, &cv, &mk_dv, 1),
|
||||
Some(mk),
|
||||
"planted terminal PK must verify"
|
||||
);
|
||||
let mut bad = pk;
|
||||
bad[0] ^= 0xFF;
|
||||
assert_eq!(
|
||||
probe::walk_pk_against_tables(std::slice::from_ref(&bad), &subdiff, &cv, &mk_dv, 1),
|
||||
walk_pk_against_tables_impl(std::slice::from_ref(&bad), &subdiff, &cv, &mk_dv, 1),
|
||||
None,
|
||||
"corrupted PK must be rejected"
|
||||
);
|
||||
@@ -2741,4 +2845,85 @@ mod tests {
|
||||
let mkb = [0x05, 0x00, 0x00, 0x04]; // type 0x05, no body
|
||||
assert!(probe::mkb_record_body(&mkb, 0x05).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_media_key_and_pk_from_dk_returns_intermediate_pk() {
|
||||
// Regression: a classical DK boil must yield the intermediate
|
||||
// Processing Key, not just the Media Key. The key service banks the
|
||||
// PK lineage (DK·PK·MK·VUK·UK); before the `_and_pk_` form existed it
|
||||
// recovered the MK here but lost the PK silently.
|
||||
//
|
||||
// Build a minimal classical MKB (no 0x82/0x83) with:
|
||||
// - 0x04 Subset-Difference: u_mask_shift=3, uv=0x00000002
|
||||
// - 0x05 cvalues: one cvalue C planted so AES-D(Kp, C) XOR uv == mk
|
||||
// - 0x86 Verify Media Key: mk_dv = AES-E(mk, magic || pad)
|
||||
// and a DK with node=4, uv=2, u_mask_shift=3 so dev_key_v_mask ==
|
||||
// v_mask: the calc_pk_from_dk loop is a no-op and Kp == aesg3(dk, 1).
|
||||
use super::super::decrypt::aes_ecb_encrypt as enc;
|
||||
|
||||
let dk_bytes: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||
0xFF, 0x00,
|
||||
];
|
||||
// Expected Processing Key for the no-op walk.
|
||||
let expected_pk = aesg3(&dk_bytes, 1);
|
||||
|
||||
// Plant a known Media Key.
|
||||
let mk: [u8; 16] = [
|
||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
|
||||
0xAE, 0xAF,
|
||||
];
|
||||
// uv (big-endian) = 0x00000002; validate XORs uv into mk[12..16].
|
||||
let uv_bytes: [u8; 4] = [0x00, 0x00, 0x00, 0x02];
|
||||
|
||||
// cvalue C = AES-E(Kp, mk_raw) where mk_raw = mk with the uv XOR
|
||||
// pre-undone, so the validate step XORs uv back in and recovers mk.
|
||||
let mut mk_raw = mk;
|
||||
for a in 0..4 {
|
||||
mk_raw[12 + a] ^= uv_bytes[a];
|
||||
}
|
||||
let cvalue = enc(&expected_pk, &mk_raw);
|
||||
|
||||
// mk_dv = AES-E(mk, magic || pad); validate decrypts it under mk and
|
||||
// checks the leading 8 bytes against the verify magic.
|
||||
let mut plaintext_vd = [0u8; 16];
|
||||
plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
|
||||
plaintext_vd[8..].copy_from_slice(&[0x11; 8]);
|
||||
let mk_dv = enc(&mk, &plaintext_vd);
|
||||
|
||||
// Assemble the MKB. Type/Version (0x10) header first.
|
||||
let mut mkb = vec![
|
||||
0x10, 0x00, 0x00, 0x0C, 0x48, 0x14, 0x10, 0x03, 0x00, 0x00, 0x00, 0x4D,
|
||||
];
|
||||
// 0x04 Subset-Difference: body = u_mask_shift(0x03) || uv(4 bytes).
|
||||
mkb.extend_from_slice(&[0x04, 0x00, 0x00, 0x09]);
|
||||
mkb.extend_from_slice(&[0x03]);
|
||||
mkb.extend_from_slice(&uv_bytes);
|
||||
// 0x05 cvalues: one 16-byte cvalue (mkb_find_cvalues prefers 0x05).
|
||||
mkb.extend_from_slice(&[0x05, 0x00, 0x00, 0x14]);
|
||||
mkb.extend_from_slice(&cvalue);
|
||||
// 0x86 Verify Media Key: mk_dv.
|
||||
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
||||
mkb.extend_from_slice(&mk_dv);
|
||||
|
||||
let dk = DeviceKey {
|
||||
key: dk_bytes,
|
||||
node: 4,
|
||||
uv: 2,
|
||||
u_mask_shift: 3,
|
||||
};
|
||||
|
||||
// The new `_and_pk_` form returns BOTH the MK and the intermediate PK.
|
||||
let dks = [dk];
|
||||
let (got_mk, got_pk) = derive_media_key_and_pk_from_dk(&mkb, &dks)
|
||||
.expect("classical DK boil must derive (mk, pk)");
|
||||
assert_eq!(got_mk, mk, "recovered Media Key must match the planted MK");
|
||||
assert_eq!(
|
||||
got_pk, expected_pk,
|
||||
"returned Processing Key must equal aesg3(dk, 1) for the no-op walk"
|
||||
);
|
||||
|
||||
// And the thin wrapper must still return just the MK.
|
||||
assert_eq!(derive_media_key_from_dk(&mkb, &dks), Some(mk));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -31,10 +31,10 @@ pub use keydb::{DeviceKey, DiscEntry, HostCert, KeyDb};
|
||||
pub use keys::probe;
|
||||
pub use keys::{
|
||||
AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key,
|
||||
derive_media_key_from_dk, derive_media_key_from_pk, derive_media_key_from_pk_walked,
|
||||
derive_media_key_and_pk_from_dk, derive_media_key_from_dk, derive_media_key_from_pk,
|
||||
derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, mkb_version, parse_content_cert,
|
||||
parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21,
|
||||
trim_mkb,
|
||||
parse_unit_key_ro, read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2,
|
||||
resolve_keys_v21, trim_mkb,
|
||||
};
|
||||
pub use provider::KeyProvider;
|
||||
pub use variants::{
|
||||
|
||||
+34
-67
@@ -177,66 +177,11 @@ fn aes_g(x1: &[u8; 16], x2: &[u8; 16]) -> [u8; 16] {
|
||||
|
||||
// ── Subset-difference walk that exposes (Kp, uv) ──────────────────────────
|
||||
|
||||
/// AES-G3 seed register initial value.
|
||||
const AESG3_SEED: [u8; 16] = [
|
||||
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
|
||||
];
|
||||
|
||||
/// AES-G3 single step: AES-G against the seed register at offset `inc`.
|
||||
fn aesg3_step(key: &[u8; 16], inc: u8) -> [u8; 16] {
|
||||
let mut seed = AESG3_SEED;
|
||||
seed[15] = seed[15].wrapping_add(inc);
|
||||
aes_g(key, &seed)
|
||||
}
|
||||
|
||||
fn calc_v_mask(uv: u32) -> u32 {
|
||||
let mut v_mask: u32 = 0xFFFF_FFFF;
|
||||
while (uv & !v_mask) == 0 && v_mask != 0 {
|
||||
v_mask <<= 1;
|
||||
}
|
||||
v_mask
|
||||
}
|
||||
|
||||
fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) -> [u8; 16] {
|
||||
let mut left_child = aesg3_step(dk, 0);
|
||||
let mut pk = aesg3_step(dk, 1);
|
||||
let mut right_child = aesg3_step(dk, 2);
|
||||
let mut current_v_mask = dev_key_v_mask;
|
||||
|
||||
// Bound the walk to the 32-level depth of a u32 subset-difference tree.
|
||||
// `current_v_mask` advances via an arithmetic `>> 1` which sign-extends, so
|
||||
// a disc-supplied v_mask coarser than dev_key_v_mask would otherwise drive
|
||||
// current_v_mask up to 0xFFFF_FFFF and spin forever — a crafted MKB must
|
||||
// not hang the rip thread (this runs before the KCD placeholder gate).
|
||||
let mut steps = 0u32;
|
||||
while current_v_mask != v_mask {
|
||||
if steps >= 32 {
|
||||
break;
|
||||
}
|
||||
steps += 1;
|
||||
let mut bit_pos: i32 = -1;
|
||||
for i in (0..32).rev() {
|
||||
if (current_v_mask & (1u32 << i)) == 0 {
|
||||
bit_pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let curr_key = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 {
|
||||
left_child
|
||||
} else {
|
||||
right_child
|
||||
};
|
||||
|
||||
left_child = aesg3_step(&curr_key, 0);
|
||||
pk = aesg3_step(&curr_key, 1);
|
||||
right_child = aesg3_step(&curr_key, 2);
|
||||
|
||||
current_v_mask = ((current_v_mask as i32) >> 1) as u32;
|
||||
}
|
||||
|
||||
pk
|
||||
}
|
||||
// `calc_v_mask` and `calc_pk_from_dk` (and the AES-G3 seed step they ride
|
||||
// on) are shared with the classical walk in [`super::keys`] — a single
|
||||
// definition keeps the variant SD tree byte-identical to the classical one.
|
||||
// (`aesg3` itself is imported separately in the test module.)
|
||||
use super::keys::{calc_pk_from_dk, calc_v_mask};
|
||||
|
||||
/// Outcome of a subset-difference walk against an MKB. Carries the
|
||||
/// processing key and the matching `uv` slot — both needed as inputs
|
||||
@@ -271,6 +216,27 @@ fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||
|
||||
/// Walk an MKB and return the first `(Kp, uv, cvalue)` that
|
||||
/// `device_keys` covers. Returns `None` if no DK walks any uv.
|
||||
///
|
||||
/// This is the AACS-2.1 **variant** walk; the classical walk lives in
|
||||
/// [`super::keys::derive_media_key_and_pk_from_dk`]. The two are kept
|
||||
/// separate on purpose and select MKB records in DELIBERATELY different
|
||||
/// order:
|
||||
///
|
||||
/// - cvalues: this variant walk tries record `0x07`-then-`0x05`; the
|
||||
/// classical walk tries `0x05`-then-`0x07`. On a variant MKB the
|
||||
/// small `0x07` Explicit-Subset-Difference record carries the
|
||||
/// cvalue the Precursor chain consumes, whereas a classical UHD MKB
|
||||
/// keeps its 1:1 cvalue table in the large `0x05` record (see the
|
||||
/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be
|
||||
/// unified to one order — each is correct for its own MKB shape.
|
||||
/// - finders: this walk operates on parsed [`MkbRecord`]s (needed
|
||||
/// because the variant chain also reads `0x82`/`0x83`); the
|
||||
/// classical walk operates on raw MKB bytes. Same framing, different
|
||||
/// input type.
|
||||
///
|
||||
/// Consequence: do NOT route the classical DK path through this function
|
||||
/// — on a classical MKB the `0x07`-first selection picks the wrong (or
|
||||
/// missing) cvalue and the magic check fails, so it returns `None`.
|
||||
pub fn walk_processing_key(
|
||||
records: &[MkbRecord],
|
||||
device_keys: &[DeviceKey],
|
||||
@@ -544,6 +510,10 @@ pub fn derive_media_key_variant(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// These three live in `super::keys` now (consolidated SD-walk helpers);
|
||||
// `use super::*` does not re-export the parent module's private `use`
|
||||
// imports, so pull them in directly for the tests below.
|
||||
use super::super::keys::{aesg3, calc_pk_from_dk};
|
||||
|
||||
#[test]
|
||||
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
||||
@@ -708,7 +678,7 @@ mod tests {
|
||||
/// agreeing with uv on bits 3+ (the u_mask=1 region). dk.uv ==
|
||||
/// MKB.uv and dk.u_mask_shift == MKB.u_mask_shift make
|
||||
/// `dev_key_v_mask == v_mask`, so `calc_pk_from_dk` loops zero
|
||||
/// times — Kp = aesg3_step(dk, 1).
|
||||
/// times — Kp = aesg3(dk, 1).
|
||||
/// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a
|
||||
/// Kmp whose byte-15 is exactly `kmp15`.
|
||||
/// - record 0x82 with a 16-byte body (acts as both Variant Data
|
||||
@@ -731,12 +701,12 @@ mod tests {
|
||||
// Pick a known DK; with dk.uv == MKB.uv (==2) and
|
||||
// dk.u_mask_shift == MKB.u_mask_shift (==3), dev_key_v_mask
|
||||
// equals the MKB's v_mask and the calc_pk_from_dk loop is a
|
||||
// no-op — Kp = aesg3_step(dk, 1).
|
||||
// no-op — Kp = aesg3(dk, 1).
|
||||
let dk_bytes: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||
0xFF, 0x00,
|
||||
];
|
||||
let kp = aesg3_step(&dk_bytes, 1);
|
||||
let kp = aesg3(&dk_bytes, 1);
|
||||
|
||||
// Plant Kmp with chosen byte-15, then compute C such that
|
||||
// AES-D(Kp, C) ⊕ uv == Kmp. uv=2 → low-4 bytes XOR is 00 00 00 02.
|
||||
@@ -1002,10 +972,7 @@ mod tests {
|
||||
let (recs, dk, planted_kp, _) = synthetic_variant_setup(0x00);
|
||||
let m = walk_processing_key(&recs, &[dk]).expect("variant MKB yields a match");
|
||||
assert_eq!(m.uv, 2, "matched the planted uv");
|
||||
assert_eq!(
|
||||
m.kp, planted_kp,
|
||||
"Kp equals aesg3_step(dk,1) for the no-op walk"
|
||||
);
|
||||
assert_eq!(m.kp, planted_kp, "Kp equals aesg3(dk,1) for the no-op walk");
|
||||
assert_eq!(m.cvalue_index, 0);
|
||||
}
|
||||
|
||||
|
||||
+34
-9
@@ -1307,15 +1307,40 @@ impl Disc {
|
||||
.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let mkb = udf_fs
|
||||
.read_file(reader, "/AACS/MKB_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RW.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
// Trim trailing padding to the real MKB content length, never zeroing
|
||||
// an unrecognised MKB (see `crate::aacs::trim_mkb` — restores the
|
||||
// pre-0.31.0 guard so the online key service never receives an empty
|
||||
// MKB).
|
||||
Ok((inf, crate::aacs::trim_mkb(mkb)))
|
||||
let mkb = Self::read_mkb_content(reader, udf_fs)?;
|
||||
Ok((inf, mkb))
|
||||
}
|
||||
|
||||
/// Read the AACS MKB's real record stream — NOT its zero padding.
|
||||
///
|
||||
/// `MKB_RO.inf` / `MKB_RW.inf` are allocated to a fixed ~128 MiB and
|
||||
/// zero-padded; the actual record stream is a few MiB. We read a bounded
|
||||
/// prefix, find the record-stream length via [`crate::aacs::mkb_content_len`]
|
||||
/// and return exactly that, growing the prefix if the records run past it.
|
||||
/// This avoids reading 100+ MiB of padding on every scan AND avoids the
|
||||
/// `read_file` `MAX_FILE_BYTES` cap that (since 0.31.0) rejected the padded
|
||||
/// 128 MiB MKB outright — which made `read_aacs_inputs` fail and autorip
|
||||
/// report "could not read this disc's key files" without ever contacting
|
||||
/// the keyserver.
|
||||
fn read_mkb_content(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs) -> Result<Vec<u8>> {
|
||||
const START_BYTES: usize = 16 * 1024 * 1024;
|
||||
const MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
let mut want = START_BYTES;
|
||||
loop {
|
||||
let buf = udf_fs
|
||||
.read_file_prefix(reader, "/AACS/MKB_RO.inf", want)
|
||||
.or_else(|_| udf_fs.read_file_prefix(reader, "/AACS/MKB_RW.inf", want))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let n = crate::aacs::mkb_content_len(&buf);
|
||||
// `n` strictly inside `buf` => the record walk reached the padding
|
||||
// boundary (full content captured). `buf` shorter than `want` =>
|
||||
// the whole file is already read. Otherwise the records may run
|
||||
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
||||
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
||||
return Ok(crate::aacs::trim_mkb(buf));
|
||||
}
|
||||
want = (want * 2).min(MAX_BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a disc's AACS key-input files from an ISO image: returns
|
||||
|
||||
+148
-43
@@ -21,11 +21,19 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::sector::SectorSource;
|
||||
|
||||
/// Upper bound on a single metadata file read (`read_file`). BD-ROM
|
||||
/// metadata files (.mpls/.clpi/.inf/.bdmv) are a few KiB to tens of MiB;
|
||||
/// 64 MiB is a generous ceiling. Caps the allocation so a crafted ICB
|
||||
/// info_length / extent length cannot force a huge zeroed reservation
|
||||
/// before any data is read.
|
||||
/// Upper bound on a single UNBOUNDED metadata file read (`read_file`).
|
||||
/// BD-ROM metadata files (.mpls/.clpi/.bdmv/.inf) are a few KiB to a few
|
||||
/// MiB; 64 MiB is a generous ceiling that bounds the allocation a crafted
|
||||
/// ICB info_length / extent length can force.
|
||||
///
|
||||
/// The one legitimately huge file — the AACS `MKB_RO.inf`, allocated to a
|
||||
/// fixed ~128 MiB and zero-padded — is NOT read through the unbounded path:
|
||||
/// `read_aacs_inputs_from_reader` reads a bounded prefix via
|
||||
/// `read_file_prefix` and trims to the real record length, so it never
|
||||
/// trips this cap (and never reads 100+ MiB of padding). A 0.31.0
|
||||
/// regression added this cap and read the MKB unbounded, so the cap
|
||||
/// rejected it → `read_aacs_inputs` failed → autorip reported "could not
|
||||
/// read this disc's key files" and never contacted the keyserver.
|
||||
const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
|
||||
/// Upper bound on a single directory's on-disc data. Real BD-ROM
|
||||
@@ -135,6 +143,35 @@ impl UdfFs {
|
||||
/// Read a file by path, returning its raw bytes.
|
||||
/// Reads all data extents sector by sector from disc — no buffering.
|
||||
pub fn read_file(&self, reader: &mut dyn SectorSource, path: &str) -> Result<Vec<u8>> {
|
||||
self.read_file_limited(reader, path, None)
|
||||
}
|
||||
|
||||
/// Read at most `max_bytes` of a file (rounded up to a whole sector),
|
||||
/// stopping early rather than reading the whole file.
|
||||
///
|
||||
/// Used to read only the real, record-length portion of the AACS
|
||||
/// `MKB_RO.inf` — allocated to a fixed ~128 MiB and zero-padded — instead
|
||||
/// of reading 100+ MiB of padding (and tripping `MAX_FILE_BYTES`). The
|
||||
/// caller trims the returned prefix to the MKB record length.
|
||||
pub fn read_file_prefix(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
path: &str,
|
||||
max_bytes: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
self.read_file_limited(reader, path, Some(max_bytes))
|
||||
}
|
||||
|
||||
/// Shared implementation of [`read_file`] / [`read_file_prefix`]. When
|
||||
/// `max_bytes` is `Some`, reads at most that many bytes and the whole-file
|
||||
/// `MAX_FILE_BYTES` anti-DoS cap on the declared size / extent lengths is
|
||||
/// not applied (the read is already bounded by `max_bytes`).
|
||||
fn read_file_limited(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
path: &str,
|
||||
max_bytes: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||
let mut current = &self.root;
|
||||
|
||||
@@ -166,16 +203,34 @@ impl UdfFs {
|
||||
path: path.to_string(),
|
||||
})?;
|
||||
|
||||
// Read ALL the file's data extents. Multi-extent files (fragmented or
|
||||
// split across dual layers) would otherwise be silently truncated to
|
||||
// the first extent, since the buffer is sized to entry.size and
|
||||
// truncate() can't grow it.
|
||||
// `max_bytes == None` => read the whole file; `Some(n)` => read at most
|
||||
// n bytes (rounded up to a sector) and skip the anti-DoS caps below.
|
||||
let limit = max_bytes.unwrap_or(usize::MAX);
|
||||
|
||||
// Tiny files (notably the AACS `*.inf` key files) may store their data
|
||||
// embedded inline in the ICB (AD type 3), with no out-of-line extents.
|
||||
// Honor that before the extent path, which would otherwise misparse the
|
||||
// embedded bytes as allocation descriptors and (since 0.31.0) hard-error
|
||||
// on the resulting bogus extent length.
|
||||
if let Some(mut inline) = self.read_inline_data(reader, entry.meta_lba)? {
|
||||
let want = (entry.size as usize).min(limit);
|
||||
if inline.len() > want {
|
||||
inline.truncate(want);
|
||||
}
|
||||
return Ok(inline);
|
||||
}
|
||||
|
||||
// Read the file's data extents. Multi-extent files (fragmented or split
|
||||
// across dual layers) would otherwise be silently truncated to the
|
||||
// first extent, since the buffer is sized to entry.size and truncate()
|
||||
// can't grow it.
|
||||
let extents = self.read_icb_extents(reader, entry.meta_lba)?;
|
||||
|
||||
// Reject an oversized declared total before allocating: entry.size is
|
||||
// a raw u64 off the ICB, so a crafted file could otherwise force a
|
||||
// multi-hundred-MiB / GiB allocation across its extents.
|
||||
if entry.size > MAX_FILE_BYTES {
|
||||
// Reject an oversized declared total before allocating: entry.size is a
|
||||
// raw u64 off the ICB, so a crafted file could otherwise force a
|
||||
// multi-hundred-MiB / GiB allocation. Only for the UNBOUNDED path — a
|
||||
// bounded read is limited by `limit` regardless of the declared size.
|
||||
if max_bytes.is_none() && entry.size > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
@@ -183,33 +238,30 @@ impl UdfFs {
|
||||
});
|
||||
}
|
||||
|
||||
// Read ALL the file's data extents. File DATA is in the physical
|
||||
// partition (partition_start + lba), NOT the metadata partition: ICBs
|
||||
// are in metadata, data is in physical.
|
||||
let mut data = Vec::with_capacity(entry.size as usize);
|
||||
// Read the file DATA from the physical partition (partition_start +
|
||||
// lba), NOT the metadata partition: ICBs are in metadata, data is in
|
||||
// physical. Pre-allocate to the smaller of declared size and the
|
||||
// requested prefix (capped so a bogus entry.size can't reserve GiB).
|
||||
let cap_hint = (entry.size as usize)
|
||||
.min(limit)
|
||||
.min(MAX_FILE_BYTES as usize);
|
||||
let mut data = Vec::with_capacity(cap_hint);
|
||||
let mut sector = [0u8; 2048];
|
||||
for (data_lba, data_len) in extents {
|
||||
// Cumulative guard: entry.size and each per-extent data_len are
|
||||
// capped individually above, but a crafted ICB can chain many
|
||||
// small extents (read_icb_extents follows type-3 chains up to
|
||||
// MAX_AD_BLOCKS) whose running total grows `data` into GiB. Reject
|
||||
// once the accumulated bytes would exceed MAX_FILE_BYTES.
|
||||
if data.len() as u64 + data_len as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
}
|
||||
// data_len is the disc-controlled 30-bit extent length; reject an
|
||||
// oversized extent before reading so a crafted ICB can't grow the
|
||||
// buffer past MAX_FILE_BYTES.
|
||||
if data_len as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
'extents: for (data_lba, data_len) in extents {
|
||||
if max_bytes.is_none() {
|
||||
// Anti-DoS guards for the unbounded path: a crafted ICB can
|
||||
// chain many extents whose running total grows `data` into GiB,
|
||||
// or declare a single oversized extent. (Skipped when bounded —
|
||||
// `limit` already caps the read.)
|
||||
if data.len() as u64 + data_len as u64 > MAX_FILE_BYTES
|
||||
|| data_len as u64 > MAX_FILE_BYTES
|
||||
{
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
let abs_start = self
|
||||
.partition_start
|
||||
@@ -221,6 +273,9 @@ impl UdfFs {
|
||||
})?;
|
||||
let sector_count = (data_len as u64).div_ceil(2048) as u32;
|
||||
for i in 0..sector_count {
|
||||
if data.len() >= limit {
|
||||
break 'extents;
|
||||
}
|
||||
let abs = abs_start.checked_add(i).ok_or(Error::DiscRead {
|
||||
sector: abs_start as u64,
|
||||
status: None,
|
||||
@@ -231,10 +286,12 @@ impl UdfFs {
|
||||
}
|
||||
}
|
||||
|
||||
// Trim to the real file size; if extents under-covered the file (e.g.
|
||||
// sparse), leave what we have rather than over-reporting.
|
||||
if data.len() > entry.size as usize {
|
||||
data.truncate(entry.size as usize);
|
||||
// Trim to the real file size, or to the requested prefix — whichever is
|
||||
// smaller. If extents under-covered the file (e.g. sparse), leave what
|
||||
// we have rather than over-reporting.
|
||||
let trim_to = (entry.size as usize).min(limit);
|
||||
if data.len() > trim_to {
|
||||
data.truncate(trim_to);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
@@ -382,6 +439,54 @@ impl UdfFs {
|
||||
})
|
||||
}
|
||||
|
||||
/// If this ICB stores its file data INLINE (embedded — ICB Tag flags low
|
||||
/// 3 bits == 3) rather than via out-of-line extents, return the embedded
|
||||
/// bytes. Tiny files such as the AACS `*.inf` key files are routinely
|
||||
/// embedded directly in the ICB; `read_icb_extents` finds no real extents
|
||||
/// for them (it would misparse the embedded payload as allocation
|
||||
/// descriptors), so `read_file` must read the inline payload here. Returns
|
||||
/// `Ok(None)` for the normal extent-backed case.
|
||||
///
|
||||
/// (Regression guard: 0.31.0 added a per-extent `MAX_FILE_BYTES` cap that
|
||||
/// turned the misparsed-embedded case into a hard error, which surfaced as
|
||||
/// autorip "could not read this disc's key files" on discs whose AACS
|
||||
/// `.inf` files are ICB-embedded — the keyserver was then never called.)
|
||||
fn read_inline_data(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
meta_lba: u32,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let icb_abs = self.meta_to_abs(meta_lba)?;
|
||||
let mut icb = [0u8; 2048];
|
||||
read_sector(reader, icb_abs, &mut icb)?;
|
||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||
let (ad_offset, l_ad) = match tag {
|
||||
// Extended File Entry (266) / standard File Entry (261): the
|
||||
// allocation-descriptors field (which, for embedded files, holds
|
||||
// the data itself) begins after the extended attributes.
|
||||
266 => {
|
||||
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
||||
(216 + l_ea, l_ad)
|
||||
}
|
||||
261 => {
|
||||
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
||||
(176 + l_ea, l_ad)
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
// ICB Tag flags: u16 at absolute offset 34, low 3 bits select the AD
|
||||
// type. 3 == data embedded inline in the ICB.
|
||||
let icb_flags = u16::from_le_bytes([icb[34], icb[35]]);
|
||||
if (icb_flags & 0x07) != 3 {
|
||||
return Ok(None);
|
||||
}
|
||||
let start = ad_offset.min(icb.len());
|
||||
let end = ad_offset.saturating_add(l_ad).min(icb.len());
|
||||
Ok(Some(icb[start..end].to_vec()))
|
||||
}
|
||||
|
||||
/// Read ALL allocation extents for a file from its ICB.
|
||||
/// Returns Vec of (partition_relative_lba, byte_length) pairs.
|
||||
/// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents)
|
||||
|
||||
Reference in New Issue
Block a user