Compare commits
17
Commits
125a8e5bf0
...
122a03b23d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
122a03b23d | ||
|
|
dcc553a716 | ||
|
|
3b06a4c844 | ||
|
|
f9d112e481 | ||
|
|
f4fe651cb9 | ||
|
|
5ff04649ba | ||
|
|
84aaceceb7 | ||
|
|
cdee9739fd | ||
|
|
f55f11d043 | ||
|
|
f55d8f7acd | ||
|
|
31b0ba323a | ||
|
|
188baced39 | ||
|
|
bce11a2de0 | ||
|
|
7a79577343 | ||
|
|
f07251c2d4 | ||
|
|
eb23ab4586 | ||
|
|
61b6070f03 |
@@ -1,5 +1,57 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.2.2] — 2026-07-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **AACS 2.1 Media Key Variant support.** The Media Key Variant scheme is now
|
||||||
|
detected and parsed from the real MKB record types found on variant discs —
|
||||||
|
`0x2d` (Encrypted Media Key Variant Data), `0x2f` (Variant Key Data table,
|
||||||
|
65,535 × 16), and `0x0c` (variant cvalues, one per subset-difference slot) —
|
||||||
|
replacing the earlier placeholder `0x82`/`0x83` types, which were a guess and
|
||||||
|
appear on no real MKB. The V2.0→V2.1 upgrade detection and fixtures are updated
|
||||||
|
accordingly, so a genuine AACS 2.1 variant disc now resolves.
|
||||||
|
- **`resolve_candidate`** — one composed, pure-derivation boil-down for a
|
||||||
|
candidate key at any ladder rung (DK/PK/MK/VUK → terminal unit keys), parsing
|
||||||
|
`Unit_Key_RO.inf` at the disc's declared AACS version and returning every CPS
|
||||||
|
unit key. Consumers stop re-composing the ladder; every client hardens a single
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`mk_from_dk` does the real Subset-Difference walk again.** It previously ran
|
||||||
|
the Media-Key-Variant path, which needed an integrator KCD absent in-tree and
|
||||||
|
errored for every real disc — effectively dead for both consumers. It now
|
||||||
|
performs the genuine device-key SD walk; the Volume ID enters at the VUK step
|
||||||
|
(where it belongs), not the MK step. This revives the DK→MK fallback across the
|
||||||
|
toolchain (`freemkv-keysources` adopts the corrected two-argument call).
|
||||||
|
- **autorip: a down online key service is no longer reported as a missing key.**
|
||||||
|
When the online key source resolves no key for an encrypted disc, autorip now
|
||||||
|
runs one bounded reachability probe (SSRF-pinned, ~8 s, no redirects) and
|
||||||
|
distinguishes a transient outage (transport error / 502·503·504 → down; 429 →
|
||||||
|
rate-limited) from a genuine no-key (any real HTTP answer → up). A transient
|
||||||
|
verdict triggers a bounded key-resolution retry (3 attempts, 8/16/32 s backoff)
|
||||||
|
and, if the service stays down, parks the disc in a distinct retryable state
|
||||||
|
("Key service unavailable — temporary outage, not a missing key; will retry.")
|
||||||
|
instead of the permanent "no keys found". Never hammers the drive or service.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Processing-Key resolution is ~15× faster on UHD.** A Processing Key is the
|
||||||
|
key at its subset-difference node (one AES-G from the Media Key), so it is now
|
||||||
|
tried directly against the MKB cvalue tables (matching libaacs `_calc_mk_pks`)
|
||||||
|
instead of BFS-walking the SD tree at unknown depth — which was both wrong for
|
||||||
|
terminal PKs and slow on a large UHD MKB (~181k cvalues). PK derivation on UHD
|
||||||
|
drops from ~37 s to ~2.4 s; the SD tree walk now lives solely in the device-key
|
||||||
|
path.
|
||||||
|
|
||||||
|
### autorip
|
||||||
|
|
||||||
|
- **Clear stuck move errors from the System tab.** Each move-queue error now has
|
||||||
|
a ✕ to dismiss it, plus Clear all and Refresh — so a resolved or stale error
|
||||||
|
can be cleared without restarting the container (the mover re-records any that
|
||||||
|
are still genuinely failing on its next tick).
|
||||||
|
|
||||||
## [1.2.1] — 2026-07-02
|
## [1.2.1] — 2026-07-02
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
@@ -1,500 +0,0 @@
|
|||||||
//! AACS derivation "boil-down" — one public home for the key chain.
|
|
||||||
//!
|
|
||||||
//! Thin newtypes at the API boundary and three wrapper functions over the
|
|
||||||
//! existing crypto. Nothing here re-implements a primitive: every function
|
|
||||||
//! delegates to the already-audited code in [`super::keys`] and
|
|
||||||
//! [`super::variants`], so the boil-down cannot drift from production math.
|
|
||||||
//!
|
|
||||||
//! The newtypes wrap bare `[u8; 16]` ONLY at this boundary — the crypto
|
|
||||||
//! internals continue to operate on raw arrays. They exist so a caller threads
|
|
||||||
//! the chain `DK → MK → VUK → UK` without confusing one 16-byte secret for
|
|
||||||
//! another, not to refactor the resolver.
|
|
||||||
//!
|
|
||||||
//! Chain (matches `aacs::keys::resolve_keys_classical` path 1 and
|
|
||||||
//! `aacs::keys::resolve_keys_v21` path 1 byte-for-byte):
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! mk_from_dk(device_keys, mkb, vid) → MediaKey (Km)
|
|
||||||
//! mk_from_pk(processing_keys, mkb) → MediaKey (Km)
|
|
||||||
//! vuk_from_mk(MediaKey, Vid) → Vuk (= AES-G(Km, VID))
|
|
||||||
//! uk_from_vuk(Vuk, enc_title_keys) → [UnitKey] (decrypt_unit_key each)
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! `mk_from_dk` and `mk_from_pk` are two entry points to the SAME Media Key,
|
|
||||||
//! both via the MKB's Subset-Difference cvalue tables: the device-key path
|
|
||||||
//! recovers its Processing Key at the matching SD node and walks on to the MK;
|
|
||||||
//! the processing-key path starts from a precomputed PK. Neither needs a VID
|
|
||||||
//! (the VID enters at `vuk_from_mk`).
|
|
||||||
|
|
||||||
use super::keys::{
|
|
||||||
decrypt_unit_key, derive_media_key_and_pk_from_dk, derive_media_key_from_pk, derive_vuk,
|
|
||||||
};
|
|
||||||
use super::types::DeviceKey;
|
|
||||||
|
|
||||||
/// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Vid(pub [u8; 16]);
|
|
||||||
|
|
||||||
/// Media Key (Km, 16 bytes) — the MKB-scoped key derived from device keys.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct MediaKey(pub [u8; 16]);
|
|
||||||
|
|
||||||
/// Volume Unique Key (VUK / Kvu, 16 bytes) — derived from `MediaKey` + `Vid`,
|
|
||||||
/// decrypts the per-disc encrypted title keys in `Unit_Key_RO.inf`.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Vuk(pub [u8; 16]);
|
|
||||||
|
|
||||||
/// Processing Key (Kp, 16 bytes) — an MKB Subset-Difference key that yields the
|
|
||||||
/// Media Key. A leaked/precomputed PK in the keydb, or the intermediate PK a
|
|
||||||
/// device-key walk derives at its matching SD node.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct ProcessingKey(pub [u8; 16]);
|
|
||||||
|
|
||||||
/// One decrypted per-CPS-unit AACS title key.
|
|
||||||
///
|
|
||||||
/// `idx` is the POSITIONAL index of the encrypted title key within the slice
|
|
||||||
/// handed to [`uk_from_vuk`] (i.e. its order in `Unit_Key_RO.inf`'s key-storage
|
|
||||||
/// area). The CPS-unit *number* association (the `u32` in
|
|
||||||
/// `ResolvedKeys::unit_keys`) is a higher-level concern owned by
|
|
||||||
/// [`super::keys::parse_unit_key_ro`], which pairs each positional key with its
|
|
||||||
/// declared CPS unit; this primitive only does the AES, so it surfaces position.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct UnitKey {
|
|
||||||
pub idx: u32,
|
|
||||||
pub key: [u8; 16],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive the Volume Unique Key from a Media Key and Volume ID.
|
|
||||||
///
|
|
||||||
/// Wraps [`derive_vuk`] verbatim: `VUK = AES-128-ECB-DECRYPT(MK, VID) XOR VID`.
|
|
||||||
/// This is byte-identical to the inline `derive_vuk(&mk, ctx.volume_id)` call in
|
|
||||||
/// every classical resolver path AND to the `Kvu = AES-G(Km, VID)` step inside
|
|
||||||
/// [`derive_media_key_variant`] (AES-G and `derive_vuk` are the same math), so
|
|
||||||
/// `vuk_from_mk(mk_from_dk(..)?, vid)` reproduces the V21 variant VUK exactly.
|
|
||||||
pub fn vuk_from_mk(mk: MediaKey, vid: Vid) -> Vuk {
|
|
||||||
Vuk(derive_vuk(&mk.0, &vid.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypt the disc's encrypted title keys with a VUK.
|
|
||||||
///
|
|
||||||
/// Wraps [`decrypt_unit_key`] (AES-128-ECB-DECRYPT) per entry, mirroring the
|
|
||||||
/// `derive_uks` closure in `resolve_keys_classical` / `resolve_keys_v21`. The
|
|
||||||
/// returned `UnitKey::idx` is the slice position; pair with CPS-unit numbers via
|
|
||||||
/// [`super::keys::parse_unit_key_ro`] when the numbering matters.
|
|
||||||
pub fn uk_from_vuk(vuk: Vuk, enc_title_keys: &[[u8; 16]]) -> Vec<UnitKey> {
|
|
||||||
enc_title_keys
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, enc)| UnitKey {
|
|
||||||
idx: i as u32,
|
|
||||||
key: decrypt_unit_key(&vuk.0, enc),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive the Media Key (Km) from positioned device keys via the MKB's
|
|
||||||
/// Subset-Difference tables.
|
|
||||||
///
|
|
||||||
/// Wraps [`derive_media_key_and_pk_from_dk`] — the real SD walk the resolver
|
|
||||||
/// runs: each positioned device key is placed against the MKB's subset-diff /
|
|
||||||
/// cvalue records, recovering its Processing Key at the matching node and
|
|
||||||
/// continuing to the Media Key. Reachable for real discs whenever a device key
|
|
||||||
/// applies to the MKB. No VID is involved here — it enters at [`vuk_from_mk`].
|
|
||||||
///
|
|
||||||
/// Returns [`Error::AacsMkUnavailable`] (E7018) when no supplied device key
|
|
||||||
/// resolves the MKB — the same terminal error as [`mk_from_pk`]; no numeric
|
|
||||||
/// distinction is load-bearing at this boundary.
|
|
||||||
///
|
|
||||||
/// [`Error::AacsMkUnavailable`]: crate::error::Error::AacsMkUnavailable
|
|
||||||
pub fn mk_from_dk(device_keys: &[DeviceKey], mkb: &[u8]) -> Result<MediaKey, crate::error::Error> {
|
|
||||||
// Positioned device keys drive the real Subset-Difference MKB walk
|
|
||||||
// ([`derive_media_key_and_pk_from_dk`], the same walk the resolver runs). The
|
|
||||||
// old Media-Key-Variant path needed integrator Key Correction Data absent
|
|
||||||
// in-tree, so it Err'd for EVERY real disc (dead for both consumers —
|
|
||||||
// freemkv-keysources' DK fallback and the kdb harvester). No VID is needed
|
|
||||||
// for the Media Key; it enters only at [`vuk_from_mk`].
|
|
||||||
match derive_media_key_and_pk_from_dk(mkb, device_keys) {
|
|
||||||
Some((km, _pk)) => Ok(MediaKey(km)),
|
|
||||||
None => Err(crate::error::Error::AacsMkUnavailable),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive the Media Key (Km) from one or more Processing Keys and the disc MKB.
|
|
||||||
///
|
|
||||||
/// Wraps [`derive_media_key_from_pk`] — the Subset-Difference PK→MK walk: each
|
|
||||||
/// processing key is validated (and tree-walked) against the MKB's cvalue tables
|
|
||||||
/// (records `0x04`/`0x05`) until one yields the Media Key whose verify record
|
|
||||||
/// (`0x81`/`0x86`) matches. Unlike [`mk_from_dk`] this path is reachable for
|
|
||||||
/// real discs — a leaked/precomputed AACS Processing Key in the keydb resolves
|
|
||||||
/// the Media Key directly. No VID is involved at this step; the VID enters at
|
|
||||||
/// [`vuk_from_mk`].
|
|
||||||
///
|
|
||||||
/// Returns [`Error::AacsMkUnavailable`] (E7018) when no processing key resolves
|
|
||||||
/// the MKB — the same terminal error as [`mk_from_dk`]; no numeric distinction
|
|
||||||
/// is load-bearing at this boundary.
|
|
||||||
///
|
|
||||||
/// [`Error::AacsMkUnavailable`]: crate::error::Error::AacsMkUnavailable
|
|
||||||
pub fn mk_from_pk(
|
|
||||||
processing_keys: &[[u8; 16]],
|
|
||||||
mkb: &[u8],
|
|
||||||
) -> Result<MediaKey, crate::error::Error> {
|
|
||||||
match derive_media_key_from_pk(mkb, processing_keys) {
|
|
||||||
Some(km) => Ok(MediaKey(km)),
|
|
||||||
None => Err(crate::error::Error::AacsMkUnavailable),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A candidate key at any rung of the AACS ladder, handed to [`resolve_candidate`].
|
|
||||||
///
|
|
||||||
/// Each variant carries the module's existing newtype for that rung (a `Dk` is a
|
|
||||||
/// POSITIONED [`DeviceKey`] — recover an unpositioned one with
|
|
||||||
/// [`super::keys::recover_dk_position`] first).
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum KeyCandidate {
|
|
||||||
Uk(UnitKey),
|
|
||||||
Vuk(Vuk),
|
|
||||||
Mk(MediaKey),
|
|
||||||
Pk(ProcessingKey),
|
|
||||||
Dk(DeviceKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The AACS key chain derived from a candidate, from [`resolve_candidate`].
|
|
||||||
///
|
|
||||||
/// PURE DERIVATION — no unit sampling, no validation. `unit_keys` holds every
|
|
||||||
/// CPS-unit key the disc's `Unit_Key_RO.inf` yields from the VUK (positional
|
|
||||||
/// order); the caller runs [`super::decrypt::unit_key_validates`] to find which
|
|
||||||
/// one actually opens the disc. Rungs above the candidate are `None` (a `Vuk`
|
|
||||||
/// candidate has no `mk`/`pk`/`dk`; a `Uk` candidate has only `unit_keys`).
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ResolvedChain {
|
|
||||||
/// Every unit key derived from the VUK, as `(cps_unit_number, key)` — the
|
|
||||||
/// CPS-unit numbers come from `Unit_Key_RO.inf` (via `parse_unit_key_ro`), so
|
|
||||||
/// a consumer maps `UK → CPS unit` directly. Same shape as
|
|
||||||
/// [`super::keys::ResolvedKeys::unit_keys`]. A `Uk` candidate yields exactly
|
|
||||||
/// itself, keyed by its own `idx`.
|
|
||||||
pub unit_keys: Vec<(u32, [u8; 16])>,
|
|
||||||
pub vuk: Option<Vuk>,
|
|
||||||
pub mk: Option<MediaKey>,
|
|
||||||
pub pk: Option<ProcessingKey>,
|
|
||||||
/// The positioned device key (for a `Dk` candidate).
|
|
||||||
pub dk: Option<DeviceKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derive the full AACS key chain from a candidate key of ANY ladder rung.
|
|
||||||
///
|
|
||||||
/// Runs the deterministic derivation DOWNWARD to the disc's terminal unit keys:
|
|
||||||
/// `DK → MK → VUK → UKs`, `PK → MK → VUK → UKs`, `MK → VUK → UKs`,
|
|
||||||
/// `VUK → UKs`, or `UK → itself`. Composes the module's own boil steps
|
|
||||||
/// ([`mk_from_pk`], [`vuk_from_mk`], [`uk_from_vuk`]) and parses
|
|
||||||
/// `Unit_Key_RO.inf` at the version the disc's MKB declares (48-byte stride for
|
|
||||||
/// AACS-1.0, 64 for AACS-2.x), so a multi-CPS disc yields all its unit keys from
|
|
||||||
/// the one candidate.
|
|
||||||
///
|
|
||||||
/// PURE DERIVATION: no sampling, no validation, no position recovery. Every step
|
|
||||||
/// is deterministic AES, so the returned keys are only as sound as the input
|
|
||||||
/// candidate — validate `unit_keys` against a real encrypted unit with
|
|
||||||
/// [`super::decrypt::unit_key_validates`] to prove the candidate opens the disc.
|
|
||||||
///
|
|
||||||
/// Returns `None` only when derivation itself cannot proceed: a PK its MKB
|
|
||||||
/// rejects, a `Dk` the MKB can't process, a missing VID on a path that needs
|
|
||||||
/// one, or an unparseable/empty `Unit_Key_RO.inf`.
|
|
||||||
pub fn resolve_candidate(
|
|
||||||
candidate: &KeyCandidate,
|
|
||||||
mkb: &[u8],
|
|
||||||
unit_key_ro: &[u8],
|
|
||||||
vid: Option<Vid>,
|
|
||||||
) -> Option<ResolvedChain> {
|
|
||||||
use super::keys::{AacsVersion, derive_media_key_and_pk_from_dk, mkb_type, parse_unit_key_ro};
|
|
||||||
|
|
||||||
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number.
|
|
||||||
// `.inf` parsing lives here: derive the stride version from the disc's own
|
|
||||||
// MKB, then defer the VUK→unit-keys step to the shared `derive_unit_keys`
|
|
||||||
// (the one place both resolvers and this path decrypt the title keys).
|
|
||||||
let boil = |vuk: Vuk| -> Option<Vec<(u32, [u8; 16])>> {
|
|
||||||
let version = mkb_type(mkb)
|
|
||||||
.map(|t| t.generation())
|
|
||||||
.unwrap_or(AacsVersion::V10);
|
|
||||||
let ukf = parse_unit_key_ro(unit_key_ro, version)?;
|
|
||||||
if ukf.encrypted_keys.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(super::keys::derive_unit_keys(&ukf, &vuk.0))
|
|
||||||
};
|
|
||||||
|
|
||||||
match candidate {
|
|
||||||
KeyCandidate::Uk(uk) => Some(ResolvedChain {
|
|
||||||
unit_keys: vec![(uk.idx, uk.key)],
|
|
||||||
vuk: None,
|
|
||||||
mk: None,
|
|
||||||
pk: None,
|
|
||||||
dk: None,
|
|
||||||
}),
|
|
||||||
KeyCandidate::Vuk(v) => Some(ResolvedChain {
|
|
||||||
unit_keys: boil(*v)?,
|
|
||||||
vuk: Some(*v),
|
|
||||||
mk: None,
|
|
||||||
pk: None,
|
|
||||||
dk: None,
|
|
||||||
}),
|
|
||||||
KeyCandidate::Mk(mk) => {
|
|
||||||
let vuk = vuk_from_mk(*mk, vid?);
|
|
||||||
Some(ResolvedChain {
|
|
||||||
unit_keys: boil(vuk)?,
|
|
||||||
vuk: Some(vuk),
|
|
||||||
mk: Some(*mk),
|
|
||||||
pk: None,
|
|
||||||
dk: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
KeyCandidate::Pk(pk) => {
|
|
||||||
let mk = mk_from_pk(std::slice::from_ref(&pk.0), mkb).ok()?;
|
|
||||||
let vuk = vuk_from_mk(mk, vid?);
|
|
||||||
Some(ResolvedChain {
|
|
||||||
unit_keys: boil(vuk)?,
|
|
||||||
vuk: Some(vuk),
|
|
||||||
mk: Some(mk),
|
|
||||||
pk: Some(*pk),
|
|
||||||
dk: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
KeyCandidate::Dk(dk) => {
|
|
||||||
let (km, pk) = derive_media_key_and_pk_from_dk(mkb, std::slice::from_ref(dk))?;
|
|
||||||
let mk = MediaKey(km);
|
|
||||||
let vuk = vuk_from_mk(mk, vid?);
|
|
||||||
Some(ResolvedChain {
|
|
||||||
unit_keys: boil(vuk)?,
|
|
||||||
vuk: Some(vuk),
|
|
||||||
mk: Some(mk),
|
|
||||||
pk: Some(ProcessingKey(pk)),
|
|
||||||
dk: Some(dk.clone()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::aacs::decrypt::aes_ecb_encrypt;
|
|
||||||
use crate::aacs::keys::{decrypt_unit_key, derive_vuk};
|
|
||||||
|
|
||||||
/// `vuk_from_mk` must equal the inline `derive_vuk` path bit-for-bit, for
|
|
||||||
/// several known (MK, VID) vectors.
|
|
||||||
#[test]
|
|
||||||
fn vuk_from_mk_matches_inline_derive_vuk() {
|
|
||||||
let cases: [([u8; 16], [u8; 16]); 3] = [
|
|
||||||
([0x5A; 16], [0xA5; 16]),
|
|
||||||
([0x11; 16], [0x22; 16]),
|
|
||||||
(
|
|
||||||
[
|
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
|
|
||||||
0x0D, 0x0E, 0x0F,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B, 0x3C,
|
|
||||||
0x2D, 0x1E, 0x0F,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
for (mk, vid) in cases {
|
|
||||||
let inline = derive_vuk(&mk, &vid);
|
|
||||||
let boiled = vuk_from_mk(MediaKey(mk), Vid(vid));
|
|
||||||
assert_eq!(boiled.0, inline, "vuk_from_mk must equal derive_vuk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `uk_from_vuk` must equal the inline `decrypt_unit_key` path bit-for-bit
|
|
||||||
/// and carry positional indices 0..n. Built by encrypting known plaintext
|
|
||||||
/// title keys under the VUK (the same primitive the resolver inverts).
|
|
||||||
#[test]
|
|
||||||
fn uk_from_vuk_matches_inline_decrypt_unit_key() {
|
|
||||||
let vuk = [0x5Au8; 16];
|
|
||||||
let plain_keys = [[0x11u8; 16], [0x22u8; 16], [0xCDu8; 16]];
|
|
||||||
let enc: Vec<[u8; 16]> = plain_keys
|
|
||||||
.iter()
|
|
||||||
.map(|k| aes_ecb_encrypt(&vuk, k))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let boiled = uk_from_vuk(Vuk(vuk), &enc);
|
|
||||||
assert_eq!(boiled.len(), enc.len());
|
|
||||||
for (i, uk) in boiled.iter().enumerate() {
|
|
||||||
assert_eq!(uk.idx, i as u32, "idx must be the positional index");
|
|
||||||
// Matches the inline derive_uks closure: decrypt_unit_key(vuk, enc).
|
|
||||||
assert_eq!(uk.key, decrypt_unit_key(&vuk, &enc[i]));
|
|
||||||
// And recovers the original plaintext title key.
|
|
||||||
assert_eq!(
|
|
||||||
uk.key, plain_keys[i],
|
|
||||||
"VUK roundtrip recovers the title key"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `uk_from_vuk` on an empty slice yields no keys (no panic, no phantom idx).
|
|
||||||
#[test]
|
|
||||||
fn uk_from_vuk_empty_is_empty() {
|
|
||||||
assert!(uk_from_vuk(Vuk([0u8; 16]), &[]).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `mk_from_dk` returns `Err(AacsMkUnavailable)` when the MKB has no
|
|
||||||
/// processable Subset-Difference tables (empty MKB, or one with no
|
|
||||||
/// mk_dv/cvalues/subdiff records) — never a wrong key, never a panic.
|
|
||||||
#[test]
|
|
||||||
fn mk_from_dk_errors_on_unprocessable_mkb() {
|
|
||||||
let dk = DeviceKey {
|
|
||||||
key: [0x11; 16],
|
|
||||||
node: 1,
|
|
||||||
uv: 1,
|
|
||||||
u_mask_shift: 0,
|
|
||||||
};
|
|
||||||
// Empty MKB → no SD records to walk → Err.
|
|
||||||
let e = mk_from_dk(std::slice::from_ref(&dk), &[]);
|
|
||||||
assert!(matches!(e, Err(crate::error::Error::AacsMkUnavailable)));
|
|
||||||
|
|
||||||
// An MKB with no complete Subset-Difference tables (mk_dv / cvalues /
|
|
||||||
// subdiff) cannot yield a Media Key, so the real walk also errors —
|
|
||||||
// never silently yields a key.
|
|
||||||
let mut mkb: Vec<u8> = Vec::new();
|
|
||||||
mkb.extend_from_slice(&[0x82, 0x00, 0x00, 0x14]); // stray data record only
|
|
||||||
mkb.extend_from_slice(&[0xAB; 16]);
|
|
||||||
let e2 = mk_from_dk(&[dk], &mkb);
|
|
||||||
assert!(matches!(e2, Err(crate::error::Error::AacsMkUnavailable)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a 4-byte MKB record header (type + 3-byte big-endian total length,
|
|
||||||
/// header included) and append `body`. Mirrors the MKB record framing the
|
|
||||||
/// parser expects; no crypto.
|
|
||||||
fn mkb_record(rec_type: u8, body: &[u8]) -> Vec<u8> {
|
|
||||||
let total = 4 + body.len();
|
|
||||||
let mut rec = Vec::with_capacity(total);
|
|
||||||
rec.push(rec_type);
|
|
||||||
rec.push(((total >> 16) & 0xFF) as u8);
|
|
||||||
rec.push(((total >> 8) & 0xFF) as u8);
|
|
||||||
rec.push((total & 0xFF) as u8);
|
|
||||||
rec.extend_from_slice(body);
|
|
||||||
rec
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `mk_from_pk` resolves a planted Processing Key against a synthetic MKB and
|
|
||||||
/// drives the FULL boil chain PK → MK → VUK → UK. The MKB is built with the
|
|
||||||
/// same (pk, cv, mk_dv, uv) construction the production SD walk validates, so
|
|
||||||
/// this proves a PK entry yields real Unit Keys — not just an `Ok`.
|
|
||||||
#[test]
|
|
||||||
fn mk_from_pk_drives_full_chain_to_uks() {
|
|
||||||
let pk: [u8; 16] = [
|
|
||||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
|
||||||
0xFF, 0x00,
|
|
||||||
];
|
|
||||||
let mk: [u8; 16] = [
|
|
||||||
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
|
|
||||||
0xAE, 0xAF,
|
|
||||||
];
|
|
||||||
let uv: [u8; 4] = [0x00, 0x00, 0x04, 0x00];
|
|
||||||
|
|
||||||
// cv = AES-E(pk, mk_raw), where mk_raw is mk with the last-4-bytes-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[a];
|
|
||||||
}
|
|
||||||
let cv = aes_ecb_encrypt(&pk, &mk_raw);
|
|
||||||
|
|
||||||
// mk_dv = AES-E(mk, magic||pad): AES-D(mk, mk_dv) starts with the AACS
|
|
||||||
// verify sentinel.
|
|
||||||
let mut vd = [0x11u8; 16];
|
|
||||||
vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
|
|
||||||
let mk_dv = aes_ecb_encrypt(&mk, &vd);
|
|
||||||
|
|
||||||
// Synthetic MKB: type/version (0x10), verify record (0x86 = mk_dv),
|
|
||||||
// one-entry SD index (0x04 = [u_mask_shift=0][uv]), one-entry cvalue
|
|
||||||
// table (0x05 = cv).
|
|
||||||
let mut sd = vec![0u8];
|
|
||||||
sd.extend_from_slice(&uv);
|
|
||||||
let mut mkb = Vec::new();
|
|
||||||
mkb.extend_from_slice(&mkb_record(0x10, &[0, 0, 0, 0x20, 0, 0, 0, 0x52]));
|
|
||||||
mkb.extend_from_slice(&mkb_record(0x86, &mk_dv));
|
|
||||||
mkb.extend_from_slice(&mkb_record(0x04, &sd));
|
|
||||||
mkb.extend_from_slice(&mkb_record(0x05, &cv));
|
|
||||||
|
|
||||||
// PK → MK.
|
|
||||||
let got_mk = mk_from_pk(std::slice::from_ref(&pk), &mkb).expect("planted PK resolves MK");
|
|
||||||
assert_eq!(got_mk, MediaKey(mk), "mk_from_pk recovers the planted MK");
|
|
||||||
|
|
||||||
// MK → VUK → UK over an encrypted title key.
|
|
||||||
let vid = Vid([0x42u8; 16]);
|
|
||||||
let plain_uk = [0x7Eu8; 16];
|
|
||||||
let vuk = vuk_from_mk(got_mk, vid);
|
|
||||||
let enc = aes_ecb_encrypt(&vuk.0, &plain_uk);
|
|
||||||
let uks = uk_from_vuk(vuk, std::slice::from_ref(&enc));
|
|
||||||
assert_eq!(uks.len(), 1);
|
|
||||||
assert_eq!(uks[0].key, plain_uk, "PK chain recovers the title key");
|
|
||||||
|
|
||||||
// A corrupt PK resolves nothing.
|
|
||||||
let mut bad = pk;
|
|
||||||
bad[0] ^= 0xFF;
|
|
||||||
assert!(matches!(
|
|
||||||
mk_from_pk(std::slice::from_ref(&bad), &mkb),
|
|
||||||
Err(crate::error::Error::AacsMkUnavailable)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimal AACS-1.0 (48-byte stride) `Unit_Key_RO.inf` with `n` encrypted
|
|
||||||
/// unit keys — `parse_unit_key_ro` numbers CPS units 1..=n.
|
|
||||||
fn synth_inf(encs: &[[u8; 16]]) -> Vec<u8> {
|
|
||||||
let uk_pos = 32usize;
|
|
||||||
let stride = 48usize;
|
|
||||||
let n = encs.len();
|
|
||||||
let total = uk_pos + 48 + n.saturating_sub(1) * stride + 16;
|
|
||||||
let mut inf = vec![0u8; total.max(20)];
|
|
||||||
inf[..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
|
|
||||||
inf[uk_pos..uk_pos + 2].copy_from_slice(&(n as u16).to_be_bytes());
|
|
||||||
for (i, k) in encs.iter().enumerate() {
|
|
||||||
let o = uk_pos + 48 + i * stride;
|
|
||||||
inf[o..o + 16].copy_from_slice(k);
|
|
||||||
}
|
|
||||||
inf
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A VUK candidate boils to ALL the disc's unit keys, each paired with its
|
|
||||||
/// declared CPS-unit number, and each key equals the VUK-decrypt of its slot.
|
|
||||||
#[test]
|
|
||||||
fn resolve_candidate_vuk_returns_all_cps_units() {
|
|
||||||
let vuk = Vuk([0x33u8; 16]);
|
|
||||||
let encs = [[0x11u8; 16], [0x22u8; 16], [0x44u8; 16]];
|
|
||||||
let inf = synth_inf(&encs);
|
|
||||||
let r = resolve_candidate(&KeyCandidate::Vuk(vuk), &[], &inf, None).expect("vuk derives");
|
|
||||||
let cps: Vec<u32> = r.unit_keys.iter().map(|(c, _)| *c).collect();
|
|
||||||
assert_eq!(
|
|
||||||
cps,
|
|
||||||
vec![1, 2, 3],
|
|
||||||
"every CPS unit surfaced, numbered from the inf"
|
|
||||||
);
|
|
||||||
for ((_, key), enc) in r.unit_keys.iter().zip(encs.iter()) {
|
|
||||||
assert_eq!(
|
|
||||||
*key,
|
|
||||||
decrypt_unit_key(&vuk.0, enc),
|
|
||||||
"key = VUK-decrypt of its slot"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert_eq!(r.vuk, Some(vuk));
|
|
||||||
assert!(r.mk.is_none() && r.pk.is_none() && r.dk.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A bare UK candidate is terminal — it returns itself keyed by its own idx.
|
|
||||||
#[test]
|
|
||||||
fn resolve_candidate_uk_is_itself() {
|
|
||||||
let uk = UnitKey {
|
|
||||||
idx: 2,
|
|
||||||
key: [0x9u8; 16],
|
|
||||||
};
|
|
||||||
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &[], None).expect("uk is terminal");
|
|
||||||
assert_eq!(r.unit_keys, vec![(2, uk.key)]);
|
|
||||||
assert!(r.vuk.is_none() && r.mk.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MK/PK/DK paths derive the VUK from a VID; without one, derivation stops.
|
|
||||||
#[test]
|
|
||||||
fn resolve_candidate_mk_requires_vid() {
|
|
||||||
let r = resolve_candidate(&KeyCandidate::Mk(MediaKey([1u8; 16])), &[], &[], None);
|
|
||||||
assert!(r.is_none(), "MK path returns None without a VID");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
//! AACS content decryption — AES primitives, unit decryption, bus encryption.
|
//! AACS content decryption — aligned-unit / bus decryption and TS verification.
|
||||||
|
//! The low-level AES primitives it uses live in [`super::crypto`].
|
||||||
|
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray};
|
||||||
|
|
||||||
|
use super::crypto::{aes_cbc_decrypt, aes_ecb_encrypt};
|
||||||
|
// Available at module scope for this module's test fixtures (they reference
|
||||||
|
// `super::AACS_IV` when building CBC ciphertext directly); test-only.
|
||||||
|
#[cfg(test)]
|
||||||
|
use super::crypto::AACS_IV;
|
||||||
|
|
||||||
// ── AACS constants ──────────────────────────────────────────────────────────
|
// ── AACS constants ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Fixed IV used by AACS for all AES-CBC operations.
|
/// Size of an AACS aligned unit (3 × 2048-byte sectors). [BD] §3.10.1.
|
||||||
pub(crate) const AACS_IV: [u8; 16] = [
|
|
||||||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
|
|
||||||
pub const ALIGNED_UNIT_LEN: usize = 6144;
|
pub const ALIGNED_UNIT_LEN: usize = 6144;
|
||||||
|
|
||||||
/// An AACS aligned unit spans this many 2048-byte sectors (3).
|
/// An AACS aligned unit spans this many 2048-byte sectors (3).
|
||||||
@@ -47,58 +49,6 @@ use crate::consts::BD_SOURCE_PACKET_BYTES;
|
|||||||
/// TS sync byte.
|
/// TS sync byte.
|
||||||
const TS_SYNC: u8 = 0x47;
|
const TS_SYNC: u8 = 0x47;
|
||||||
|
|
||||||
// ── AES primitives ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// AES-128-ECB encrypt a single 16-byte block.
|
|
||||||
pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
|
||||||
let mut block = GenericArray::clone_from_slice(data);
|
|
||||||
cipher.encrypt_block(&mut block);
|
|
||||||
let mut out = [0u8; 16];
|
|
||||||
out.copy_from_slice(&block);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AES-128-ECB decrypt a single 16-byte block.
|
|
||||||
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
|
||||||
let mut block = GenericArray::clone_from_slice(data);
|
|
||||||
cipher.decrypt_block(&mut block);
|
|
||||||
let mut out = [0u8; 16];
|
|
||||||
out.copy_from_slice(&block);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AES-128-CBC decrypt in-place with the fixed AACS IV.
|
|
||||||
///
|
|
||||||
/// Precondition: `data.len()` is a multiple of 16. Any trailing partial
|
|
||||||
/// block is silently ignored; all callers pass aligned regions (6128 and
|
|
||||||
/// 2032 bytes), and the assert documents/enforces that contract.
|
|
||||||
pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
|
|
||||||
debug_assert!(
|
|
||||||
data.len() % 16 == 0,
|
|
||||||
"aes_cbc_decrypt requires a block-aligned slice"
|
|
||||||
);
|
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
|
||||||
let num_blocks = data.len() / 16;
|
|
||||||
// Process blocks in reverse to avoid clobbering ciphertext needed for XOR
|
|
||||||
for i in (0..num_blocks).rev() {
|
|
||||||
let offset = i * 16;
|
|
||||||
let prev = if i == 0 {
|
|
||||||
AACS_IV
|
|
||||||
} else {
|
|
||||||
let mut p = [0u8; 16];
|
|
||||||
p.copy_from_slice(&data[(i - 1) * 16..i * 16]);
|
|
||||||
p
|
|
||||||
};
|
|
||||||
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
|
|
||||||
cipher.decrypt_block(&mut block);
|
|
||||||
for j in 0..16 {
|
|
||||||
data[offset + j] = block[j] ^ prev[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Content decryption ──────────────────────────────────────────────────────
|
// ── Content decryption ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// True if a 6144-byte aligned unit's MPEG-TS sync structure is DESTROYED — it
|
/// True if a 6144-byte aligned unit's MPEG-TS sync structure is DESTROYED — it
|
||||||
@@ -127,7 +77,7 @@ pub fn ts_sync_destroyed(unit: &[u8]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The AUTHORITATIVE AACS "is this aligned unit encrypted?" signal — the Copy
|
/// The AUTHORITATIVE AACS "is this aligned unit encrypted?" signal — the Copy
|
||||||
/// Permission Indicator (CPI) in the top 2 bits of byte 0. Byte 0 is the first
|
/// Permission Indicator (CPI) in the top 2 bits of byte 0. [BD] §3.10.2. Byte 0 is the first
|
||||||
/// byte of the first source packet's `TP_extra_header`, which AACS always leaves
|
/// byte of the first source packet's `TP_extra_header`, which AACS always leaves
|
||||||
/// in the clear (the first 16 bytes of every unit are the unencrypted SEED). So
|
/// in the clear (the first 16 bytes of every unit are the unencrypted SEED). So
|
||||||
/// this is readable WITHOUT a key:
|
/// this is readable WITHOUT a key:
|
||||||
@@ -385,7 +335,10 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save original first 16 bytes (the plaintext seed / header) and derive the
|
// Save original first 16 bytes (the plaintext seed / header) and derive the
|
||||||
// per-unit decrypt key (identical to `decrypt_unit_checked`).
|
// per-unit Block Key (identical to `decrypt_unit_checked`).
|
||||||
|
// Block Key = AES-128E(Kcu, seed) ⊕ seed ([BD] §3.10.1 Fig 3-8, two-node
|
||||||
|
// construction: encrypt the clear seed under the CPS Unit Key, then XOR the
|
||||||
|
// seed back in — the trailing ⊕seed is load-bearing).
|
||||||
let mut header = [0u8; 16];
|
let mut header = [0u8; 16];
|
||||||
header.copy_from_slice(&unit[..16]);
|
header.copy_from_slice(&unit[..16]);
|
||||||
let derived = aes_ecb_encrypt(unit_key, &header);
|
let derived = aes_ecb_encrypt(unit_key, &header);
|
||||||
@@ -393,6 +346,7 @@ pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
|||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
decrypt_key[i] = derived[i] ^ header[i];
|
decrypt_key[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
|
// Final 6128 bytes of the aligned unit under the Block Key; first 16 = clear seed. [BD] §3.10.1.
|
||||||
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
|
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
|
||||||
|
|
||||||
// Verify content packets; zero out padding packets (their decrypted bytes are
|
// Verify content packets; zero out padding packets (their decrypted bytes are
|
||||||
@@ -583,7 +537,9 @@ pub fn decrypt_unit_full(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::super::crypto::aes_ecb_decrypt;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use aes::cipher::BlockEncrypt; // test fixtures build ciphertext directly
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_aes_ecb_roundtrip() {
|
fn test_aes_ecb_roundtrip() {
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//! AACS common cryptographic primitives — [C] Chapter 2 / §3.2.2.
|
||||||
|
//!
|
||||||
|
//! Source: `[C]` = AACS Introduction and Common Cryptographic Elements Book,
|
||||||
|
//! Rev 0.953. The shared low-level building blocks — AES-128 ECB E/D, AES-G,
|
||||||
|
//! the AES-G3 Triple Generator, AES-CBC decrypt — and their fixed constants
|
||||||
|
//! (`iv0`, `s0`). Used by every AACS generation; relocated here so the
|
||||||
|
//! primitives live in one place instead of being scattered across the
|
||||||
|
//! content / keys / variant modules.
|
||||||
|
|
||||||
|
use aes::Aes128;
|
||||||
|
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
|
|
||||||
|
/// Fixed IV used by AACS for all AES-CBC operations. [C] §2.1.2 (default CBC IV, `iv0`).
|
||||||
|
pub(crate) const AACS_IV: [u8; 16] = [
|
||||||
|
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// AES-128-ECB encrypt a single 16-byte block. [C] §2.1.1 (`AES-128E`).
|
||||||
|
pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||||
|
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||||
|
let mut block = GenericArray::clone_from_slice(data);
|
||||||
|
cipher.encrypt_block(&mut block);
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out.copy_from_slice(&block);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AES-128-ECB decrypt a single 16-byte block. [C] §2.1.1 (`AES-128D`).
|
||||||
|
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||||
|
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||||
|
let mut block = GenericArray::clone_from_slice(data);
|
||||||
|
cipher.decrypt_block(&mut block);
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out.copy_from_slice(&block);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AES-128-CBC decrypt in-place with the fixed AACS IV. [C] §2.1.2 (`AES-128CBCD`).
|
||||||
|
///
|
||||||
|
/// Precondition: `data.len()` is a multiple of 16. Any trailing partial
|
||||||
|
/// block is silently ignored; all callers pass aligned regions (6128 and
|
||||||
|
/// 2032 bytes), and the assert documents/enforces that contract.
|
||||||
|
pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
|
||||||
|
debug_assert!(
|
||||||
|
data.len() % 16 == 0,
|
||||||
|
"aes_cbc_decrypt requires a block-aligned slice"
|
||||||
|
);
|
||||||
|
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||||
|
let num_blocks = data.len() / 16;
|
||||||
|
// Process blocks in reverse to avoid clobbering ciphertext needed for XOR
|
||||||
|
for i in (0..num_blocks).rev() {
|
||||||
|
let offset = i * 16;
|
||||||
|
let prev = if i == 0 {
|
||||||
|
AACS_IV
|
||||||
|
} else {
|
||||||
|
let mut p = [0u8; 16];
|
||||||
|
p.copy_from_slice(&data[(i - 1) * 16..i * 16]);
|
||||||
|
p
|
||||||
|
};
|
||||||
|
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
|
||||||
|
cipher.decrypt_block(&mut block);
|
||||||
|
for j in 0..16 {
|
||||||
|
data[offset + j] = block[j] ^ prev[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AES-G(x1, x2) = AES-128D(x1, x2) XOR x2. [C] §2.1.3 (note: uses AES-128**D**).
|
||||||
|
///
|
||||||
|
/// The Media Key Variant chain uses AES-G to derive both the variant
|
||||||
|
/// number (`Kvn = AES-G(Kp, Nonce)`) and the Volume Unique Key
|
||||||
|
/// (`Kvu = AES-G(Km, VID)`). See [`super::keys::derive_vuk`] for the
|
||||||
|
/// classical VUK form — the math is identical, this exposes it as a
|
||||||
|
/// neutral primitive for the variant chain.
|
||||||
|
pub(crate) fn aes_g(x1: &[u8; 16], x2: &[u8; 16]) -> [u8; 16] {
|
||||||
|
let mut out = aes_ecb_decrypt(x1, x2);
|
||||||
|
for i in 0..16 {
|
||||||
|
out[i] ^= x2[i];
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AACS-G3 seed constant (`s0`). [C] §3.2.2.
|
||||||
|
pub(crate) const AESG3_SEED: [u8; 16] = [
|
||||||
|
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// AACS-G3: derive a subkey from a parent key. [C] §3.2.2 (Triple AES Generator:
|
||||||
|
/// left=`D(k,s0)⊕s0` inc 0, pk=`D(k,s0+1)⊕(s0+1)` inc 1, right=`D(k,s0+2)⊕(s0+2)` inc 2).
|
||||||
|
/// seed[15] += inc, then AES-DEC(key, seed) XOR seed.
|
||||||
|
///
|
||||||
|
/// Shared with [`super::variants`] (its variant chain runs the same SD
|
||||||
|
/// tree); a single definition keeps the two walks byte-identical.
|
||||||
|
pub(crate) 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);
|
||||||
|
for i in 0..16 {
|
||||||
|
out[i] ^= seed[i];
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
//! Media-key derivation: DK/PK → Media Key via the subset-difference tree.
|
||||||
|
//! [C] §3.2.2–§3.2.5.
|
||||||
|
|
||||||
|
use super::crypto::*;
|
||||||
|
use super::inf::*;
|
||||||
|
use super::mkb::*;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
/// Derive Media Key from MKB data using processing keys.
|
||||||
|
///
|
||||||
|
/// A Processing Key is **terminal**: it is the key at its Subset-Difference
|
||||||
|
/// node, one `AES-G` from the Media Key. So this is the fast path — each PK is
|
||||||
|
/// tried *directly* against the MKB cvalue tables (no tree descent), matching
|
||||||
|
/// libaacs `_calc_mk_pks` (iterate PKs × cvalues). On a large AACS 2.x UHD MKB
|
||||||
|
/// (~181k cvalues) this is ~15x faster than treating a PK as a device-node
|
||||||
|
/// label and walking the tree.
|
||||||
|
///
|
||||||
|
/// If you hold a **device-node label** at unknown tree depth (not a terminal
|
||||||
|
/// PK), derive its Media Key through the device-key path
|
||||||
|
/// ([`derive_media_key_from_dk`]) — that path owns the Subset-Difference tree
|
||||||
|
/// walk; the PK path never descends.
|
||||||
|
///
|
||||||
|
/// MKB format:
|
||||||
|
/// Record type 0x10 = Type and Version Record (has MKB version)
|
||||||
|
/// Record type 0x81 = Verify Media Key Record, AACS 1.0 (has mk_dv)
|
||||||
|
/// Record type 0x86 = Verify Media Key Record, AACS 2.0/2.1 (has mk_dv)
|
||||||
|
/// Record type 0x04 = Subset-Difference Index (has UVS entries)
|
||||||
|
/// Record type 0x05 = Media Key Data Record (cvalues, 1:1 with 0x04)
|
||||||
|
/// Record type 0x07 = Explicit Subset-Difference Record (NOT cvalues)
|
||||||
|
pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Option<[u8; 16]> {
|
||||||
|
let mk_dv = mkb_find_mk_dv(mkb)?;
|
||||||
|
let uvs = mkb_find_subdiff_records(mkb)?;
|
||||||
|
let cvalues = mkb_find_cvalues(mkb)?;
|
||||||
|
try_pk_against_tables(processing_keys, &uvs, &cvalues, &mk_dv)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core terminal-PK table scan over explicit record bodies. Each processing
|
||||||
|
/// key is tried **directly** against every `(uv, cvalue)` pair — no tree
|
||||||
|
/// descent. Reached in production via [`derive_media_key_from_pk`]; factored
|
||||||
|
/// out so reproduction harnesses can drive it with explicit tables.
|
||||||
|
pub(crate) fn try_pk_against_tables(
|
||||||
|
processing_keys: &[[u8; 16]],
|
||||||
|
uvs: &[u8],
|
||||||
|
cvalues: &[u8],
|
||||||
|
mk_dv: &[u8; 16],
|
||||||
|
) -> Option<[u8; 16]> {
|
||||||
|
let num_uvs = uvs
|
||||||
|
.chunks(5)
|
||||||
|
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
for pk in processing_keys {
|
||||||
|
for i in 0..num_uvs {
|
||||||
|
if (i + 1) * 16 > cvalues.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let record_start = i * 5;
|
||||||
|
if record_start + 5 > uvs.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let uv = &uvs[record_start + 1..record_start + 5];
|
||||||
|
let cv = &cvalues[i * 16..(i + 1) * 16];
|
||||||
|
if let Some(mk) = validate_processing_key(pk, cv, uv, mk_dv) {
|
||||||
|
return Some(mk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a processing key against a cvalue/UV pair.
|
||||||
|
/// Returns the Media Key if valid.
|
||||||
|
///
|
||||||
|
/// Steps (media key: [C] §3.2.4; verify relation: [C] §3.2.5.1.4):
|
||||||
|
/// 1. `mk = AES-128D(pk, cvalue)` [C] §3.2.4
|
||||||
|
/// 2. `mk[12..16] ^= uv` (4 bytes XOR into the last 4 bytes only) [C] §3.2.4
|
||||||
|
/// 3. `dec_vd = AES-128D(mk, mk_dv)` [C] §3.2.5.1.4
|
||||||
|
/// 4. If `dec_vd[0..8] == 01 23 45 67 89 AB CD EF` → valid. [C] §3.2.5.1.4
|
||||||
|
pub(crate) fn validate_processing_key(
|
||||||
|
pk: &[u8; 16],
|
||||||
|
cvalue: &[u8],
|
||||||
|
uv: &[u8],
|
||||||
|
mk_dv: &[u8; 16],
|
||||||
|
) -> Option<[u8; 16]> {
|
||||||
|
if cvalue.len() < 16 || uv.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: mk = AES-128D(pk, cvalue)
|
||||||
|
let mut cv = [0u8; 16];
|
||||||
|
cv.copy_from_slice(&cvalue[..16]);
|
||||||
|
let mut mk = aes_ecb_decrypt(pk, &cv);
|
||||||
|
|
||||||
|
// Step 2: XOR uv into the last 4 bytes of mk (mk[12..16]).
|
||||||
|
for a in 0..4 {
|
||||||
|
mk[12 + a] ^= uv[a];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3 + 4: dec_vd = AES-128D(mk, mk_dv); verify magic.
|
||||||
|
let dec_vd = aes_ecb_decrypt(&mk, mk_dv);
|
||||||
|
const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
||||||
|
if dec_vd[..8] == VERIFY_MAGIC {
|
||||||
|
return Some(mk);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute v_mask from a UV value. [C] §3.2.3. 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;
|
||||||
|
}
|
||||||
|
v_mask
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive processing key from device key using subset-difference tree traversal.
|
||||||
|
/// [C] §3.2.4 (device-tree descent, MSB-branch, terminal PK). 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] {
|
||||||
|
// Descend from the device node to the record node, following the record's
|
||||||
|
// `uv` bits. At each level only the child we descend INTO is needed (the
|
||||||
|
// sibling is computed but never used), and the Processing Key is the
|
||||||
|
// `aesg3(.,1)` of the FINAL node — so we derive ONE child per level and the
|
||||||
|
// PK once at the end, instead of left/pk/right at every level. Identical
|
||||||
|
// result, ~3x fewer block ops. (left child = `aesg3(node,0)`, right = `,2`.)
|
||||||
|
let mut node = *dk;
|
||||||
|
let mut current_v_mask = dev_key_v_mask;
|
||||||
|
|
||||||
|
// The subset-difference tree is at most 32 levels deep (u32 mask), so the
|
||||||
|
// walk must converge in <= 32 steps. The arithmetic `>> 1` sign-extends
|
||||||
|
// current_v_mask, so a v_mask coarser than dev_key_v_mask (reachable from
|
||||||
|
// a crafted/corrupt MKB) would otherwise saturate at 0xFFFF_FFFF and spin
|
||||||
|
// forever — bound the loop to keep a bad disc from hanging the rip thread.
|
||||||
|
let mut steps = 0u32;
|
||||||
|
while current_v_mask != v_mask {
|
||||||
|
if steps >= 32 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
steps += 1;
|
||||||
|
// Find the highest unset bit in current_v_mask
|
||||||
|
let mut bit_pos: i32 = -1;
|
||||||
|
for i in (0..32).rev() {
|
||||||
|
if (current_v_mask & (1u32 << i)) == 0 {
|
||||||
|
bit_pos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inc = if bit_pos < 0 || (uv & (1u32 << bit_pos as u32)) == 0 {
|
||||||
|
0 // left child
|
||||||
|
} else {
|
||||||
|
2 // right child
|
||||||
|
};
|
||||||
|
node = aesg3(&node, inc);
|
||||||
|
|
||||||
|
current_v_mask = ((current_v_mask as i32) >> 1) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
aesg3(&node, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)?;
|
||||||
|
|
||||||
|
// Count UV entries
|
||||||
|
let num_uvs = uvs
|
||||||
|
.chunks(5)
|
||||||
|
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
for dk in device_keys {
|
||||||
|
let device_number = dk.node as u32;
|
||||||
|
|
||||||
|
// Find applying subset-difference for this device
|
||||||
|
for uvs_idx in 0..num_uvs {
|
||||||
|
let p_uv = &uvs[1 + 5 * uvs_idx..];
|
||||||
|
let u_mask_shift = uvs[5 * uvs_idx]; // byte before the UV value
|
||||||
|
|
||||||
|
if u_mask_shift & 0xC0 != 0 {
|
||||||
|
break; // device revoked
|
||||||
|
}
|
||||||
|
// Shifts of 32..=63 (0x20..=0x3F pass the 0xC0 mask above) would
|
||||||
|
// panic in debug / wrap to a wrong mask in release. The MKB byte
|
||||||
|
// is disc-controlled, so a crafted/corrupt MKB must not crash the
|
||||||
|
// ripper: skip an out-of-range slot rather than `<<` it.
|
||||||
|
if u_mask_shift >= 32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
||||||
|
if uv == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// u-mask = shift count of low-order 0 bits ([C] §3.2.5.1.5); v-mask [C] §3.2.3.
|
||||||
|
let u_mask: u32 = 0xFFFF_FFFF << u_mask_shift;
|
||||||
|
let v_mask = calc_v_mask(uv);
|
||||||
|
|
||||||
|
// Subset-difference applies iff (d&mu)==(uv&mu) && (d&mv)!=(uv&mv). [C] §3.2.4.
|
||||||
|
if ((device_number & u_mask) == (uv & u_mask))
|
||||||
|
&& ((device_number & v_mask) != (uv & v_mask))
|
||||||
|
{
|
||||||
|
// Found matching subset-difference — find the right device key.
|
||||||
|
// dk.u_mask_shift is a u8 from keydb with no range check;
|
||||||
|
// guard the shift the same way as the MKB byte above.
|
||||||
|
if dk.u_mask_shift >= 32 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dev_key_v_mask = calc_v_mask(dk.uv);
|
||||||
|
let dev_key_u_mask: u32 = 0xFFFF_FFFF << dk.u_mask_shift;
|
||||||
|
|
||||||
|
if u_mask == dev_key_u_mask && (uv & dev_key_v_mask) == (dk.uv & dev_key_v_mask) {
|
||||||
|
// Derive processing key via tree traversal
|
||||||
|
let pk = calc_pk_from_dk(&dk.key, uv, v_mask, dev_key_v_mask);
|
||||||
|
|
||||||
|
// Validate and derive media key
|
||||||
|
if uvs_idx < cvalues.len() / 16 {
|
||||||
|
let cv = &cvalues[uvs_idx * 16..(uvs_idx + 1) * 16];
|
||||||
|
if let Some(mk) =
|
||||||
|
validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv)
|
||||||
|
{
|
||||||
|
return Some((mk, pk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
// The slots are independent, so the scan parallelises — a UHD MKB has ~181k
|
||||||
|
// slots (~26s single-threaded). `find_map_any` returns the first matching
|
||||||
|
// node found by any thread and cancels the rest; a valid MKB has exactly one
|
||||||
|
// matching subset-difference, so which thread finds it is immaterial.
|
||||||
|
use rayon::prelude::*;
|
||||||
|
let found = (0..num_uvs.min(n_cv)).into_par_iter().find_map_any(|i| {
|
||||||
|
let u_mask_shift = uvs[5 * i];
|
||||||
|
if u_mask_shift >= 32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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): cheapest, most common.
|
||||||
|
if validate_processing_key(&pk_zero_descent, cv, uv_bytes, &mk_dv).is_some() {
|
||||||
|
return Some((uv_r, u_mask_shift));
|
||||||
|
}
|
||||||
|
// Descent: device is an ANCESTOR of the slot. Walk the depth bit up from
|
||||||
|
// the slot's lowest set bit; each level descends to the slot's node.
|
||||||
|
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 Some((uv_d, u_mask_shift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
found.and_then(|(uv, mask)| resolve_dk_node(mkb, key, uv, mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub(crate) 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Public, side-effect-free accessors over the MKB record helpers, exposed so
|
||||||
|
/// independent reproduction harnesses (e.g. `examples/prove_hkd_aacs.rs`) can
|
||||||
|
/// exercise the exact same parser + verify primitives the production walk uses.
|
||||||
|
/// These are thin wrappers — no new logic.
|
||||||
|
pub mod probe {
|
||||||
|
use super::super::crypto::aes_ecb_decrypt;
|
||||||
|
|
||||||
|
/// `mk_dv` from the MKB's Verify-Media-Key record (type 0x81 / 0x86).
|
||||||
|
pub fn mkb_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
super::mkb_find_mk_dv(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body of the MKB's Subset-Difference Index record (type 0x04).
|
||||||
|
pub fn mkb_subdiff(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
super::mkb_find_subdiff_records(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body of the MKB's Media-Key-Data (cvalues) record. Selects record
|
||||||
|
/// `0x05` (the large cvalue table, 1:1 with the `0x04` Subset-Difference
|
||||||
|
/// index on AACS 2.x UHD MKBs), falling back to `0x07` only when `0x05`
|
||||||
|
/// is absent.
|
||||||
|
pub fn mkb_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
super::mkb_find_cvalues(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Body (header stripped) of the first MKB record of `rec_type`. Lets a
|
||||||
|
/// harness pin an exact record type for cross-checking the production
|
||||||
|
/// cvalue selection (e.g. compare record `0x05` vs `0x07` sizes).
|
||||||
|
pub fn mkb_record_body(mkb: &[u8], rec_type: u8) -> Option<Vec<u8>> {
|
||||||
|
super::find_record_body(mkb, rec_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AES-128-ECB single-block decrypt (the AACS verify primitive).
|
||||||
|
pub fn aes_dec(key: &[u8; 16], block: &[u8; 16]) -> [u8; 16] {
|
||||||
|
aes_ecb_decrypt(key, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does `km` satisfy the MKB's Verify-Media-Key relation?
|
||||||
|
/// `AES-D(km, mk_dv)[0..8] == 01 23 45 67 89 AB CD EF`.
|
||||||
|
pub fn km_verifies(mkb: &[u8], km: &[u8; 16]) -> bool {
|
||||||
|
match super::mkb_find_mk_dv(mkb) {
|
||||||
|
Some(mk_dv) => {
|
||||||
|
aes_ecb_decrypt(km, &mk_dv)[..8] == [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Volume key: Media Key + Volume ID → VUK → unit keys ──────────────────────
|
||||||
|
|
||||||
|
/// Derive VUK from Media Key and Volume ID. [PR] §3.3 / [BD] §3.3
|
||||||
|
/// (`Kvu = AES-G(Km, IDv)`; AES-G uses AES-128D):
|
||||||
|
/// VUK = AES-128-ECB-DECRYPT(media_key, volume_id) XOR volume_id
|
||||||
|
pub fn derive_vuk(media_key: &[u8; 16], volume_id: &[u8; 16]) -> [u8; 16] {
|
||||||
|
let mut vuk = aes_ecb_decrypt(media_key, volume_id);
|
||||||
|
for i in 0..16 {
|
||||||
|
vuk[i] ^= volume_id[i];
|
||||||
|
}
|
||||||
|
vuk
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt an encrypted unit key using the VUK (AES-128-ECB). [PR] §3.5
|
||||||
|
/// (Title Key unwrap `Kt = AES-128D(Ku, Kte)`); the BD "CPS Unit Key" synonym is [BD] §3.9.3.
|
||||||
|
pub fn decrypt_unit_key(vuk: &[u8; 16], encrypted_uk: &[u8; 16]) -> [u8; 16] {
|
||||||
|
aes_ecb_decrypt(vuk, encrypted_uk)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt every encrypted unit key in a parsed `Unit_Key_RO.inf` with a VUK,
|
||||||
|
/// paired with its declared CPS-unit number. THE single VUK→unit-keys step:
|
||||||
|
/// both classical/v21 resolvers and [`resolve_candidate`] call this, so the
|
||||||
|
/// map cannot drift between the player and harvest paths.
|
||||||
|
pub(crate) fn derive_unit_keys(uk_file: &UnitKeyFile, vuk: &[u8; 16]) -> Vec<(u32, [u8; 16])> {
|
||||||
|
uk_file
|
||||||
|
.encrypted_keys
|
||||||
|
.iter()
|
||||||
|
.map(|(num, enc_key)| (*num, decrypt_unit_key(vuk, enc_key)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A candidate key at any rung of the AACS ladder, handed to [`resolve_candidate`].
|
||||||
|
///
|
||||||
|
/// Each variant carries the [`super::types`] newtype for that rung (a `Dk` is a
|
||||||
|
/// POSITIONED [`DeviceKey`] — recover an unpositioned one with
|
||||||
|
/// [`recover_dk_position`] first).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum KeyCandidate {
|
||||||
|
Uk(UnitKey),
|
||||||
|
Vuk(Vuk),
|
||||||
|
Mk(MediaKey),
|
||||||
|
Pk(ProcessingKey),
|
||||||
|
Dk(DeviceKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The AACS key chain derived from a candidate, from [`resolve_candidate`].
|
||||||
|
///
|
||||||
|
/// PURE DERIVATION — no unit sampling, no validation. `unit_keys` holds every
|
||||||
|
/// CPS-unit key the disc's `Unit_Key_RO.inf` yields from the VUK (paired with
|
||||||
|
/// its declared CPS-unit number); the caller runs
|
||||||
|
/// [`super::content::unit_key_validates`] to find which one actually opens the
|
||||||
|
/// disc. Rungs above the candidate are `None`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ResolvedChain {
|
||||||
|
pub unit_keys: Vec<(u32, [u8; 16])>,
|
||||||
|
pub vuk: Option<Vuk>,
|
||||||
|
pub mk: Option<MediaKey>,
|
||||||
|
pub pk: Option<ProcessingKey>,
|
||||||
|
/// The positioned device key (for a `Dk` candidate).
|
||||||
|
pub dk: Option<DeviceKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive the full AACS key chain from a candidate key of ANY ladder rung.
|
||||||
|
///
|
||||||
|
/// Runs the deterministic derivation DOWNWARD to the disc's terminal unit keys:
|
||||||
|
/// `DK → MK → VUK → UKs`, `PK → MK → VUK → UKs`, `MK → VUK → UKs`,
|
||||||
|
/// `VUK → UKs`, or `UK → itself`. Composes the raw derivation primitives
|
||||||
|
/// ([`derive_media_key_from_pk`], [`derive_media_key_and_pk_from_dk`],
|
||||||
|
/// [`derive_vuk`], [`derive_unit_keys`]) and parses `Unit_Key_RO.inf` at the
|
||||||
|
/// version the disc's MKB declares, so a multi-CPS disc yields all its unit
|
||||||
|
/// keys from the one candidate.
|
||||||
|
///
|
||||||
|
/// PURE DERIVATION: no sampling, no validation, no position recovery. Validate
|
||||||
|
/// `unit_keys` against a real encrypted unit with
|
||||||
|
/// [`super::content::unit_key_validates`] to prove the candidate opens the disc.
|
||||||
|
///
|
||||||
|
/// Returns `None` only when derivation itself cannot proceed: a PK its MKB
|
||||||
|
/// rejects, a `Dk` the MKB can't process, a missing VID on a path that needs
|
||||||
|
/// one, or an unparseable/empty `Unit_Key_RO.inf`.
|
||||||
|
pub fn resolve_candidate(
|
||||||
|
candidate: &KeyCandidate,
|
||||||
|
mkb: &[u8],
|
||||||
|
unit_key_ro: &[u8],
|
||||||
|
vid: Option<Vid>,
|
||||||
|
) -> Option<ResolvedChain> {
|
||||||
|
// Boil a VUK → all unit keys, each paired with its declared CPS-unit number.
|
||||||
|
// Derive the stride version from the disc's own MKB, then defer to the shared
|
||||||
|
// `derive_unit_keys` (the one place both resolvers and this path decrypt).
|
||||||
|
let boil = |vuk: Vuk| -> Option<Vec<(u32, [u8; 16])>> {
|
||||||
|
let version = mkb_type(mkb)
|
||||||
|
.map(|t| t.generation())
|
||||||
|
.unwrap_or(AacsVersion::V10);
|
||||||
|
let ukf = parse_unit_key_ro(unit_key_ro, version)?;
|
||||||
|
if ukf.encrypted_keys.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(derive_unit_keys(&ukf, &vuk.0))
|
||||||
|
};
|
||||||
|
|
||||||
|
match candidate {
|
||||||
|
KeyCandidate::Uk(uk) => Some(ResolvedChain {
|
||||||
|
unit_keys: vec![(uk.idx, uk.key)],
|
||||||
|
vuk: None,
|
||||||
|
mk: None,
|
||||||
|
pk: None,
|
||||||
|
dk: None,
|
||||||
|
}),
|
||||||
|
KeyCandidate::Vuk(v) => Some(ResolvedChain {
|
||||||
|
unit_keys: boil(*v)?,
|
||||||
|
vuk: Some(*v),
|
||||||
|
mk: None,
|
||||||
|
pk: None,
|
||||||
|
dk: None,
|
||||||
|
}),
|
||||||
|
KeyCandidate::Mk(mk) => {
|
||||||
|
let vuk = Vuk(derive_vuk(&mk.0, &vid?.0));
|
||||||
|
Some(ResolvedChain {
|
||||||
|
unit_keys: boil(vuk)?,
|
||||||
|
vuk: Some(vuk),
|
||||||
|
mk: Some(*mk),
|
||||||
|
pk: None,
|
||||||
|
dk: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeyCandidate::Pk(pk) => {
|
||||||
|
let km = derive_media_key_from_pk(mkb, std::slice::from_ref(&pk.0))?;
|
||||||
|
let vuk = Vuk(derive_vuk(&km, &vid?.0));
|
||||||
|
Some(ResolvedChain {
|
||||||
|
unit_keys: boil(vuk)?,
|
||||||
|
vuk: Some(vuk),
|
||||||
|
mk: Some(MediaKey(km)),
|
||||||
|
pk: Some(*pk),
|
||||||
|
dk: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeyCandidate::Dk(dk) => {
|
||||||
|
let (km, pk) = derive_media_key_and_pk_from_dk(mkb, std::slice::from_ref(dk))?;
|
||||||
|
let vuk = Vuk(derive_vuk(&km, &vid?.0));
|
||||||
|
Some(ResolvedChain {
|
||||||
|
unit_keys: boil(vuk)?,
|
||||||
|
vuk: Some(vuk),
|
||||||
|
mk: Some(MediaKey(km)),
|
||||||
|
pk: Some(ProcessingKey(pk)),
|
||||||
|
dk: Some(dk.clone()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod resolve_candidate_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||||
|
|
||||||
|
/// Minimal AACS-1.0 (48-byte stride) `Unit_Key_RO.inf` with `n` encrypted
|
||||||
|
/// unit keys — `parse_unit_key_ro` numbers CPS units 1..=n.
|
||||||
|
fn synth_inf(encs: &[[u8; 16]]) -> Vec<u8> {
|
||||||
|
let uk_pos = 32usize;
|
||||||
|
let stride = 48usize;
|
||||||
|
let n = encs.len();
|
||||||
|
let total = uk_pos + 48 + n.saturating_sub(1) * stride + 16;
|
||||||
|
let mut inf = vec![0u8; total.max(20)];
|
||||||
|
inf[..4].copy_from_slice(&(uk_pos as u32).to_be_bytes());
|
||||||
|
inf[uk_pos..uk_pos + 2].copy_from_slice(&(n as u16).to_be_bytes());
|
||||||
|
for (i, k) in encs.iter().enumerate() {
|
||||||
|
let o = uk_pos + 48 + i * stride;
|
||||||
|
inf[o..o + 16].copy_from_slice(k);
|
||||||
|
}
|
||||||
|
inf
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A VUK candidate boils to ALL the disc's unit keys, each paired with its
|
||||||
|
/// declared CPS-unit number, and each key equals the VUK-decrypt of its slot.
|
||||||
|
#[test]
|
||||||
|
fn resolve_candidate_vuk_returns_all_cps_units() {
|
||||||
|
let vuk = Vuk([0x33u8; 16]);
|
||||||
|
let encs = [[0x11u8; 16], [0x22u8; 16], [0x44u8; 16]];
|
||||||
|
let inf = synth_inf(&encs);
|
||||||
|
let r = resolve_candidate(&KeyCandidate::Vuk(vuk), &[], &inf, None).expect("vuk derives");
|
||||||
|
let cps: Vec<u32> = r.unit_keys.iter().map(|(c, _)| *c).collect();
|
||||||
|
assert_eq!(
|
||||||
|
cps,
|
||||||
|
vec![1, 2, 3],
|
||||||
|
"every CPS unit surfaced, numbered from the inf"
|
||||||
|
);
|
||||||
|
for ((_, key), enc) in r.unit_keys.iter().zip(encs.iter()) {
|
||||||
|
assert_eq!(
|
||||||
|
*key,
|
||||||
|
decrypt_unit_key(&vuk.0, enc),
|
||||||
|
"key = VUK-decrypt of its slot"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(r.vuk, Some(vuk));
|
||||||
|
assert!(r.mk.is_none() && r.pk.is_none() && r.dk.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bare UK candidate is terminal — it returns itself keyed by its own idx.
|
||||||
|
#[test]
|
||||||
|
fn resolve_candidate_uk_is_itself() {
|
||||||
|
let uk = UnitKey {
|
||||||
|
idx: 2,
|
||||||
|
key: [0x9u8; 16],
|
||||||
|
};
|
||||||
|
let r = resolve_candidate(&KeyCandidate::Uk(uk), &[], &[], None).expect("uk is terminal");
|
||||||
|
assert_eq!(r.unit_keys, vec![(2, uk.key)]);
|
||||||
|
assert!(r.vuk.is_none() && r.mk.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MK/PK/DK paths derive the VUK from a VID; without one, derivation stops.
|
||||||
|
#[test]
|
||||||
|
fn resolve_candidate_mk_requires_vid() {
|
||||||
|
let r = resolve_candidate(&KeyCandidate::Mk(MediaKey([1u8; 16])), &[], &[], None);
|
||||||
|
assert!(r.is_none(), "MK path returns None without a VID");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A planted Processing Key resolves against a synthetic MKB and drives the
|
||||||
|
/// FULL chain PK → MK → VUK → UK — proving a PK candidate yields real keys.
|
||||||
|
#[test]
|
||||||
|
fn resolve_candidate_pk_drives_full_chain() {
|
||||||
|
let pk: [u8; 16] = [
|
||||||
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||||
|
0xFF, 0x00,
|
||||||
|
];
|
||||||
|
let mk: [u8; 16] = [
|
||||||
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
|
||||||
|
0xAE, 0xAF,
|
||||||
|
];
|
||||||
|
let uv: [u8; 4] = [0x00, 0x00, 0x04, 0x00];
|
||||||
|
|
||||||
|
let mut mk_raw = mk;
|
||||||
|
for a in 0..4 {
|
||||||
|
mk_raw[12 + a] ^= uv[a];
|
||||||
|
}
|
||||||
|
let cv = aes_ecb_encrypt(&pk, &mk_raw);
|
||||||
|
|
||||||
|
let mut vd = [0x11u8; 16];
|
||||||
|
vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
|
||||||
|
let mk_dv = aes_ecb_encrypt(&mk, &vd);
|
||||||
|
|
||||||
|
// 4-byte record header (type + BE24 total length) + body.
|
||||||
|
let rec = |t: u8, body: &[u8]| -> Vec<u8> {
|
||||||
|
let total = 4 + body.len();
|
||||||
|
let mut r = vec![
|
||||||
|
t,
|
||||||
|
((total >> 16) & 0xFF) as u8,
|
||||||
|
((total >> 8) & 0xFF) as u8,
|
||||||
|
(total & 0xFF) as u8,
|
||||||
|
];
|
||||||
|
r.extend_from_slice(body);
|
||||||
|
r
|
||||||
|
};
|
||||||
|
let mut sd = vec![0u8];
|
||||||
|
sd.extend_from_slice(&uv);
|
||||||
|
let mut mkb = Vec::new();
|
||||||
|
mkb.extend_from_slice(&rec(0x10, &[0, 0, 0, 0x20, 0, 0, 0, 0x52]));
|
||||||
|
mkb.extend_from_slice(&rec(0x86, &mk_dv));
|
||||||
|
mkb.extend_from_slice(&rec(0x04, &sd));
|
||||||
|
mkb.extend_from_slice(&rec(0x05, &cv));
|
||||||
|
|
||||||
|
let vid = Vid([0x42u8; 16]);
|
||||||
|
let plain_uk = [0x7Eu8; 16];
|
||||||
|
let vuk = derive_vuk(&mk, &vid.0);
|
||||||
|
let enc = aes_ecb_encrypt(&vuk, &plain_uk);
|
||||||
|
let inf = synth_inf(std::slice::from_ref(&enc));
|
||||||
|
|
||||||
|
let r = resolve_candidate(&KeyCandidate::Pk(ProcessingKey(pk)), &mkb, &inf, Some(vid))
|
||||||
|
.expect("planted PK resolves the full chain");
|
||||||
|
assert_eq!(r.mk, Some(MediaKey(mk)), "PK recovers the planted MK");
|
||||||
|
assert_eq!(r.unit_keys.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
r.unit_keys[0].1, plain_uk,
|
||||||
|
"PK chain recovers the title key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
pub fn collect_host_certs(
|
pub fn collect_host_certs(
|
||||||
opts: &crate::disc::ScanOptions,
|
opts: &crate::disc::ScanOptions,
|
||||||
mkb: Option<u32>,
|
mkb: Option<u32>,
|
||||||
) -> Vec<crate::aacs::HostCert> {
|
) -> Vec<crate::aacs::types::HostCert> {
|
||||||
let mut host_certs: Vec<crate::aacs::HostCert> = Vec::new();
|
let mut host_certs: Vec<crate::aacs::types::HostCert> = Vec::new();
|
||||||
if let Some(c) = &opts.credentials {
|
if let Some(c) = &opts.credentials {
|
||||||
host_certs.extend(c.host_certs.iter().cloned());
|
host_certs.extend(c.host_certs.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
//! AACS on-disc key-input files: `Unit_Key_RO.inf` parsing, the disc-hash
|
||||||
|
//! keydb lookup key, the Content Certificate, and the in-drive MKB read.
|
||||||
|
//! These turn raw disc files into the structures the key paths consume.
|
||||||
|
|
||||||
|
use super::mkb::*;
|
||||||
|
|
||||||
|
/// Parsed Unit_Key_RO.inf file.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnitKeyFile {
|
||||||
|
/// Disc hash (SHA1 of the entire file) — used as KEYDB lookup key
|
||||||
|
pub disc_hash: [u8; 20],
|
||||||
|
/// Application type (1 = BD-ROM)
|
||||||
|
pub app_type: u8,
|
||||||
|
/// Number of BDMV directories
|
||||||
|
pub num_bdmv_dir: u8,
|
||||||
|
/// Whether SKB MKB is used
|
||||||
|
pub use_skb_mkb: bool,
|
||||||
|
/// AACS generation this file's stride matches
|
||||||
|
pub version: AacsVersion,
|
||||||
|
/// Encrypted unit keys (CPS unit number, encrypted key)
|
||||||
|
pub encrypted_keys: Vec<(u32, [u8; 16])>,
|
||||||
|
/// Title → CPS unit index mapping (title_idx → unit_key_idx)
|
||||||
|
pub title_cps_unit: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute disc hash (SHA1 of Unit_Key_RO.inf content).
|
||||||
|
pub fn disc_hash(data: &[u8]) -> [u8; 20] {
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
let hash = Sha1::digest(data);
|
||||||
|
let mut out = [0u8; 20];
|
||||||
|
out.copy_from_slice(&hash);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format disc hash as hex string with 0x prefix (for KEYDB lookup).
|
||||||
|
pub fn disc_hash_hex(hash: &[u8; 20]) -> String {
|
||||||
|
let mut s = String::with_capacity(42);
|
||||||
|
s.push_str("0x");
|
||||||
|
for b in hash {
|
||||||
|
s.push_str(&format!("{b:02X}"));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse Unit_Key_RO.inf from raw bytes.
|
||||||
|
///
|
||||||
|
/// Format (from AACS spec):
|
||||||
|
/// [0..4] BE32: offset to key storage area (uk_pos)
|
||||||
|
/// [16] app_type (1 = BD-ROM)
|
||||||
|
/// [17] num_bdmv_dir
|
||||||
|
/// [18] bit 7: use_skb_mkb
|
||||||
|
/// [20..22] BE16: first_play CPS unit
|
||||||
|
/// [22..24] BE16: top_menu CPS unit
|
||||||
|
/// [24..26] BE16: num_titles
|
||||||
|
/// [26..] title entries: 2 bytes padding + 2 bytes CPS unit, × num_titles
|
||||||
|
///
|
||||||
|
/// Key storage at uk_pos:
|
||||||
|
/// [uk_pos..uk_pos+2] BE16: num_unit_keys
|
||||||
|
/// [uk_pos+48..] encrypted keys, 16 bytes each
|
||||||
|
/// AACS 1.0: 48-byte stride
|
||||||
|
/// AACS 2.0 / 2.1: 64-byte stride (48 + 16 extra)
|
||||||
|
pub fn parse_unit_key_ro(data: &[u8], version: AacsVersion) -> Option<UnitKeyFile> {
|
||||||
|
if data.len() < 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash = disc_hash(data);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
let app_type = data[16];
|
||||||
|
let num_bdmv_dir = data[17];
|
||||||
|
let use_skb_mkb = (data[18] >> 7) & 1 == 1;
|
||||||
|
|
||||||
|
// Key storage offset
|
||||||
|
let uk_pos = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||||
|
if uk_pos + 2 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of unit keys
|
||||||
|
let num_uk = u16::from_be_bytes([data[uk_pos], data[uk_pos + 1]]) as usize;
|
||||||
|
if num_uk == 0 {
|
||||||
|
return Some(UnitKeyFile {
|
||||||
|
disc_hash: hash,
|
||||||
|
app_type,
|
||||||
|
num_bdmv_dir,
|
||||||
|
use_skb_mkb,
|
||||||
|
version,
|
||||||
|
encrypted_keys: Vec::new(),
|
||||||
|
title_cps_unit: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stride between keys
|
||||||
|
let stride = version.unit_key_stride();
|
||||||
|
|
||||||
|
// Validate size
|
||||||
|
let keys_start = uk_pos + 48; // first key at uk_pos + 48
|
||||||
|
if keys_start + 16 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract encrypted keys
|
||||||
|
let mut encrypted_keys = Vec::with_capacity(num_uk);
|
||||||
|
let mut pos = keys_start;
|
||||||
|
for i in 0..num_uk {
|
||||||
|
if pos + 16 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
key.copy_from_slice(&data[pos..pos + 16]);
|
||||||
|
encrypted_keys.push(((i + 1) as u32, key));
|
||||||
|
pos += stride;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loop above `break`s if the buffer runs out mid-key. A short list
|
||||||
|
// means the .inf is malformed/truncated — reject it rather than silently
|
||||||
|
// accepting fewer keys than the header declared, which would later map
|
||||||
|
// title CPS units to nonexistent keys.
|
||||||
|
if encrypted_keys.len() != num_uk {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title → CPS unit mapping. libaacs (unit_key.c) validates each on-disc CPS
|
||||||
|
// value is in `1..=num_uk` (else zeroes it) and converts the 1-based on-disc
|
||||||
|
// index to a 0-based key index. We mirror that so the stored value is a safe,
|
||||||
|
// ready-to-use key index rather than a raw 1-based number.
|
||||||
|
let to_key_idx = |cps: u16| -> u16 {
|
||||||
|
if cps >= 1 && cps as usize <= num_uk {
|
||||||
|
cps - 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut title_cps_unit = Vec::new();
|
||||||
|
if data.len() >= 26 {
|
||||||
|
let first_play = u16::from_be_bytes([data[20], data[21]]);
|
||||||
|
let top_menu = u16::from_be_bytes([data[22], data[23]]);
|
||||||
|
let num_titles = u16::from_be_bytes([data[24], data[25]]) as usize;
|
||||||
|
|
||||||
|
title_cps_unit.push(to_key_idx(first_play));
|
||||||
|
title_cps_unit.push(to_key_idx(top_menu));
|
||||||
|
|
||||||
|
for i in 0..num_titles {
|
||||||
|
let off = 26 + i * 4 + 2; // 2 bytes padding + 2 bytes CPS unit
|
||||||
|
if off + 2 <= data.len() {
|
||||||
|
let cps = u16::from_be_bytes([data[off], data[off + 1]]);
|
||||||
|
title_cps_unit.push(to_key_idx(cps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(UnitKeyFile {
|
||||||
|
disc_hash: hash,
|
||||||
|
app_type,
|
||||||
|
num_bdmv_dir,
|
||||||
|
use_skb_mkb,
|
||||||
|
version,
|
||||||
|
encrypted_keys,
|
||||||
|
title_cps_unit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MKB disc structure format code.
|
||||||
|
const MKB_DISC_STRUCTURE_FORMAT: u8 = 0x83;
|
||||||
|
|
||||||
|
/// MKB pack buffer size.
|
||||||
|
const MKB_PACK_SIZE: usize = 32772;
|
||||||
|
|
||||||
|
/// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83).
|
||||||
|
/// Returns the concatenated MKB data from all packs.
|
||||||
|
pub fn read_mkb_from_drive(
|
||||||
|
session: &mut dyn crate::scsi::ScsiTransport,
|
||||||
|
) -> crate::error::Result<Vec<u8>> {
|
||||||
|
use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE};
|
||||||
|
|
||||||
|
let cdb = [
|
||||||
|
SCSI_READ_DISC_STRUCTURE,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
MKB_DISC_STRUCTURE_FORMAT,
|
||||||
|
(MKB_PACK_SIZE >> 8) as u8,
|
||||||
|
(MKB_PACK_SIZE & 0xFF) as u8,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
|
let mut buf = vec![0u8; 32772];
|
||||||
|
session.execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000)?;
|
||||||
|
|
||||||
|
let data_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
|
||||||
|
if data_len < 2 {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let len = data_len - 2;
|
||||||
|
let num_packs = buf[3] as usize;
|
||||||
|
|
||||||
|
let mut mkb = Vec::with_capacity(32768 * num_packs.max(1));
|
||||||
|
if len > 0 && len <= 32768 {
|
||||||
|
mkb.extend_from_slice(&buf[4..4 + len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read remaining packs
|
||||||
|
for pack in 1..num_packs {
|
||||||
|
let mut cdb = [
|
||||||
|
SCSI_READ_DISC_STRUCTURE,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
MKB_DISC_STRUCTURE_FORMAT,
|
||||||
|
(MKB_PACK_SIZE >> 8) as u8,
|
||||||
|
(MKB_PACK_SIZE & 0xFF) as u8,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
|
// Pack number goes in address field
|
||||||
|
cdb[2] = ((pack >> 24) & 0xFF) as u8;
|
||||||
|
cdb[3] = ((pack >> 16) & 0xFF) as u8;
|
||||||
|
cdb[4] = ((pack >> 8) & 0xFF) as u8;
|
||||||
|
cdb[5] = (pack & 0xFF) as u8;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; 32772];
|
||||||
|
if session
|
||||||
|
.execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
|
||||||
|
if len > 2 && len - 2 <= 32768 {
|
||||||
|
mkb.extend_from_slice(&buf[4..4 + len - 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(mkb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AACS Content Certificate — identifies disc AACS version and features.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ContentCert {
|
||||||
|
/// Bus encryption enabled flag
|
||||||
|
pub bus_encryption: bool,
|
||||||
|
/// Content Certificate ID (6 bytes)
|
||||||
|
pub cc_id: [u8; 6],
|
||||||
|
/// AACS generation indicated by the certificate type byte.
|
||||||
|
///
|
||||||
|
/// Cert type `0x00` → [`AacsVersion::V10`]; any other value →
|
||||||
|
/// [`AacsVersion::V20`]. The certificate alone cannot distinguish
|
||||||
|
/// V20 from V21 — Variant detection happens after the MKB walk.
|
||||||
|
pub version: AacsVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a Content Certificate (ContentXXX.cer) file.
|
||||||
|
pub fn parse_content_cert(data: &[u8]) -> Option<ContentCert> {
|
||||||
|
if data.len() < 20 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content Certificate layout (matches libaacs content_cert.c):
|
||||||
|
// [0] certificate type (0x00 = AACS1, 0x10 = AACS2)
|
||||||
|
// [1] bit7 bus_encryption_enabled_flag (libaacs: `p[1] >> 7`)
|
||||||
|
// [14..20] cc_id (6 bytes) (libaacs: `p + 14`)
|
||||||
|
let version = if data[0] == 0x00 {
|
||||||
|
AacsVersion::V10
|
||||||
|
} else {
|
||||||
|
AacsVersion::V20
|
||||||
|
};
|
||||||
|
// The flag is bit 7 of byte 1, NOT bit 0. Reading bit 0 (the prior bug) made
|
||||||
|
// a bus-encrypted cert (byte1=0x80) read as `false`, defeating the
|
||||||
|
// AacsBusKeyUnavailable fail-loud gate in disc/encrypt.rs.
|
||||||
|
let bus_encryption = (data[1] >> 7) & 1 == 1;
|
||||||
|
let mut cc_id = [0u8; 6];
|
||||||
|
cc_id.copy_from_slice(&data[14..20]);
|
||||||
|
|
||||||
|
Some(ContentCert {
|
||||||
|
bus_encryption,
|
||||||
|
cc_id,
|
||||||
|
version,
|
||||||
|
})
|
||||||
|
}
|
||||||
+325
@@ -0,0 +1,325 @@
|
|||||||
|
//! AACS Media Key Block — [C] Chapter 3.
|
||||||
|
//!
|
||||||
|
//! The MKB record format (framing walker, the `MkbRecord` view, record-body
|
||||||
|
//! finders), the MKBType / AACS-generation classification, and MKB-file
|
||||||
|
//! utilities (content length, trimming, version). Consolidated here from the
|
||||||
|
//! former `keys.rs` / `variant.rs` so the one place that understands MKB bytes
|
||||||
|
//! is `mkb`. A follow-up collapses the remaining duplicate finders (see the
|
||||||
|
//! private refactor notes) — for now both dialects live here side by side.
|
||||||
|
|
||||||
|
/// A single MKB record produced by [`walk_mkb`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MkbRecord {
|
||||||
|
/// Byte offset of the record within the MKB.
|
||||||
|
pub offset: usize,
|
||||||
|
/// Record type byte.
|
||||||
|
pub rec_type: u8,
|
||||||
|
/// Record length in bytes (includes the 4-byte header).
|
||||||
|
pub rec_len: usize,
|
||||||
|
/// Record body (the bytes after the 4-byte header).
|
||||||
|
pub body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk an MKB into a flat list of records.
|
||||||
|
///
|
||||||
|
/// MKB record framing per AACS: 1 byte type, 3 bytes BE length
|
||||||
|
/// INCLUDING the 4-byte header, followed by payload. The walker stops
|
||||||
|
/// at the first `(type=0, len=0)` end marker or at end of buffer.
|
||||||
|
pub fn walk_mkb(mkb: &[u8]) -> Vec<MkbRecord> {
|
||||||
|
mkb_records(mkb)
|
||||||
|
.map(|(offset, rec_type, rec_len)| MkbRecord {
|
||||||
|
offset,
|
||||||
|
rec_type,
|
||||||
|
rec_len,
|
||||||
|
body: mkb[offset + 4..offset + rec_len].to_vec(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// THE single MKB record-framing walker: yields `(offset, rec_type, rec_len)`
|
||||||
|
/// for each record — a 4-byte header (type byte + big-endian 24-bit length)
|
||||||
|
/// then the body — stopping at the `00 000000` end marker or a
|
||||||
|
/// malformed/out-of-bounds length. Lazy (no body clone), so a find-one-record
|
||||||
|
/// caller never materialises the multi-MB cvalue table. [`walk_mkb`] and every
|
||||||
|
/// MKB record walk in `aacs::keys` are built on this, so the framing rules — and
|
||||||
|
/// any future fix to them — live in exactly one place (they had drifted across
|
||||||
|
/// six hand-rolled copies).
|
||||||
|
pub(crate) fn mkb_records(mkb: &[u8]) -> impl Iterator<Item = (usize, u8, usize)> + '_ {
|
||||||
|
let mut pos = 0usize;
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
if pos + 4 > mkb.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rec_type = mkb[pos];
|
||||||
|
let rec_len = ((mkb[pos + 1] as usize) << 16)
|
||||||
|
| ((mkb[pos + 2] as usize) << 8)
|
||||||
|
| (mkb[pos + 3] as usize);
|
||||||
|
if rec_type == 0 && rec_len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if rec_len < 4 || pos + rec_len > mkb.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let here = pos;
|
||||||
|
pos += rec_len;
|
||||||
|
Some((here, rec_type, rec_len))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mkb_find_body(records: &[MkbRecord], rec_type: u8) -> Option<&[u8]> {
|
||||||
|
records
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.rec_type == rec_type && !r.body.is_empty())
|
||||||
|
.map(|r| r.body.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AACS protection generation a disc carries.
|
||||||
|
///
|
||||||
|
/// The content cert byte distinguishes V10 (`0x00`) from V20 (`0x01`). V21
|
||||||
|
/// cannot be detected from the cert alone — a V21 disc carries a V20 cert
|
||||||
|
/// and is upgraded to `V21` only after the MKB walk turns up the real Variant
|
||||||
|
/// records `0x2d` / `0x2f` (Encrypted Media Key Variant Data and the Variant
|
||||||
|
/// Key Data table).
|
||||||
|
///
|
||||||
|
/// Key-storage stride in `Unit_Key_RO.inf` is 48 bytes for V10 and 64
|
||||||
|
/// bytes for V20 / V21.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AacsVersion {
|
||||||
|
/// AACS 1.0 — original BD-ROM.
|
||||||
|
V10,
|
||||||
|
/// AACS 2.0 — UHD-BD, classical Media Key derivation.
|
||||||
|
V20,
|
||||||
|
/// AACS 2.1 — UHD-BD with Media Key Variant chain on top of V20.
|
||||||
|
V21,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AACS major version as the small integer threaded through the scan / key
|
||||||
|
/// paths (`AacsState.version`, `DiscInputs.version`, `DiscInputsCtx::new`):
|
||||||
|
/// 1 = AACS 1.0 (BD), 2 = AACS 2.x (UHD). Centralised so the bare `1`/`2` — and
|
||||||
|
/// the V10-vs-else stride choice it drives — lives in exactly one place.
|
||||||
|
pub const AACS_MAJOR_BD: u8 = 1;
|
||||||
|
|
||||||
|
pub const AACS_MAJOR_UHD: u8 = 2;
|
||||||
|
|
||||||
|
impl AacsVersion {
|
||||||
|
/// Stride (in bytes) between successive encrypted unit keys in
|
||||||
|
/// `Unit_Key_RO.inf`.
|
||||||
|
pub(crate) fn unit_key_stride(self) -> usize {
|
||||||
|
match self {
|
||||||
|
AacsVersion::V10 => 48,
|
||||||
|
AacsVersion::V20 | AacsVersion::V21 => 64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This version as the major integer ([`AACS_MAJOR_BD`] / [`AACS_MAJOR_UHD`]).
|
||||||
|
pub fn major(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
AacsVersion::V10 => AACS_MAJOR_BD,
|
||||||
|
AacsVersion::V20 | AacsVersion::V21 => AACS_MAJOR_UHD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The version a bare major integer selects for stride purposes: only the
|
||||||
|
/// BD major is V10; every other value takes the V20/V21 64-byte stride.
|
||||||
|
pub fn from_major(major: u8) -> Self {
|
||||||
|
if major == AACS_MAJOR_BD {
|
||||||
|
AacsVersion::V10
|
||||||
|
} else {
|
||||||
|
AacsVersion::V20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB.
|
||||||
|
/// 0x81: [C] §3.2.5.1.4. 0x86 (AACS 2.x): [libaacs] `mkb.c` — not in the public spec.
|
||||||
|
pub(crate) fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
// Verify-Media-Key record (0x81 for AACS 1.0, 0x86 for AACS 2.x): mk_dv is
|
||||||
|
// the 16 bytes at record offset 4 (body offset 0). Needs rec_len >= 20.
|
||||||
|
let found = mkb_records(mkb).find(|&(_, rt, len)| (rt == 0x81 || rt == 0x86) && len >= 20);
|
||||||
|
match found {
|
||||||
|
Some((o, rec_type, rec_len)) => {
|
||||||
|
let mut dv = [0u8; 16];
|
||||||
|
dv.copy_from_slice(&mkb[o + 4..o + 20]);
|
||||||
|
tracing::debug!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "mkb_mk_dv_found",
|
||||||
|
rec_type,
|
||||||
|
pos = o,
|
||||||
|
rec_len,
|
||||||
|
"mk_dv extracted from MKB"
|
||||||
|
);
|
||||||
|
Some(dv)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "mkb_mk_dv_not_found",
|
||||||
|
"no 0x81/0x86 record with rec_len>=20 found"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find Subset-Difference records (type 0x04) in MKB. [C] §3.2.5.1.5.
|
||||||
|
pub(crate) fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
find_record_body(mkb, 0x04)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the Media Key Data Record (cvalues table) in an MKB. [C] §3.2.4 / §3.2.5.1.7.
|
||||||
|
///
|
||||||
|
/// The cvalue table is record type `0x05` (Media Key Data) on BOTH AACS
|
||||||
|
/// 1.0 and AACS 2.x MKBs — its 16-byte cvalue entries are 1:1 with the
|
||||||
|
/// 5-byte Subset-Difference index entries in record `0x04`. This matches
|
||||||
|
/// libaacs, whose `mkb_cvalues()` reads `0x05` and `mkb_subdiff_records()`
|
||||||
|
/// reads `0x04`.
|
||||||
|
///
|
||||||
|
/// On AACS 2.x in-drive UHD MKBs the `0x05` table is large (the full
|
||||||
|
/// subset-difference cvalue set: ~181k entries on a retail MKB, 1:1 with
|
||||||
|
/// the giant `0x04` index), while record `0x07` (Explicit
|
||||||
|
/// Subset-Difference Record) is a much smaller structure (~96 entries) and
|
||||||
|
/// is NOT the cvalue table. An earlier version of this function preferred
|
||||||
|
/// `0x07`, which under-tested the Subset-Difference walk on UHD discs and
|
||||||
|
/// prevented the DK→walk path from ever finding the matching uv. The
|
||||||
|
/// selection MUST therefore be `0x05`-first; `0x07` is only a fallback for
|
||||||
|
/// malformed/legacy MKBs that somehow lack a `0x05` record.
|
||||||
|
pub(crate) fn mkb_find_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if let Some(body) = find_record_body(mkb, 0x05) {
|
||||||
|
return Some(body);
|
||||||
|
}
|
||||||
|
find_record_body(mkb, 0x07)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk an MKB and return the payload (header stripped) of the first
|
||||||
|
/// record matching `rec_type`. Returns `None` if no such record exists or
|
||||||
|
/// the record is empty.
|
||||||
|
pub(crate) fn find_record_body(mkb: &[u8], rec_type_wanted: u8) -> Option<Vec<u8>> {
|
||||||
|
mkb_records(mkb)
|
||||||
|
.find(|&(_, rt, len)| rt == rec_type_wanted && len > 4)
|
||||||
|
.map(|(o, _, len)| mkb[o + 4..o + len].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real content length of an MKB: the byte offset where the record stream
|
||||||
|
/// ends. MKB files (especially `MKB_RW.inf`, but `MKB_RO.inf` too on some
|
||||||
|
/// discs) are allocated to a fixed size — often ~128 MiB — with the records at
|
||||||
|
/// the front and the rest zero padding. Walking records (type+len) and stopping
|
||||||
|
/// at the first padding byte (`type == 0` / zero-length / overrun) gives the
|
||||||
|
/// actual size so callers can trim off megabytes of zeros before sending or
|
||||||
|
/// archiving. Returns `mkb.len()` only if the whole buffer parsed as records.
|
||||||
|
pub fn mkb_content_len(mkb: &[u8]) -> usize {
|
||||||
|
// End of the last framed record = where the fixed-region zero padding begins.
|
||||||
|
// (The `00 000000` terminator / overrun stops the walk; real MKBs pad with
|
||||||
|
// zeros, so this matches the prior "stop at the first padding byte".)
|
||||||
|
mkb_records(mkb)
|
||||||
|
.last()
|
||||||
|
.map(|(o, _, len)| o + len)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trim an MKB's trailing fixed-region padding to its real content length —
|
||||||
|
/// but ONLY when [`mkb_content_len`] actually found one. It returns 0 for an
|
||||||
|
/// MKB whose first record cannot be parsed; truncating to 0 in that case would
|
||||||
|
/// hand downstream consumers (and the online key service) an EMPTY MKB that can
|
||||||
|
/// never resolve. So a 0 (or a length that isn't strictly inside the buffer)
|
||||||
|
/// leaves the MKB untouched. A 0.31.0 regression dropped this guard and
|
||||||
|
/// `truncate`-d unconditionally, zeroing unrecognised MKBs.
|
||||||
|
pub fn trim_mkb(mut mkb: Vec<u8>) -> Vec<u8> {
|
||||||
|
let n = mkb_content_len(&mkb);
|
||||||
|
if n > 0 && n < mkb.len() {
|
||||||
|
mkb.truncate(n);
|
||||||
|
}
|
||||||
|
mkb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get MKB version from Type and Version Record (type 0x10).
|
||||||
|
/// Layout: 4-byte record header at `pos` (type + BE24 length), then the
|
||||||
|
/// record body starts at `pos + 4`. The body holds the BE u32 Type field at
|
||||||
|
/// body offset 0 (`pos + 4`), then the BE u32 version at body offset 4
|
||||||
|
/// (`pos + 8`).
|
||||||
|
pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
||||||
|
// Type-and-Version record (0x10): version is the BE u32 at body offset 4
|
||||||
|
// (record offset 8). Needs rec_len >= 12 (4 header + 4 type + 4 version).
|
||||||
|
mkb_records(mkb)
|
||||||
|
.find(|&(_, rt, len)| rt == 0x10 && len >= 12)
|
||||||
|
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 8], mkb[o + 9], mkb[o + 10], mkb[o + 11]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `0x00031003` — recordable media MKB (Class I & II compute Km directly).
|
||||||
|
pub const MKB_TYPE_3_RECORDABLE: u32 = 0x0003_1003;
|
||||||
|
|
||||||
|
/// `0x00041003` — AACS 1.0 pre-recorded content MKB (KCD-based). Standard BD.
|
||||||
|
pub const MKB_TYPE_4_PRERECORDED: u32 = 0x0004_1003;
|
||||||
|
|
||||||
|
/// `0x000A1003` — Class II / Unified MKB (Sequence-Key-Block functionality).
|
||||||
|
pub const MKB_TYPE_10_CLASS_II: u32 = 0x000A_1003;
|
||||||
|
|
||||||
|
/// `0x48141003` — AACS 2.0 Category C (UHD content). libaacs `MKB_20_CATEGORY_C`.
|
||||||
|
pub const MKB_20_CATEGORY_C: u32 = 0x4814_1003;
|
||||||
|
|
||||||
|
/// `0x48151003` — AACS 2.1 Category C (UHD content). libaacs `MKB_21_CATEGORY_C`.
|
||||||
|
pub const MKB_21_CATEGORY_C: u32 = 0x4815_1003;
|
||||||
|
|
||||||
|
/// The AACS MKB Type field, decoded.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MkbType {
|
||||||
|
/// Type 3 — recordable media.
|
||||||
|
Recordable,
|
||||||
|
/// Type 4 — AACS 1.0 pre-recorded content (KCD). Standard Blu-ray.
|
||||||
|
Prerecorded,
|
||||||
|
/// Type 10 — Class II / Unified (SKB).
|
||||||
|
ClassII,
|
||||||
|
/// AACS 2.0 Category C — UHD content.
|
||||||
|
CategoryC20,
|
||||||
|
/// AACS 2.1 Category C — UHD content.
|
||||||
|
CategoryC21,
|
||||||
|
/// Unrecognized MKBType value (raw field preserved).
|
||||||
|
Other(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MkbType {
|
||||||
|
pub(crate) fn from_raw(raw: u32) -> Self {
|
||||||
|
match raw {
|
||||||
|
MKB_TYPE_3_RECORDABLE => MkbType::Recordable,
|
||||||
|
MKB_TYPE_4_PRERECORDED => MkbType::Prerecorded,
|
||||||
|
MKB_TYPE_10_CLASS_II => MkbType::ClassII,
|
||||||
|
MKB_20_CATEGORY_C => MkbType::CategoryC20,
|
||||||
|
MKB_21_CATEGORY_C => MkbType::CategoryC21,
|
||||||
|
other => MkbType::Other(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AACS generation this MKB belongs to (Category C → 2.0/2.1, else 1.0).
|
||||||
|
pub fn generation(self) -> AacsVersion {
|
||||||
|
match self {
|
||||||
|
MkbType::CategoryC21 => AacsVersion::V21,
|
||||||
|
MkbType::CategoryC20 => AacsVersion::V20,
|
||||||
|
_ => AacsVersion::V10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` for UHD (AACS 2.x Category C); `false` for Blu-ray (AACS 1.x).
|
||||||
|
pub fn is_uhd(self) -> bool {
|
||||||
|
matches!(self, MkbType::CategoryC20 | MkbType::CategoryC21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw 32-bit MKBType field from the Type-and-Version record (0x10), bytes
|
||||||
|
/// 4-7. `None` if no 0x10 record is present. [C] §3.2.5.1.1 Table 3-2.
|
||||||
|
pub fn mkb_type_raw(mkb: &[u8]) -> Option<u32> {
|
||||||
|
// Type-and-Version record (0x10): the 32-bit MKBType is bytes 4-7 (body
|
||||||
|
// offset 0). Needs rec_len >= 8 (4 header + 4 type).
|
||||||
|
mkb_records(mkb)
|
||||||
|
.find(|&(_, rt, len)| rt == 0x10 && len >= 8)
|
||||||
|
.map(|(o, _, _)| u32::from_be_bytes([mkb[o + 4], mkb[o + 5], mkb[o + 6], mkb[o + 7]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode an MKB's Type field. `None` if no Type-and-Version record is present.
|
||||||
|
pub fn mkb_type(mkb: &[u8]) -> Option<MkbType> {
|
||||||
|
mkb_type_raw(mkb).map(MkbType::from_raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Some(true)` if this MKB is a UHD (AACS 2.x Category C) block, `Some(false)`
|
||||||
|
/// for Blu-ray (AACS 1.x), `None` if the Type record is absent.
|
||||||
|
pub fn mkb_is_uhd(mkb: &[u8]) -> Option<bool> {
|
||||||
|
mkb_type(mkb).map(MkbType::is_uhd)
|
||||||
|
}
|
||||||
+49
-59
@@ -13,15 +13,29 @@
|
|||||||
//!
|
//!
|
||||||
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
||||||
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
||||||
|
//!
|
||||||
|
//! ## Spec provenance
|
||||||
|
//!
|
||||||
|
//! The crypto below carries `[TAG] §x.y` citations back to the published AACS
|
||||||
|
//! specification (Final Rev 0.953), so each primitive links to the section it
|
||||||
|
//! implements:
|
||||||
|
//! - `[C]` — AACS Introduction and Common Cryptographic Elements Book (primitives, MKB/key-management).
|
||||||
|
//! - `[PR]` — AACS Pre-recorded Video Book (Volume/Title Key layer).
|
||||||
|
//! - `[BD]` — AACS Blu-ray Disc Pre-recorded Book (CPS Unit Key, Aligned Unit, Block Key).
|
||||||
|
//! - `[libaacs]` — the libaacs reference implementation, cited only where the spec
|
||||||
|
//! is silent (the `0x86` verify record and the Category-C MKBType names).
|
||||||
|
|
||||||
pub mod boil;
|
pub mod content;
|
||||||
pub mod decrypt;
|
pub mod crypto;
|
||||||
|
pub mod derive;
|
||||||
pub mod host_certs;
|
pub mod host_certs;
|
||||||
pub mod keys;
|
pub mod inf;
|
||||||
|
pub mod mkb;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
pub mod resolve;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod variants;
|
pub mod variant;
|
||||||
|
|
||||||
/// On-disc UDF paths to the AACS key-input files (with their fallbacks).
|
/// On-disc UDF paths to the AACS key-input files (with their fallbacks).
|
||||||
/// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`,
|
/// Centralised so every reader (`resolve_vid_only`, `read_aacs_inputs`,
|
||||||
@@ -35,53 +49,29 @@ pub const PATH_MKB_RW: &str = "/AACS/MKB_RW.inf";
|
|||||||
pub const PATH_CONTENT_CERT: &str = "/AACS/Content000.cer";
|
pub const PATH_CONTENT_CERT: &str = "/AACS/Content000.cer";
|
||||||
pub const PATH_CONTENT_CERT_ALT: &str = "/AACS/Content001.cer";
|
pub const PATH_CONTENT_CERT_ALT: &str = "/AACS/Content001.cer";
|
||||||
|
|
||||||
// Boil-down derivation primitives (thin newtypes + wrappers over the crypto).
|
// The module structure IS the public API — consumers import from the owning
|
||||||
pub use boil::{
|
// module directly (e.g. `aacs::content::decrypt_unit`, `aacs::mkb::MkbType`,
|
||||||
KeyCandidate, MediaKey, ProcessingKey, ResolvedChain, UnitKey, Vid, Vuk, mk_from_dk,
|
// `aacs::derive::{derive_vuk, resolve_candidate}`, `aacs::resolve::resolve_keys_v2`).
|
||||||
mk_from_pk, resolve_candidate, uk_from_vuk, vuk_from_mk,
|
// The `derive::probe` reproduction harness stays reachable via its module path.
|
||||||
};
|
//
|
||||||
// Structured, English-free resolution trace.
|
// A small set of flat re-exports is kept for the typed key primitives and the
|
||||||
pub use trace::{KeyNode, KeyOutcome, KeyStep, ResolutionTrace, UnlockOutcome, UnlockStep};
|
// content-decrypt entry points that downstream key-source crates import through
|
||||||
|
// the `aacs::` path. These are the stable, load-bearing names; keeping them here
|
||||||
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
// lets those crates track the module refactor without a lockstep re-pin.
|
||||||
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
pub use content::{ALIGNED_UNIT_LEN, decrypt_unit_try_keys};
|
||||||
pub use decrypt::{
|
pub use derive::derive_vuk;
|
||||||
ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, UnitKeyResult, aacs_unit_encrypted,
|
pub use types::{DeviceKey, HostCert, MediaKey, ProcessingKey, UnitKey, Vid, Vuk};
|
||||||
aacs_unit_needs_decrypt, aacs_unit_still_ciphertext, decrypt_bus, decrypt_unit,
|
|
||||||
decrypt_unit_checked, decrypt_unit_full, decrypt_unit_try_keys, fill_null_ts_unit,
|
|
||||||
is_unit_aligned, ts_packet_total, ts_sync_count, ts_sync_destroyed, unit_is_clean_ps,
|
|
||||||
unit_is_clean_ts, unit_key_validates,
|
|
||||||
};
|
|
||||||
// `probe` is a reproduction-harness helper (see keys.rs), not part of the
|
|
||||||
// documented 1.0 surface; keep it reachable but off the rendered docs so we
|
|
||||||
// don't commit semver stability to test primitives.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use keys::probe;
|
|
||||||
pub use keys::{
|
|
||||||
AACS_MAJOR_BD, AACS_MAJOR_UHD, AacsVersion, ContentCert, MKB_20_CATEGORY_C, MKB_21_CATEGORY_C,
|
|
||||||
MKB_TYPE_3_RECORDABLE, MKB_TYPE_4_PRERECORDED, MKB_TYPE_10_CLASS_II, MkbType, ResolveContext,
|
|
||||||
ResolveFailure, ResolvedKeys, UnitKeyFile, decrypt_unit_key, 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_is_uhd, mkb_type, mkb_type_raw, mkb_version, parse_content_cert,
|
|
||||||
parse_unit_key_ro, read_mkb_from_drive, recover_dk_position, resolve_keys_v1, resolve_keys_v2,
|
|
||||||
resolve_keys_v21, resolve_keys_with_reason, trim_mkb,
|
|
||||||
};
|
|
||||||
pub use provider::KeyProvider;
|
|
||||||
pub use types::{DeviceKey, DiscEntry, HostCert};
|
|
||||||
pub use variants::{
|
|
||||||
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, MkbRecord, ProcessingKeyMatch,
|
|
||||||
derive_media_key_variant, is_variant_mkb, variant_nonce, walk_mkb, walk_processing_key,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
//! Re-export surface guards. The module's public API is the set of
|
//! Surface guards. The public API is the module tree itself (no facade).
|
||||||
//! `pub use` items above. A regression that drops or renames an export
|
//! Touching one representative item per module keeps these as a
|
||||||
//! (the class of bug that shipped in 0.31.0 by silently changing a
|
//! compile-time contract that the module paths stay stable.
|
||||||
//! surface) breaks compilation of these references, so they act as a
|
|
||||||
//! compile-time contract for the crate's AACS surface.
|
|
||||||
|
|
||||||
use super::*;
|
use super::content::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
||||||
|
use super::inf::{disc_hash, disc_hash_hex};
|
||||||
|
use super::mkb::{AacsVersion, mkb_content_len, walk_mkb};
|
||||||
|
use super::variant::is_variant_mkb;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aligned_unit_len_is_three_2048_byte_sectors() {
|
fn aligned_unit_len_is_three_2048_byte_sectors() {
|
||||||
@@ -103,21 +93,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn key_correction_data_placeholder_is_all_zero() {
|
fn public_helpers_are_callable_by_module_path() {
|
||||||
// The variant chain refuses to run against this all-zero placeholder
|
// Touch a representative function from each module so a dropped/renamed
|
||||||
// KCD; the public constant must therefore be exactly 16 zero bytes.
|
// item fails to compile. Smoke calls, not behavioural assertions.
|
||||||
assert_eq!(KEY_CORRECTION_DATA_PLACEHOLDER, [0u8; 16]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn public_helpers_are_callable_through_the_facade() {
|
|
||||||
// Touch a representative function from each re-export group so a
|
|
||||||
// dropped/renamed export fails to compile. These are smoke calls, not
|
|
||||||
// behavioural assertions (behaviour is covered in each module).
|
|
||||||
let _ = ts_sync_destroyed(&[0u8; ALIGNED_UNIT_LEN]);
|
let _ = ts_sync_destroyed(&[0u8; ALIGNED_UNIT_LEN]);
|
||||||
let _ = mkb_content_len(&[]);
|
let _ = mkb_content_len(&[]);
|
||||||
let _ = is_variant_mkb(&walk_mkb(&[]));
|
let _ = is_variant_mkb(&walk_mkb(&[]));
|
||||||
let _ = disc_hash_hex(&disc_hash(b"x"));
|
let _ = disc_hash_hex(&disc_hash(b"x"));
|
||||||
let _ = mk_from_pk(&[[0u8; 16]], &[]);
|
let _ = super::derive::resolve_candidate(
|
||||||
|
&super::derive::KeyCandidate::Uk(super::types::UnitKey {
|
||||||
|
idx: 0,
|
||||||
|
key: [0u8; 16],
|
||||||
|
}),
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,38 @@ pub struct HostCert {
|
|||||||
pub certificate_v2: Option<Vec<u8>>,
|
pub certificate_v2: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Vid(pub [u8; 16]);
|
||||||
|
|
||||||
|
/// Media Key (Km, 16 bytes) — the MKB-scoped key derived from device keys.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct MediaKey(pub [u8; 16]);
|
||||||
|
|
||||||
|
/// Volume Unique Key (VUK / Kvu, 16 bytes) — derived from `MediaKey` + `Vid`,
|
||||||
|
/// decrypts the per-disc encrypted title keys in `Unit_Key_RO.inf`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Vuk(pub [u8; 16]);
|
||||||
|
|
||||||
|
/// Processing Key (Kp, 16 bytes) — an MKB Subset-Difference key that yields the
|
||||||
|
/// Media Key. A leaked/precomputed PK in the keydb, or the intermediate PK a
|
||||||
|
/// device-key walk derives at its matching SD node.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ProcessingKey(pub [u8; 16]);
|
||||||
|
|
||||||
|
/// One decrypted per-CPS-unit AACS title key.
|
||||||
|
///
|
||||||
|
/// `idx` is the POSITIONAL index of the encrypted title key within the slice
|
||||||
|
/// handed to the VUK→UK step (i.e. its order in `Unit_Key_RO.inf`'s key-storage
|
||||||
|
/// area). The CPS-unit *number* association is a higher-level concern owned by
|
||||||
|
/// [`super::inf::parse_unit_key_ro`], which pairs each positional key with its
|
||||||
|
/// declared CPS unit; this primitive only does the AES, so it surfaces position.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct UnitKey {
|
||||||
|
pub idx: u32,
|
||||||
|
pub key: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
/// A per-disc entry from the key database.
|
/// A per-disc entry from the key database.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DiscEntry {
|
pub struct DiscEntry {
|
||||||
|
|||||||
@@ -3,31 +3,19 @@
|
|||||||
//! On AACS 2.1 the Media Key derivation gains a second stage on top of
|
//! On AACS 2.1 the Media Key derivation gains a second stage on top of
|
||||||
//! the classical subset-difference walk. The classical walk yields a
|
//! the classical subset-difference walk. The classical walk yields a
|
||||||
//! Media Key Precursor (Kmp) rather than the final Media Key; the
|
//! Media Key Precursor (Kmp) rather than the final Media Key; the
|
||||||
//! Precursor combines with disc-supplied Variant Key Data (VKD) and an
|
//! Precursor combines with disc-supplied Variant Key Data (VKD) and the
|
||||||
//! integrator-supplied Key Correction Data (KCD) constant to produce
|
//! fixed Key Correction Data (KCD) constant to produce the Media Key.
|
||||||
//! the Media Key.
|
|
||||||
//!
|
//!
|
||||||
//! This module is wiring only — `resolve_keys` is not aware of it. The
|
//! The entry point is [`derive_media_key_variant`] — a `Kp -> Km`
|
||||||
//! entry point is [`derive_media_key_variant`]. The Variant scheme is
|
//! derivation. Deriving `Kp` itself from device keys (DK -> PK) is the
|
||||||
//! detected via the new MKB record types `0x82` (Encrypted Media Key
|
//! separate [`walk_processing_key`] step. The Variant scheme is detected
|
||||||
//! Variant Data + Variant Key Data) and `0x83` (Variant Number). When
|
//! via the AACS 2.1 MKB records `0x2d` (Encrypted Media Key Variant Data
|
||||||
//! a disc carries neither, callers should fall back to the classical
|
//! / C), `0x2f` (Variant Key Data table, up to 65,535×16), and `0x0c`
|
||||||
//! single-stage derivation in [`super::keys`].
|
//! (variant cvalues, one per `0x04` subset-difference slot). When a disc
|
||||||
|
//! carries none, callers fall back to the classical single-stage
|
||||||
|
//! derivation in [`super::derive`].
|
||||||
//!
|
//!
|
||||||
//! **Status: the chain cannot yet produce a key on a real disc.** Two
|
//! The chain:
|
||||||
//! sub-fields are unfinished:
|
|
||||||
//! - [`variants_for_uv`] (the `VARIANTS[uv]` lookup in the `0x83`
|
|
||||||
//! record) is a stub that always returns `None`, so the chain
|
|
||||||
//! short-circuits with [`MediaKeyVariantError::VariantsTableUnavailable`].
|
|
||||||
//! - The Encrypted Media Key Variant Data (C) and the Variant Key
|
|
||||||
//! Data (VKD) table are *distinct* sub-fields of the `0x82` record
|
|
||||||
//! per AACS 2.1, but [`variant_data_record`] (C) and
|
|
||||||
//! [`variant_key_data`] (VKD) both currently return the *whole*
|
|
||||||
//! first `0x82` body — so on a single-`0x82` disc they alias. The
|
|
||||||
//! `0x82` sub-field offsets must be fixed against a real Variant
|
|
||||||
//! disc before this chain is wired into `resolve_keys`.
|
|
||||||
//!
|
|
||||||
//! The chain follows the published spec:
|
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! Kmp = AES-128D(Kp, C) XOR uv
|
//! Kmp = AES-128D(Kp, C) XOR uv
|
||||||
@@ -38,165 +26,103 @@
|
|||||||
//! Km = AES-128D(Kpnew, VKD) XOR uv
|
//! Km = AES-128D(Kpnew, VKD) XOR uv
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Two condition bits on `Kmp[15]` route off the hardcoded-KCD path
|
//! **Status.** The record layout is pinned against real variant MKBs:
|
||||||
//! (Soft Correction and Online Challenge). The chain refuses to run in
|
//! `variants_for_uv` reads the `VARIANTS[uv]` table from `0x2d`, `C` from
|
||||||
//! either case — callers must handle those modes out of band.
|
//! the `0x2d` head, `VKD` from `0x2f`, and the Nonce from the `0x2d`
|
||||||
|
//! tail. The one input still missing is a covering 2.1 Processing Key to
|
||||||
|
//! run the chain end-to-end against the `0x86` Verify-Media-Key record —
|
||||||
|
//! which would also confirm the last layout picks (the 16-bit `Kvn` width
|
||||||
|
//! vs. a narrower spec value, and Nonce head-vs-tail). Until then the
|
||||||
|
//! final verify gate rejects any wrong pick, so a bad key is never
|
||||||
|
//! emitted — only an error.
|
||||||
//!
|
//!
|
||||||
//! # Status: Kp verification
|
//! Two condition bits on `Kmp[15]` route off the default KCD path (Soft
|
||||||
|
//! Correction and Online Challenge); the chain does not model those modes
|
||||||
|
//! and treats such a slot as non-covering.
|
||||||
//!
|
//!
|
||||||
//! On the classical path [`walk_processing_key`] gates each match on
|
//! **Verify gate.** On the classical path [`walk_processing_key`] gates
|
||||||
//! the VERIFY_MAGIC relation, which authenticates the Processing Key.
|
//! each match on the VERIFY_MAGIC relation, which authenticates the
|
||||||
//! On a variant MKB that magic check does NOT hold (the walk yields a
|
//! Processing Key. On a variant MKB that magic does NOT hold (the walk
|
||||||
//! Media Key *Precursor*, not the Media Key), so the walk accepts a
|
//! yields a Precursor, not the Media Key), so the authoritative gate is
|
||||||
//! variant match without it. The replacement gate lives at the END of
|
//! at the END of the chain: the derived `Km` is verified against the
|
||||||
//! [`derive_media_key_variant`]: the derived final `Km` is verified
|
//! MKB's Verify-Media-Key record before it is ever returned.
|
||||||
//! against the MKB's Verify-Media-Key record before any `(Km, Kvu)` is
|
|
||||||
//! returned. A future implementer wiring [`variants_for_uv`] must keep
|
|
||||||
//! that final gate — the per-match magic check no longer protects the
|
|
||||||
//! variant path.
|
|
||||||
|
|
||||||
use super::decrypt::aes_ecb_decrypt;
|
use super::crypto::{aes_ecb_decrypt, aes_g};
|
||||||
|
use super::mkb::*;
|
||||||
use super::types::DeviceKey;
|
use super::types::DeviceKey;
|
||||||
|
|
||||||
// ── Public constants ──────────────────────────────────────────────────────
|
// ── Public constants ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Placeholder Key Correction Data. Sixteen zero bytes.
|
/// AACS 2.1 Key Correction Data — a fixed algorithm constant.
|
||||||
///
|
const KEY_CORRECTION_DATA: [u8; 16] = [
|
||||||
/// Integrators MUST supply a non-placeholder KCD via the `kcd` argument
|
0x3b, 0x62, 0x8a, 0x78, 0x29, 0x00, 0xca, 0x2f, 0xdb, 0xe7, 0x7a, 0x49, 0xfe, 0x22, 0xd6, 0x6e,
|
||||||
/// to [`derive_media_key_variant`]; the chain refuses to operate when
|
];
|
||||||
/// the supplied KCD compares equal to this placeholder.
|
|
||||||
pub const KEY_CORRECTION_DATA_PLACEHOLDER: [u8; 16] = [0u8; 16];
|
|
||||||
|
|
||||||
// ── MKB record walking ────────────────────────────────────────────────────
|
// ── MKB record walking ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A single MKB record produced by [`walk_mkb`].
|
/// True iff `records` contains at least one Media Key Variant record.
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MkbRecord {
|
|
||||||
/// Byte offset of the record within the MKB.
|
|
||||||
pub offset: usize,
|
|
||||||
/// Record type byte.
|
|
||||||
pub rec_type: u8,
|
|
||||||
/// Record length in bytes (includes the 4-byte header).
|
|
||||||
pub rec_len: usize,
|
|
||||||
/// Record body (the bytes after the 4-byte header).
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk an MKB into a flat list of records.
|
|
||||||
///
|
///
|
||||||
/// MKB record framing per AACS: 1 byte type, 3 bytes BE length
|
/// The real AACS 2.1 Variant markers — confirmed against a live variant MKB —
|
||||||
/// INCLUDING the 4-byte header, followed by payload. The walker stops
|
/// are `0x2d` (Encrypted Media Key Variant Data / C) and `0x2f` (Variant Key
|
||||||
/// at the first `(type=0, len=0)` end marker or at end of buffer.
|
/// Data table, 65,535×16). Both are absent from non-variant 1.0/2.0 MKBs (which
|
||||||
pub fn walk_mkb(mkb: &[u8]) -> Vec<MkbRecord> {
|
/// instead carry `0x05` host-revocation-signature and no `0x0c`/`0x2d`/`0x2f`).
|
||||||
mkb_records(mkb)
|
/// The earlier `0x82`/`0x83` guess was speculative and never appeared in any
|
||||||
.map(|(offset, rec_type, rec_len)| MkbRecord {
|
/// real MKB.
|
||||||
offset,
|
|
||||||
rec_type,
|
|
||||||
rec_len,
|
|
||||||
body: mkb[offset + 4..offset + rec_len].to_vec(),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// THE single MKB record-framing walker: yields `(offset, rec_type, rec_len)`
|
|
||||||
/// for each record — a 4-byte header (type byte + big-endian 24-bit length)
|
|
||||||
/// then the body — stopping at the `00 000000` end marker or a
|
|
||||||
/// malformed/out-of-bounds length. Lazy (no body clone), so a find-one-record
|
|
||||||
/// caller never materialises the multi-MB cvalue table. [`walk_mkb`] and every
|
|
||||||
/// MKB record walk in `aacs::keys` are built on this, so the framing rules — and
|
|
||||||
/// any future fix to them — live in exactly one place (they had drifted across
|
|
||||||
/// six hand-rolled copies).
|
|
||||||
pub(crate) fn mkb_records(mkb: &[u8]) -> impl Iterator<Item = (usize, u8, usize)> + '_ {
|
|
||||||
let mut pos = 0usize;
|
|
||||||
std::iter::from_fn(move || {
|
|
||||||
if pos + 4 > mkb.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let rec_type = mkb[pos];
|
|
||||||
let rec_len = ((mkb[pos + 1] as usize) << 16)
|
|
||||||
| ((mkb[pos + 2] as usize) << 8)
|
|
||||||
| (mkb[pos + 3] as usize);
|
|
||||||
if rec_type == 0 && rec_len == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if rec_len < 4 || pos + rec_len > mkb.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let here = pos;
|
|
||||||
pos += rec_len;
|
|
||||||
Some((here, rec_type, rec_len))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True iff `records` contains at least one Media Key Variant record
|
|
||||||
/// (type `0x82` or `0x83`).
|
|
||||||
pub fn is_variant_mkb(records: &[MkbRecord]) -> bool {
|
pub fn is_variant_mkb(records: &[MkbRecord]) -> bool {
|
||||||
records.iter().any(|r| matches!(r.rec_type, 0x82 | 0x83))
|
records.iter().any(|r| matches!(r.rec_type, 0x2d | 0x2f))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body of the Encrypted Media Key Variant Data record (type `0x82`).
|
/// Body of the Encrypted Media Key Variant Data record (type `0x2d`).
|
||||||
///
|
///
|
||||||
/// Returns the whole first `0x82` body; the internal C / VKD sub-field
|
/// Confirmed against a live variant MKB as the `0x2d` record (92,220 bytes on
|
||||||
/// split is not yet decoded, so this aliases [`variant_key_data`] on a
|
/// the reference disc — not a clean multiple of 16, so it is a structured /
|
||||||
/// single-`0x82` disc. `pub(crate)` until the sub-field offsets are fixed
|
/// count-prefixed record, not a flat C-block table). The exact per-uv C
|
||||||
/// against a real variant disc — it is not part of the public surface
|
/// selection is the one sub-field still unconfirmed without a real disc+key to
|
||||||
/// because it knowingly returns an undecoded composite.
|
/// test against; the chain currently consumes the leading 16-byte block.
|
||||||
|
/// `pub(crate)` until that offset is pinned.
|
||||||
pub(crate) fn variant_data_record(records: &[MkbRecord]) -> Option<&[u8]> {
|
pub(crate) fn variant_data_record(records: &[MkbRecord]) -> Option<&[u8]> {
|
||||||
records
|
records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.rec_type == 0x82)
|
.find(|r| r.rec_type == 0x2d)
|
||||||
.map(|r| r.body.as_slice())
|
.map(|r| r.body.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 16-byte Nonce from the Variant Number record (type `0x83`). Returns
|
/// 16-byte Nonce for `Kvn = AES-G(Kp, Nonce)`.
|
||||||
/// the first 16 bytes of the body.
|
///
|
||||||
|
/// **UNCONFIRMED source.** The `0x2d` Encrypted-Media-Key-Variant-Data record is
|
||||||
|
/// the most likely home for a per-disc nonce, so this reads its trailing 16
|
||||||
|
/// bytes. Confirming this (vs. a fixed slice elsewhere in `0x2d`) needs a
|
||||||
|
/// covering key to run the whole chain against the `0x86` verify; until then a
|
||||||
|
/// wrong nonce can only fail that final gate, never emit a bad key.
|
||||||
pub fn variant_nonce(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
pub fn variant_nonce(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||||
let r = records.iter().find(|r| r.rec_type == 0x83)?;
|
let r = records.iter().find(|r| r.rec_type == 0x2d)?;
|
||||||
if r.body.len() < 16 {
|
if r.body.len() < 16 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut out = [0u8; 16];
|
let mut out = [0u8; 16];
|
||||||
out.copy_from_slice(&r.body[..16]);
|
out.copy_from_slice(&r.body[r.body.len() - 16..]);
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Body of the Variant Key Data record. Returns the first `0x82` body
|
/// The Variant Key Data (VKD) table — record type `0x2f`.
|
||||||
/// that is a non-empty multiple of 16 bytes.
|
|
||||||
///
|
///
|
||||||
/// Like [`variant_data_record`], this returns the whole `0x82` body and
|
/// Confirmed against a live variant MKB: exactly 65,535 × 16 = 1,048,560 bytes,
|
||||||
/// aliases it on a single-`0x82` disc; the C / VKD sub-field split is
|
/// indexed by the resolved `VKDidx`. This is disc-public data (it is why the
|
||||||
/// undecoded. `pub(crate)` until fixed against a real variant disc.
|
/// VKD alone buys nothing without the Media Key chain above it).
|
||||||
pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
|
pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
|
||||||
records
|
records
|
||||||
.iter()
|
.iter()
|
||||||
.find(|r| r.rec_type == 0x82 && !r.body.is_empty() && r.body.len() % 16 == 0)
|
.find(|r| r.rec_type == 0x2f && !r.body.is_empty() && r.body.len() % 16 == 0)
|
||||||
.map(|r| r.body.as_slice())
|
.map(|r| r.body.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AES-G ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// AES-G(x1, x2) = AES-128D(x1, x2) XOR x2.
|
|
||||||
///
|
|
||||||
/// The Media Key Variant chain uses AES-G to derive both the variant
|
|
||||||
/// number (`Kvn = AES-G(Kp, Nonce)`) and the Volume Unique Key
|
|
||||||
/// (`Kvu = AES-G(Km, VID)`). See [`super::keys::derive_vuk`] for the
|
|
||||||
/// classical VUK form — the math is identical, this exposes it as a
|
|
||||||
/// neutral primitive for the variant chain.
|
|
||||||
fn aes_g(x1: &[u8; 16], x2: &[u8; 16]) -> [u8; 16] {
|
|
||||||
let mut out = aes_ecb_decrypt(x1, x2);
|
|
||||||
for i in 0..16 {
|
|
||||||
out[i] ^= x2[i];
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Subset-difference walk that exposes (Kp, uv) ──────────────────────────
|
// ── Subset-difference walk that exposes (Kp, uv) ──────────────────────────
|
||||||
|
|
||||||
// `calc_v_mask` and `calc_pk_from_dk` (and the AES-G3 seed step they ride
|
// `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
|
// on) are shared with the classical walk in [`super::keys`] — a single
|
||||||
// definition keeps the variant SD tree byte-identical to the classical one.
|
// definition keeps the variant SD tree byte-identical to the classical one.
|
||||||
// (`aesg3` itself is imported separately in the test module.)
|
// (`aesg3` itself is imported separately in the test module.)
|
||||||
use super::keys::{calc_pk_from_dk, calc_v_mask};
|
use super::derive::{calc_pk_from_dk, calc_v_mask};
|
||||||
|
|
||||||
/// Outcome of a subset-difference walk against an MKB. Carries the
|
/// Outcome of a subset-difference walk against an MKB. Carries the
|
||||||
/// processing key and the matching `uv` slot — both needed as inputs
|
/// processing key and the matching `uv` slot — both needed as inputs
|
||||||
@@ -213,13 +139,6 @@ pub struct ProcessingKeyMatch {
|
|||||||
pub cvalue_index: usize,
|
pub cvalue_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mkb_find_body(records: &[MkbRecord], rec_type: u8) -> Option<&[u8]> {
|
|
||||||
records
|
|
||||||
.iter()
|
|
||||||
.find(|r| r.rec_type == rec_type && !r.body.is_empty())
|
|
||||||
.map(|r| r.body.as_slice())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
||||||
let r = records
|
let r = records
|
||||||
.iter()
|
.iter()
|
||||||
@@ -245,7 +164,7 @@ fn mkb_find_mk_dv(records: &[MkbRecord]) -> Option<[u8; 16]> {
|
|||||||
/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be
|
/// note on [`super::keys::probe::mkb_cvalues`]). They must NOT be
|
||||||
/// unified to one order — each is correct for its own MKB shape.
|
/// unified to one order — each is correct for its own MKB shape.
|
||||||
/// - finders: this walk operates on parsed [`MkbRecord`]s (needed
|
/// - finders: this walk operates on parsed [`MkbRecord`]s (needed
|
||||||
/// because the variant chain also reads `0x82`/`0x83`); the
|
/// because the variant chain also reads `0x2d`/`0x2f`); the
|
||||||
/// classical walk operates on raw MKB bytes. Same framing, different
|
/// classical walk operates on raw MKB bytes. Same framing, different
|
||||||
/// input type.
|
/// input type.
|
||||||
///
|
///
|
||||||
@@ -258,7 +177,13 @@ pub fn walk_processing_key(
|
|||||||
) -> Option<ProcessingKeyMatch> {
|
) -> Option<ProcessingKeyMatch> {
|
||||||
let mk_dv = mkb_find_mk_dv(records)?;
|
let mk_dv = mkb_find_mk_dv(records)?;
|
||||||
let uvs = mkb_find_body(records, 0x04)?;
|
let uvs = mkb_find_body(records, 0x04)?;
|
||||||
let cvalues = mkb_find_body(records, 0x07).or_else(|| mkb_find_body(records, 0x05))?;
|
// Variant cvalue source: a real variant MKB carries its per-uv cvalue table
|
||||||
|
// in record `0x0c` (confirmed 46,101×16, one per `0x04` subset-difference
|
||||||
|
// slot). Fall back to `0x07`/`0x05` for the synthetic fixtures and any MKB
|
||||||
|
// shape that keeps its cvalues there.
|
||||||
|
let cvalues = mkb_find_body(records, 0x0c)
|
||||||
|
.or_else(|| mkb_find_body(records, 0x07))
|
||||||
|
.or_else(|| mkb_find_body(records, 0x05))?;
|
||||||
|
|
||||||
let num_uvs = uvs
|
let num_uvs = uvs
|
||||||
.chunks(5)
|
.chunks(5)
|
||||||
@@ -371,10 +296,8 @@ pub enum MediaKeyVariantError {
|
|||||||
/// `Kmp[15]` carries bit `0x04`: the online-challenge path applies
|
/// `Kmp[15]` carries bit `0x04`: the online-challenge path applies
|
||||||
/// for this Precursor. Out of scope for the hardcoded-KCD chain.
|
/// for this Precursor. Out of scope for the hardcoded-KCD chain.
|
||||||
OnlineChallengeRequired,
|
OnlineChallengeRequired,
|
||||||
/// Supplied KCD equals [`KEY_CORRECTION_DATA_PLACEHOLDER`]. The
|
/// `VARIANTS[uv]` could not be read from the `0x2d` record for the
|
||||||
/// derivation refuses to run with the all-zero placeholder.
|
/// matched slot.
|
||||||
KcdNotProvided,
|
|
||||||
/// `VARIANTS[uv]` lookup for the matched uv is not implemented.
|
|
||||||
VariantsTableUnavailable,
|
VariantsTableUnavailable,
|
||||||
/// VKD index resolved out of the supplied `vkd_table`.
|
/// VKD index resolved out of the supplied `vkd_table`.
|
||||||
VkdIndexOutOfRange,
|
VkdIndexOutOfRange,
|
||||||
@@ -392,7 +315,6 @@ impl std::fmt::Display for MediaKeyVariantError {
|
|||||||
MediaKeyVariantError::ProcessingKeyUnavailable => 7102,
|
MediaKeyVariantError::ProcessingKeyUnavailable => 7102,
|
||||||
MediaKeyVariantError::SoftCorrectionRequired => 7103,
|
MediaKeyVariantError::SoftCorrectionRequired => 7103,
|
||||||
MediaKeyVariantError::OnlineChallengeRequired => 7104,
|
MediaKeyVariantError::OnlineChallengeRequired => 7104,
|
||||||
MediaKeyVariantError::KcdNotProvided => 7105,
|
|
||||||
MediaKeyVariantError::VariantsTableUnavailable => 7106,
|
MediaKeyVariantError::VariantsTableUnavailable => 7106,
|
||||||
MediaKeyVariantError::VkdIndexOutOfRange => 7107,
|
MediaKeyVariantError::VkdIndexOutOfRange => 7107,
|
||||||
MediaKeyVariantError::MediaKeyVerifyFailed => 7108,
|
MediaKeyVariantError::MediaKeyVerifyFailed => 7108,
|
||||||
@@ -405,50 +327,153 @@ impl std::error::Error for MediaKeyVariantError {}
|
|||||||
|
|
||||||
// ── Chain ─────────────────────────────────────────────────────────────────
|
// ── Chain ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Look up the per-slot `VARIANTS` value for the matched subset-difference
|
/// Look up the per-slot `VARIANTS` value for the matched subset-difference slot,
|
||||||
/// slot. AACS 2.1 keys the VARIANTS table by the matched SD slot (the same
|
/// keyed by the same index that selected the cvalue ([`ProcessingKeyMatch::cvalue_index`]).
|
||||||
/// index that selected the cvalue), so the caller passes
|
|
||||||
/// [`ProcessingKeyMatch::cvalue_index`]. The byte layout of the per-slot entry
|
|
||||||
/// in the Variant Number record is undocumented and disc-specific; this helper
|
|
||||||
/// returns `None` until a Variant disc is available to fix the layout against.
|
|
||||||
///
|
///
|
||||||
/// `sd_slot_index` is the matched subset-difference slot (== cvalue index).
|
/// LAYOUT (fixed against a real 2.1 variant MKB — Zombieland v70, `MKB_RO.inf`):
|
||||||
fn variants_for_uv(_records: &[MkbRecord], _sd_slot_index: usize) -> Option<u16> {
|
/// the `0x2d` Encrypted-Media-Key-Variant-Data body is exactly
|
||||||
None
|
/// `46_100*2 + 16 = 92_216` bytes, i.e. one **big-endian u16 `VARIANTS` entry per
|
||||||
|
/// subset-difference slot** (1:1 with the `0x0c` variant cvalues and the `0x04`
|
||||||
|
/// subset-differences), with the 16-byte per-disc Nonce packed at the **tail**
|
||||||
|
/// (see [`variant_nonce`]). So the VARIANTS table is the leading `sd_count*2`
|
||||||
|
/// bytes and this reads its `sd_slot_index`-th entry.
|
||||||
|
///
|
||||||
|
/// The record/field *sizing* is confirmed; the one bit still to pin against a
|
||||||
|
/// covering key is Nonce-head-vs-tail (both fit the size) — a wrong pick can only
|
||||||
|
/// yield a wrong `Km`, which the final Verify-Media-Key gate rejects (never a
|
||||||
|
/// silent bad key).
|
||||||
|
fn variants_for_uv(records: &[MkbRecord], sd_slot_index: usize) -> Option<u16> {
|
||||||
|
let body = records.iter().find(|r| r.rec_type == 0x2d)?.body.as_slice();
|
||||||
|
let off = sd_slot_index.checked_mul(2)?;
|
||||||
|
let bytes = body.get(off..off + 2)?;
|
||||||
|
Some(u16::from_be_bytes([bytes[0], bytes[1]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the Media Key Variant chain on an MKB.
|
/// Enumerate the `(uv, slot_index)` pairs of a variant MKB's subset-difference
|
||||||
|
/// record (`0x04`), in table order — the same parse [`walk_processing_key`] uses
|
||||||
|
/// to index cvalues. Factored out so a bare Processing Key (which arrives without
|
||||||
|
/// its slot) can be tried against each slot.
|
||||||
|
fn variant_uv_slots(records: &[MkbRecord]) -> Option<Vec<(u32, usize)>> {
|
||||||
|
let uvs = mkb_find_body(records, 0x04)?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut idx = 0usize;
|
||||||
|
while (idx + 1) * 5 <= uvs.len() {
|
||||||
|
let u_mask_shift = uvs[5 * idx];
|
||||||
|
// The `0xC0` revoked-marker terminates the table (matches the walk's
|
||||||
|
// `take_while`). Shifts ≥ 32 are out of range and skipped, never wrapped.
|
||||||
|
if u_mask_shift & 0xC0 != 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let p_uv = &uvs[1 + 5 * idx..];
|
||||||
|
let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
|
||||||
|
if uv != 0 && u_mask_shift < 32 {
|
||||||
|
out.push((uv, idx));
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The MKB-derived inputs the variant chain needs for every slot it tries against
|
||||||
|
/// a given Processing Key. Fetched once by [`derive_media_key_variant`] so the
|
||||||
|
/// per-slot body stays a lean `(Kp, uv, slot)` call.
|
||||||
|
struct VariantMkb<'a> {
|
||||||
|
records: &'a [MkbRecord],
|
||||||
|
nonce: [u8; 16],
|
||||||
|
vkd_table: &'a [u8],
|
||||||
|
c_block: [u8; 16],
|
||||||
|
mk_dv: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The variant chain body for ONE known `(Kp, uv, slot)`: derive and verify the
|
||||||
|
/// Media Key against the MKB's Verify-Media-Key record. VID-free — the Km is
|
||||||
|
/// MKB-scoped; the VUK is a separate [`super::derive::derive_vuk`] step. Returns
|
||||||
|
/// the verified Km, or a classification of why this slot did not yield one.
|
||||||
|
fn variant_km_for_slot(
|
||||||
|
m: &VariantMkb<'_>,
|
||||||
|
kp: &[u8; 16],
|
||||||
|
uv: u32,
|
||||||
|
slot_index: usize,
|
||||||
|
) -> Result<[u8; 16], MediaKeyVariantError> {
|
||||||
|
// Step: Kmp = AES-128D(Kp, C) XOR uv (uv into low 4 bytes).
|
||||||
|
let mut kmp = aes_ecb_decrypt(kp, &m.c_block);
|
||||||
|
let uv_bytes = uv.to_be_bytes();
|
||||||
|
for i in 0..4 {
|
||||||
|
kmp[12 + i] ^= uv_bytes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Condition bits on Kmp[15] select the correction mode. Bit 0x02 (SoftKCD)
|
||||||
|
// and 0x04 (online challenge) need out-of-band data we don't model; the
|
||||||
|
// default path (neither bit set) uses the fixed KCD constant.
|
||||||
|
if kmp[15] & 0b0000_0010 != 0 {
|
||||||
|
return Err(MediaKeyVariantError::SoftCorrectionRequired);
|
||||||
|
}
|
||||||
|
if kmp[15] & 0b0000_0100 != 0 {
|
||||||
|
return Err(MediaKeyVariantError::OnlineChallengeRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Kpnew = Kmp XOR KCD.
|
||||||
|
let mut kpnew = [0u8; 16];
|
||||||
|
for i in 0..16 {
|
||||||
|
kpnew[i] = kmp[i] ^ KEY_CORRECTION_DATA[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Kvn = AES-G(Kp, Nonce) & 0xFFFF (low 16 bits, BE).
|
||||||
|
let kvn_block = aes_g(kp, &m.nonce);
|
||||||
|
let kvn = u16::from_be_bytes([kvn_block[14], kvn_block[15]]);
|
||||||
|
|
||||||
|
// Step: VKD_idx = Kvn XOR VARIANTS[uv]; VKD = vkd_table[VKD_idx].
|
||||||
|
let v_for_uv = variants_for_uv(m.records, slot_index)
|
||||||
|
.ok_or(MediaKeyVariantError::VariantsTableUnavailable)?;
|
||||||
|
let vkd_idx = kvn ^ v_for_uv;
|
||||||
|
let off = (vkd_idx as usize) * 16;
|
||||||
|
if off + 16 > m.vkd_table.len() {
|
||||||
|
return Err(MediaKeyVariantError::VkdIndexOutOfRange);
|
||||||
|
}
|
||||||
|
let mut vkd = [0u8; 16];
|
||||||
|
vkd.copy_from_slice(&m.vkd_table[off..off + 16]);
|
||||||
|
|
||||||
|
// Step: Km = AES-128D(Kpnew, VKD) XOR uv.
|
||||||
|
let mut km = aes_ecb_decrypt(&kpnew, &vkd);
|
||||||
|
for i in 0..4 {
|
||||||
|
km[12 + i] ^= uv_bytes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gate: the derived Media Key MUST reproduce the MKB's Verify-Media-Key magic
|
||||||
|
// (the per-match magic in `walk_processing_key` only saw the Precursor). This
|
||||||
|
// is the authoritative check — no unverified key is ever returned.
|
||||||
|
const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
||||||
|
if aes_ecb_decrypt(&km, &m.mk_dv)[..8] != VERIFY_MAGIC {
|
||||||
|
return Err(MediaKeyVariantError::MediaKeyVerifyFailed);
|
||||||
|
}
|
||||||
|
Ok(km)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive the AACS 2.1 variant **Media Key** from a Processing Key.
|
||||||
///
|
///
|
||||||
/// Inputs:
|
/// The one deterministic `Kp → Km` derivation for a variant MKB. A leaked 2.1
|
||||||
|
/// Processing Key arrives without its subset-difference slot, so this tries `pk`
|
||||||
|
/// against every slot and returns the Km for the slot whose full chain passes the
|
||||||
|
/// MKB's Verify-Media-Key record — exactly the shape of the classical bare-PK
|
||||||
|
/// [`super::derive::derive_media_key_from_pk`], gated by the chain's own verify so
|
||||||
|
/// an unverified key is never returned.
|
||||||
///
|
///
|
||||||
/// - `mkb_records` : MKB pre-walked via [`walk_mkb`].
|
/// VID-free by design: the Media Key is MKB-scoped. Derive the per-disc VUK from
|
||||||
/// - `device_keys` : pool of device keys; the chain runs against the
|
/// the returned Km with [`super::derive::derive_vuk`]. Deriving a Processing Key
|
||||||
/// first uv slot any DK covers.
|
/// from device keys (DK → PK) is a separate concern — walk it first via
|
||||||
/// - `kcd` : integrator-supplied Key Correction Data. Must not
|
/// [`walk_processing_key`], then call this.
|
||||||
/// equal [`KEY_CORRECTION_DATA_PLACEHOLDER`].
|
|
||||||
/// - `vid` : 16-byte Volume ID for the disc. Used to derive
|
|
||||||
/// the final VUK alongside the Media Key.
|
|
||||||
///
|
///
|
||||||
/// Returns `(Km, Kvu)` on success.
|
/// Errors: `NotVariantMkb` (caller should use the classical path), `MkbIncomplete`
|
||||||
///
|
/// (a required record is missing), or `ProcessingKeyUnavailable` (no slot verified
|
||||||
/// NOTE: the `VARIANTS[uv]` lookup ([`variants_for_uv`]) is not yet
|
/// — `pk` does not cover this MKB, or its slot needs the soft-correction / online
|
||||||
/// implemented, so on a real Variant disc this always returns
|
/// path, surfaced as `SoftCorrectionRequired` / `OnlineChallengeRequired`).
|
||||||
/// `Err(`[`MediaKeyVariantError::VariantsTableUnavailable`]`)` before a
|
|
||||||
/// key is produced. The chain can only succeed against synthetic test
|
|
||||||
/// fixtures today.
|
|
||||||
pub fn derive_media_key_variant(
|
pub fn derive_media_key_variant(
|
||||||
mkb_records: &[MkbRecord],
|
mkb_records: &[MkbRecord],
|
||||||
device_keys: &[DeviceKey],
|
pk: &[u8; 16],
|
||||||
kcd: &[u8; 16],
|
) -> Result<[u8; 16], MediaKeyVariantError> {
|
||||||
vid: &[u8; 16],
|
|
||||||
) -> Result<([u8; 16], [u8; 16]), MediaKeyVariantError> {
|
|
||||||
if !is_variant_mkb(mkb_records) {
|
if !is_variant_mkb(mkb_records) {
|
||||||
return Err(MediaKeyVariantError::NotVariantMkb);
|
return Err(MediaKeyVariantError::NotVariantMkb);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pkm = walk_processing_key(mkb_records, device_keys)
|
|
||||||
.ok_or(MediaKeyVariantError::ProcessingKeyUnavailable)?;
|
|
||||||
|
|
||||||
let nonce = variant_nonce(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let nonce = variant_nonce(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let vkd_table = variant_key_data(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let vkd_table = variant_key_data(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
let c_value = variant_data_record(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let c_value = variant_data_record(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
@@ -457,69 +482,31 @@ pub fn derive_media_key_variant(
|
|||||||
}
|
}
|
||||||
let mut c_block = [0u8; 16];
|
let mut c_block = [0u8; 16];
|
||||||
c_block.copy_from_slice(&c_value[..16]);
|
c_block.copy_from_slice(&c_value[..16]);
|
||||||
|
|
||||||
// Step: Kmp = AES-128D(Kp, C) XOR uv (uv into low 4 bytes).
|
|
||||||
let mut kmp = aes_ecb_decrypt(&pkm.kp, &c_block);
|
|
||||||
let uv_bytes = pkm.uv.to_be_bytes();
|
|
||||||
for i in 0..4 {
|
|
||||||
kmp[12 + i] ^= uv_bytes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condition bits on Kmp[15] route off the hardcoded-KCD path.
|
|
||||||
if kmp[15] & 0b0000_0010 != 0 {
|
|
||||||
return Err(MediaKeyVariantError::SoftCorrectionRequired);
|
|
||||||
}
|
|
||||||
if kmp[15] & 0b0000_0100 != 0 {
|
|
||||||
return Err(MediaKeyVariantError::OnlineChallengeRequired);
|
|
||||||
}
|
|
||||||
if kcd == &KEY_CORRECTION_DATA_PLACEHOLDER {
|
|
||||||
return Err(MediaKeyVariantError::KcdNotProvided);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step: Kpnew = Kmp XOR KCD.
|
|
||||||
let mut kpnew = [0u8; 16];
|
|
||||||
for i in 0..16 {
|
|
||||||
kpnew[i] = kmp[i] ^ kcd[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step: Kvn = AES-G(Kp, Nonce) & 0xFFFF (low 16 bits, BE).
|
|
||||||
let kvn_block = aes_g(&pkm.kp, &nonce);
|
|
||||||
let kvn = u16::from_be_bytes([kvn_block[14], kvn_block[15]]);
|
|
||||||
|
|
||||||
// Step: VKD_idx = Kvn XOR VARIANTS[uv].
|
|
||||||
let v_for_uv = variants_for_uv(mkb_records, pkm.cvalue_index)
|
|
||||||
.ok_or(MediaKeyVariantError::VariantsTableUnavailable)?;
|
|
||||||
let vkd_idx = kvn ^ v_for_uv;
|
|
||||||
|
|
||||||
// Step: VKD = vkd_table[VKD_idx * 16 .. +16].
|
|
||||||
let off = (vkd_idx as usize) * 16;
|
|
||||||
if off + 16 > vkd_table.len() {
|
|
||||||
return Err(MediaKeyVariantError::VkdIndexOutOfRange);
|
|
||||||
}
|
|
||||||
let mut vkd = [0u8; 16];
|
|
||||||
vkd.copy_from_slice(&vkd_table[off..off + 16]);
|
|
||||||
|
|
||||||
// Step: Km = AES-128D(Kpnew, VKD) XOR uv.
|
|
||||||
let mut km = aes_ecb_decrypt(&kpnew, &vkd);
|
|
||||||
for i in 0..4 {
|
|
||||||
km[12 + i] ^= uv_bytes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gate: verify the derived Media Key against the MKB's Verify-Media-Key
|
|
||||||
// record. On the variant path the per-match magic check in
|
|
||||||
// `walk_processing_key` does NOT hold (it only saw the Precursor), so this
|
|
||||||
// is the authoritative Kp/Km verification — it MUST run before returning a
|
|
||||||
// real key.
|
|
||||||
let mk_dv = mkb_find_mk_dv(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
let mk_dv = mkb_find_mk_dv(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
let slots = variant_uv_slots(mkb_records).ok_or(MediaKeyVariantError::MkbIncomplete)?;
|
||||||
if aes_ecb_decrypt(&km, &mk_dv)[..8] != VERIFY_MAGIC {
|
let m = VariantMkb {
|
||||||
return Err(MediaKeyVariantError::MediaKeyVerifyFailed);
|
records: mkb_records,
|
||||||
|
nonce,
|
||||||
|
vkd_table,
|
||||||
|
c_block,
|
||||||
|
mk_dv,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try `pk` against each slot; return the first verified Km. If none verify,
|
||||||
|
// surface a correction-mode error over the generic miss so a disc that needs
|
||||||
|
// the soft/online path is distinguishable from a non-covering key.
|
||||||
|
let mut correction: Option<MediaKeyVariantError> = None;
|
||||||
|
for (uv, slot_index) in slots {
|
||||||
|
match variant_km_for_slot(&m, pk, uv, slot_index) {
|
||||||
|
Ok(km) => return Ok(km),
|
||||||
|
Err(e @ MediaKeyVariantError::SoftCorrectionRequired)
|
||||||
|
| Err(e @ MediaKeyVariantError::OnlineChallengeRequired) => {
|
||||||
|
correction.get_or_insert(e);
|
||||||
}
|
}
|
||||||
|
Err(_) => {}
|
||||||
// Step: Kvu = AES-G(Km, VID).
|
}
|
||||||
let kvu = aes_g(&km, vid);
|
}
|
||||||
|
Err(correction.unwrap_or(MediaKeyVariantError::ProcessingKeyUnavailable))
|
||||||
Ok((km, kvu))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -528,7 +515,8 @@ mod tests {
|
|||||||
// These three live in `super::keys` now (consolidated SD-walk helpers);
|
// These three live in `super::keys` now (consolidated SD-walk helpers);
|
||||||
// `use super::*` does not re-export the parent module's private `use`
|
// `use super::*` does not re-export the parent module's private `use`
|
||||||
// imports, so pull them in directly for the tests below.
|
// imports, so pull them in directly for the tests below.
|
||||||
use super::super::keys::{aesg3, calc_pk_from_dk};
|
use super::super::crypto::aesg3;
|
||||||
|
use super::super::derive::calc_pk_from_dk;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
|
||||||
@@ -560,12 +548,14 @@ mod tests {
|
|||||||
|
|
||||||
fn synthetic_mkb_with_variant() -> Vec<u8> {
|
fn synthetic_mkb_with_variant() -> Vec<u8> {
|
||||||
let mut mkb = synthetic_mkb_classical();
|
let mut mkb = synthetic_mkb_classical();
|
||||||
// 0x82 — 16-byte body (Variant data / VKD slot).
|
// 0x2d — Encrypted Media Key Variant Data: C (head 16) then the
|
||||||
mkb.extend_from_slice(&[0x82, 0x00, 0x00, 0x14]);
|
// trailing 16-byte Nonce, 32-byte body.
|
||||||
|
mkb.extend_from_slice(&[0x2d, 0x00, 0x00, 0x24]);
|
||||||
mkb.extend_from_slice(&[0xEE; 16]);
|
mkb.extend_from_slice(&[0xEE; 16]);
|
||||||
// 0x83 — 16-byte body (Variant Nonce).
|
|
||||||
mkb.extend_from_slice(&[0x83, 0x00, 0x00, 0x14]);
|
|
||||||
mkb.extend_from_slice(&[0x55; 16]);
|
mkb.extend_from_slice(&[0x55; 16]);
|
||||||
|
// 0x2f — Variant Key Data table: one 16-byte VKD entry.
|
||||||
|
mkb.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
||||||
|
mkb.extend_from_slice(&[0xCC; 16]);
|
||||||
mkb
|
mkb
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,9 +584,12 @@ mod tests {
|
|||||||
fn variant_detection_positive_on_variant() {
|
fn variant_detection_positive_on_variant() {
|
||||||
let recs = walk_mkb(&synthetic_mkb_with_variant());
|
let recs = walk_mkb(&synthetic_mkb_with_variant());
|
||||||
assert!(is_variant_mkb(&recs));
|
assert!(is_variant_mkb(&recs));
|
||||||
|
// Nonce = trailing 16 of 0x2d; VKD = the 0x2f entry; C = the whole 0x2d.
|
||||||
assert_eq!(variant_nonce(&recs), Some([0x55; 16]));
|
assert_eq!(variant_nonce(&recs), Some([0x55; 16]));
|
||||||
assert_eq!(variant_key_data(&recs), Some(&[0xEE; 16][..]));
|
assert_eq!(variant_key_data(&recs), Some(&[0xCC; 16][..]));
|
||||||
assert_eq!(variant_data_record(&recs), Some(&[0xEE; 16][..]));
|
let c = variant_data_record(&recs).unwrap();
|
||||||
|
assert_eq!(&c[..16], &[0xEE; 16][..]);
|
||||||
|
assert_eq!(&c[16..], &[0x55; 16][..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Chain entry-point classification ──
|
// ── Chain entry-point classification ──
|
||||||
@@ -604,49 +597,36 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn chain_rejects_non_variant_mkb() {
|
fn chain_rejects_non_variant_mkb() {
|
||||||
let recs = walk_mkb(&synthetic_mkb_classical());
|
let recs = walk_mkb(&synthetic_mkb_classical());
|
||||||
let err = derive_media_key_variant(&recs, &[], &[0xAA; 16], &[0u8; 16])
|
let err = derive_media_key_variant(&recs, &[0xAA; 16])
|
||||||
.expect_err("classical MKB must be rejected");
|
.expect_err("classical MKB must be rejected");
|
||||||
assert_eq!(err, MediaKeyVariantError::NotVariantMkb);
|
assert_eq!(err, MediaKeyVariantError::NotVariantMkb);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn chain_rejects_placeholder_kcd() {
|
|
||||||
// To reach the KCD check we need a complete variant MKB AND a
|
|
||||||
// DK that walks it. We construct both via the synthetic
|
|
||||||
// fixture below.
|
|
||||||
let (recs, dk, _kp, _expected_kmp) = synthetic_variant_setup(/*kmp15*/ 0x00);
|
|
||||||
let err =
|
|
||||||
derive_media_key_variant(&recs, &[dk], &KEY_CORRECTION_DATA_PLACEHOLDER, &[0u8; 16])
|
|
||||||
.expect_err("placeholder KCD must be rejected");
|
|
||||||
assert_eq!(err, MediaKeyVariantError::KcdNotProvided);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_detects_soft_correction_bit() {
|
fn chain_detects_soft_correction_bit() {
|
||||||
let (recs, dk, _, _) = synthetic_variant_setup(/*kmp15*/ 0x02);
|
// Kmp[15] bit 0x02 on the covering PK's slot surfaces the soft-correction
|
||||||
let err = derive_media_key_variant(&recs, &[dk], &[0xAA; 16], &[0u8; 16])
|
// classification over the generic no-slot miss.
|
||||||
|
let (recs, _dk, kp, _) = synthetic_variant_setup(/*kmp15*/ 0x02);
|
||||||
|
let err = derive_media_key_variant(&recs, &kp)
|
||||||
.expect_err("bit 0x02 must surface SoftCorrectionRequired");
|
.expect_err("bit 0x02 must surface SoftCorrectionRequired");
|
||||||
assert_eq!(err, MediaKeyVariantError::SoftCorrectionRequired);
|
assert_eq!(err, MediaKeyVariantError::SoftCorrectionRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_detects_online_challenge_bit() {
|
fn chain_detects_online_challenge_bit() {
|
||||||
let (recs, dk, _, _) = synthetic_variant_setup(/*kmp15*/ 0x04);
|
let (recs, _dk, kp, _) = synthetic_variant_setup(/*kmp15*/ 0x04);
|
||||||
let err = derive_media_key_variant(&recs, &[dk], &[0xAA; 16], &[0u8; 16])
|
let err = derive_media_key_variant(&recs, &kp)
|
||||||
.expect_err("bit 0x04 must surface OnlineChallengeRequired");
|
.expect_err("bit 0x04 must surface OnlineChallengeRequired");
|
||||||
assert_eq!(err, MediaKeyVariantError::OnlineChallengeRequired);
|
assert_eq!(err, MediaKeyVariantError::OnlineChallengeRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_surfaces_variants_table_gap_on_clean_kmp() {
|
fn variants_for_uv_reads_the_table_from_0x2d() {
|
||||||
// With both condition bits clear and a non-placeholder KCD, the
|
// variants_for_uv reads the VARIANTS u16 from the 0x2d record, so on a
|
||||||
// chain advances to the per-uv VARIANTS[uv] lookup, which is
|
// variant MKB that carries 0x2d it yields Some (never dead-stops the chain
|
||||||
// not yet wired. That returns VariantsTableUnavailable —
|
// at VariantsTableUnavailable).
|
||||||
// proving the bit checks and KCD check all passed.
|
let (recs, _dk, _kp, _) = synthetic_variant_setup(/*kmp15*/ 0x00);
|
||||||
let (recs, dk, _, _) = synthetic_variant_setup(/*kmp15*/ 0x00);
|
assert!(variants_for_uv(&recs, 0).is_some());
|
||||||
let err = derive_media_key_variant(&recs, &[dk], &[0xAA; 16], &[0u8; 16])
|
|
||||||
.expect_err("expected VariantsTableUnavailable at the per-uv lookup");
|
|
||||||
assert_eq!(err, MediaKeyVariantError::VariantsTableUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -659,7 +639,6 @@ mod tests {
|
|||||||
MediaKeyVariantError::ProcessingKeyUnavailable,
|
MediaKeyVariantError::ProcessingKeyUnavailable,
|
||||||
MediaKeyVariantError::SoftCorrectionRequired,
|
MediaKeyVariantError::SoftCorrectionRequired,
|
||||||
MediaKeyVariantError::OnlineChallengeRequired,
|
MediaKeyVariantError::OnlineChallengeRequired,
|
||||||
MediaKeyVariantError::KcdNotProvided,
|
|
||||||
MediaKeyVariantError::VariantsTableUnavailable,
|
MediaKeyVariantError::VariantsTableUnavailable,
|
||||||
MediaKeyVariantError::VkdIndexOutOfRange,
|
MediaKeyVariantError::VkdIndexOutOfRange,
|
||||||
MediaKeyVariantError::MediaKeyVerifyFailed,
|
MediaKeyVariantError::MediaKeyVerifyFailed,
|
||||||
@@ -696,13 +675,13 @@ mod tests {
|
|||||||
/// times — Kp = aesg3(dk, 1).
|
/// times — Kp = aesg3(dk, 1).
|
||||||
/// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a
|
/// - one cvalue in record 0x07 chosen so AES-D(Kp, C) ⊕ uv produces a
|
||||||
/// Kmp whose byte-15 is exactly `kmp15`.
|
/// Kmp whose byte-15 is exactly `kmp15`.
|
||||||
/// - record 0x82 with a 16-byte body (acts as both Variant Data
|
/// - record 0x2d (Encrypted Media Key Variant Data): a 32-byte body
|
||||||
/// and Variant Key Data; satisfies the parser heuristics).
|
/// carrying C in the head 16 bytes and a 16-byte Nonce in the tail.
|
||||||
/// - record 0x83 with a 16-byte Nonce.
|
/// - record 0x2f (Variant Key Data): one 16-byte entry.
|
||||||
///
|
///
|
||||||
/// Returns (records, dk, planted_kp, planted_kmp).
|
/// Returns (records, dk, planted_kp, planted_kmp).
|
||||||
fn synthetic_variant_setup(kmp15: u8) -> (Vec<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) {
|
fn synthetic_variant_setup(kmp15: u8) -> (Vec<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) {
|
||||||
use crate::aacs::decrypt::aes_ecb_encrypt;
|
use crate::aacs::crypto::aes_ecb_encrypt;
|
||||||
|
|
||||||
// Build header.
|
// Build header.
|
||||||
let mut mkb = vec![
|
let mut mkb = vec![
|
||||||
@@ -742,18 +721,16 @@ mod tests {
|
|||||||
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x14]);
|
||||||
mkb.extend_from_slice(&[0xCD; 16]);
|
mkb.extend_from_slice(&[0xCD; 16]);
|
||||||
|
|
||||||
// 0x82 record: holds C (Encrypted Media Key Variant Data) AND
|
// 0x2d record: Encrypted Media Key Variant Data — C (head 16) then a
|
||||||
// doubles as the VKD table (single 16-byte entry → VKDidx must
|
// trailing 16-byte Nonce (variant_nonce reads the tail), 32-byte body.
|
||||||
// resolve to 0 for `chain_surfaces_variants_table_gap` test —
|
mkb.extend_from_slice(&[0x2d, 0x00, 0x00, 0x24]);
|
||||||
// but the test never reaches the VKD lookup since the
|
|
||||||
// VARIANTS[uv] helper is not yet wired).
|
|
||||||
mkb.extend_from_slice(&[0x82, 0x00, 0x00, 0x14]);
|
|
||||||
mkb.extend_from_slice(&c_block);
|
mkb.extend_from_slice(&c_block);
|
||||||
|
|
||||||
// 0x83 record: 16-byte Nonce.
|
|
||||||
mkb.extend_from_slice(&[0x83, 0x00, 0x00, 0x14]);
|
|
||||||
mkb.extend_from_slice(&[0x77; 16]);
|
mkb.extend_from_slice(&[0x77; 16]);
|
||||||
|
|
||||||
|
// 0x2f record: Variant Key Data table — one 16-byte entry.
|
||||||
|
mkb.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
||||||
|
mkb.extend_from_slice(&[0xBB; 16]);
|
||||||
|
|
||||||
let recs = walk_mkb(&mkb);
|
let recs = walk_mkb(&mkb);
|
||||||
|
|
||||||
let dk = DeviceKey {
|
let dk = DeviceKey {
|
||||||
@@ -860,29 +837,29 @@ mod tests {
|
|||||||
// ── Record selectors ───────────────────────────────────────────────────
|
// ── Record selectors ───────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_variant_mkb_true_for_0x82_alone_and_0x83_alone() {
|
fn is_variant_mkb_true_for_0x2d_alone_and_0x2f_alone() {
|
||||||
// Either record type alone flags the MKB as variant.
|
// Either variant record type alone flags the MKB as variant.
|
||||||
let only82 = walk_mkb(&{
|
let only2d = walk_mkb(&{
|
||||||
let mut m = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 0];
|
let mut m = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 0];
|
||||||
m.extend_from_slice(&[0x82, 0x00, 0x00, 0x14]);
|
m.extend_from_slice(&[0x2d, 0x00, 0x00, 0x14]);
|
||||||
m.extend_from_slice(&[0xEE; 16]);
|
m.extend_from_slice(&[0xEE; 16]);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
assert!(is_variant_mkb(&only82));
|
assert!(is_variant_mkb(&only2d));
|
||||||
let only83 = walk_mkb(&{
|
let only2f = walk_mkb(&{
|
||||||
let mut m = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 0];
|
let mut m = vec![0x10, 0x00, 0x00, 0x08, 0, 0, 0, 0];
|
||||||
m.extend_from_slice(&[0x83, 0x00, 0x00, 0x14]);
|
m.extend_from_slice(&[0x2f, 0x00, 0x00, 0x14]);
|
||||||
m.extend_from_slice(&[0x55; 16]);
|
m.extend_from_slice(&[0x55; 16]);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
assert!(is_variant_mkb(&only83));
|
assert!(is_variant_mkb(&only2f));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn variant_nonce_requires_16_byte_body() {
|
fn variant_nonce_requires_16_byte_body() {
|
||||||
// A 0x83 record with < 16-byte body → None (no panic on the copy).
|
// A 0x2d record with < 16-byte body → None (no panic on the tail copy).
|
||||||
let recs = walk_mkb(&{
|
let recs = walk_mkb(&{
|
||||||
let mut m = vec![0x83, 0x00, 0x00, 0x0C]; // 8-byte body
|
let mut m = vec![0x2d, 0x00, 0x00, 0x0C]; // 8-byte body
|
||||||
m.extend_from_slice(&[0x11; 8]);
|
m.extend_from_slice(&[0x11; 8]);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
@@ -891,42 +868,48 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn variant_key_data_requires_nonempty_multiple_of_16() {
|
fn variant_key_data_requires_nonempty_multiple_of_16() {
|
||||||
// A 0x82 body that is NOT a multiple of 16 is rejected by
|
// A 0x2f VKD body that is NOT a multiple of 16 is rejected by
|
||||||
// variant_key_data (it needs whole 16-byte VKD slots).
|
// variant_key_data (it needs whole 16-byte VKD slots).
|
||||||
let recs = walk_mkb(&{
|
let recs = walk_mkb(&{
|
||||||
let mut m = vec![0x82, 0x00, 0x00, 0x0E]; // 10-byte body (not %16)
|
let mut m = vec![0x2f, 0x00, 0x00, 0x0E]; // 10-byte body (not %16)
|
||||||
m.extend_from_slice(&[0x22; 10]);
|
m.extend_from_slice(&[0x22; 10]);
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
assert_eq!(variant_key_data(&recs), None);
|
assert_eq!(variant_key_data(&recs), None);
|
||||||
// variant_data_record returns the body regardless of length.
|
// variant_data_record reads 0x2d and returns its body regardless of length.
|
||||||
assert_eq!(variant_data_record(&recs), Some(&[0x22u8; 10][..]));
|
let recs2 = walk_mkb(&{
|
||||||
|
let mut m = vec![0x2d, 0x00, 0x00, 0x0E];
|
||||||
|
m.extend_from_slice(&[0x33; 10]);
|
||||||
|
m
|
||||||
|
});
|
||||||
|
assert_eq!(variant_data_record(&recs2), Some(&[0x33u8; 10][..]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── derive_media_key_variant: missing-record classification ────────────
|
// ── derive_media_key_variant: missing-record classification ────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_reports_processing_key_unavailable_with_no_dks() {
|
fn chain_yields_no_key_for_non_covering_pk() {
|
||||||
// A complete variant MKB but an empty device-key pool → no uv covered
|
// A complete variant MKB but a Processing Key that covers no slot → no
|
||||||
// → ProcessingKeyUnavailable (the walk_processing_key None branch).
|
// Km verifies → an error (never a key). A non-covering key resolves to
|
||||||
|
// ProcessingKeyUnavailable, or to a correction-mode classification if its
|
||||||
|
// Kmp happens to set the soft/online bit — either way, no key is emitted.
|
||||||
let (recs, _dk, _, _) = synthetic_variant_setup(0x00);
|
let (recs, _dk, _, _) = synthetic_variant_setup(0x00);
|
||||||
let err = derive_media_key_variant(&recs, &[], &[0xAA; 16], &[0u8; 16])
|
let out = derive_media_key_variant(&recs, &[0x11; 16]);
|
||||||
.expect_err("no DK → ProcessingKeyUnavailable");
|
assert!(out.is_err(), "non-covering PK must not yield a Media Key");
|
||||||
assert_eq!(err, MediaKeyVariantError::ProcessingKeyUnavailable);
|
assert_ne!(out, Err(MediaKeyVariantError::NotVariantMkb));
|
||||||
|
assert_ne!(out, Err(MediaKeyVariantError::MkbIncomplete));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_reports_mkb_incomplete_when_nonce_missing() {
|
fn chain_reports_mkb_incomplete_when_nonce_missing() {
|
||||||
// Build a variant MKB (has 0x82 so is_variant true, and a DK can walk
|
// Build a variant MKB (still variant via 0x2f, and a DK can walk it)
|
||||||
// it) but WITHOUT a 0x83 nonce record → MkbIncomplete at the
|
// but WITHOUT the 0x2d record that carries C + the trailing Nonce →
|
||||||
// variant_nonce `?`.
|
// MkbIncomplete at the variant_nonce `?`.
|
||||||
// Start from the full setup, then rebuild the byte stream dropping
|
let (recs, _dk, kp, _) = synthetic_variant_setup(0x00);
|
||||||
// the 0x83 record.
|
// Reconstruct bytes without the 0x2d record.
|
||||||
let (recs, dk, _, _) = synthetic_variant_setup(0x00);
|
|
||||||
// Reconstruct bytes without the 0x83 record.
|
|
||||||
let mut mkb = Vec::new();
|
let mut mkb = Vec::new();
|
||||||
for r in &recs {
|
for r in &recs {
|
||||||
if r.rec_type == 0x83 {
|
if r.rec_type == 0x2d {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mkb.push(r.rec_type);
|
mkb.push(r.rec_type);
|
||||||
@@ -936,9 +919,8 @@ mod tests {
|
|||||||
mkb.extend_from_slice(&r.body);
|
mkb.extend_from_slice(&r.body);
|
||||||
}
|
}
|
||||||
let recs2 = walk_mkb(&mkb);
|
let recs2 = walk_mkb(&mkb);
|
||||||
assert!(is_variant_mkb(&recs2), "still variant via 0x82");
|
assert!(is_variant_mkb(&recs2), "still variant via 0x2f");
|
||||||
let err = derive_media_key_variant(&recs2, &[dk], &[0xAA; 16], &[0u8; 16])
|
let err = derive_media_key_variant(&recs2, &kp).expect_err("missing nonce → MkbIncomplete");
|
||||||
.expect_err("missing nonce → MkbIncomplete");
|
|
||||||
assert_eq!(err, MediaKeyVariantError::MkbIncomplete);
|
assert_eq!(err, MediaKeyVariantError::MkbIncomplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,7 +1008,7 @@ mod tests {
|
|||||||
fn error_codes_are_unique_and_in_7100_range() {
|
fn error_codes_are_unique_and_in_7100_range() {
|
||||||
// Each MediaKeyVariantError maps to a distinct E71xx code. A
|
// Each MediaKeyVariantError maps to a distinct E71xx code. A
|
||||||
// copy-paste collision (two variants sharing a code) would break
|
// copy-paste collision (two variants sharing a code) would break
|
||||||
// operator triage; assert all nine are distinct.
|
// operator triage; assert all are distinct.
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
let cases = [
|
let cases = [
|
||||||
MediaKeyVariantError::NotVariantMkb,
|
MediaKeyVariantError::NotVariantMkb,
|
||||||
@@ -1034,7 +1016,6 @@ mod tests {
|
|||||||
MediaKeyVariantError::ProcessingKeyUnavailable,
|
MediaKeyVariantError::ProcessingKeyUnavailable,
|
||||||
MediaKeyVariantError::SoftCorrectionRequired,
|
MediaKeyVariantError::SoftCorrectionRequired,
|
||||||
MediaKeyVariantError::OnlineChallengeRequired,
|
MediaKeyVariantError::OnlineChallengeRequired,
|
||||||
MediaKeyVariantError::KcdNotProvided,
|
|
||||||
MediaKeyVariantError::VariantsTableUnavailable,
|
MediaKeyVariantError::VariantsTableUnavailable,
|
||||||
MediaKeyVariantError::VkdIndexOutOfRange,
|
MediaKeyVariantError::VkdIndexOutOfRange,
|
||||||
MediaKeyVariantError::MediaKeyVerifyFailed,
|
MediaKeyVariantError::MediaKeyVerifyFailed,
|
||||||
+51
-47
@@ -244,7 +244,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Strip CPS-unit IDs — the decrypt primitives only want the raw key bytes.
|
// Strip CPS-unit IDs — the decrypt primitives only want the raw key bytes.
|
||||||
let raw_keys: Vec<[u8; 16]> = unit_keys.iter().map(|(_, k)| *k).collect();
|
let raw_keys: Vec<[u8; 16]> = unit_keys.iter().map(|(_, k)| *k).collect();
|
||||||
let rdk: Option<[u8; 16]> = *read_data_key;
|
let rdk: Option<[u8; 16]> = *read_data_key;
|
||||||
let unit_len = aacs::ALIGNED_UNIT_LEN;
|
let unit_len = aacs::content::ALIGNED_UNIT_LEN;
|
||||||
// AACS decrypts whole 6144-byte aligned units. The live mux path
|
// AACS decrypts whole 6144-byte aligned units. The live mux path
|
||||||
// (mux/disc.rs::fill_extents) issues 1- or 2-sector reads at every
|
// (mux/disc.rs::fill_extents) issues 1- or 2-sector reads at every
|
||||||
// extent tail, so a buffer is commonly NOT a multiple of the unit
|
// extent tail, so a buffer is commonly NOT a multiple of the unit
|
||||||
@@ -288,8 +288,8 @@ fn decrypt_sectors_impl(
|
|||||||
};
|
};
|
||||||
if partial_in_content {
|
if partial_in_content {
|
||||||
let partial = &buf[buf.len() - partial_len..];
|
let partial = &buf[buf.len() - partial_len..];
|
||||||
let packets = aacs::ts_packet_total(partial);
|
let packets = aacs::content::ts_packet_total(partial);
|
||||||
if packets > 0 && aacs::ts_sync_count(partial) <= packets / 2 {
|
if packets > 0 && aacs::content::ts_sync_count(partial) <= packets / 2 {
|
||||||
return Err(crate::error::Error::DecryptFailed);
|
return Err(crate::error::Error::DecryptFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +326,7 @@ fn decrypt_sectors_impl(
|
|||||||
// must happen first — it's a shared layer on top that is key-independent
|
// must happen first — it's a shared layer on top that is key-independent
|
||||||
// across all CPS units on the disc.
|
// across all CPS units on the disc.
|
||||||
let decrypt_one = |chunk: &mut [u8]| {
|
let decrypt_one = |chunk: &mut [u8]| {
|
||||||
if chunk.len() != unit_len || !aacs::aacs_unit_needs_decrypt(chunk) {
|
if chunk.len() != unit_len || !aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Save original bytes so we can restore if no key validates.
|
// Save original bytes so we can restore if no key validates.
|
||||||
@@ -335,7 +335,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Build a bus-decrypted copy to try unit keys against, or work
|
// Build a bus-decrypted copy to try unit keys against, or work
|
||||||
// in-place when there is no bus layer.
|
// in-place when there is no bus layer.
|
||||||
if let Some(ref rdk_key) = rdk {
|
if let Some(ref rdk_key) = rdk {
|
||||||
aacs::decrypt_bus(chunk, rdk_key);
|
aacs::content::decrypt_bus(chunk, rdk_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reorder the key iterator: try the cached hint first, then fall
|
// Reorder the key iterator: try the cached hint first, then fall
|
||||||
@@ -349,7 +349,7 @@ fn decrypt_sectors_impl(
|
|||||||
// Work on a per-key copy so a failing attempt doesn't
|
// Work on a per-key copy so a failing attempt doesn't
|
||||||
// clobber the bus-decrypted base we'll retry on.
|
// clobber the bus-decrypted base we'll retry on.
|
||||||
let mut attempt: Vec<u8> = chunk.to_vec();
|
let mut attempt: Vec<u8> = chunk.to_vec();
|
||||||
if aacs::decrypt_unit(&mut attempt, key) {
|
if aacs::content::decrypt_unit(&mut attempt, key) {
|
||||||
chunk.copy_from_slice(&attempt);
|
chunk.copy_from_slice(&attempt);
|
||||||
last_key_idx.store(idx, Ordering::Relaxed);
|
last_key_idx.store(idx, Ordering::Relaxed);
|
||||||
return;
|
return;
|
||||||
@@ -478,7 +478,7 @@ mod tests {
|
|||||||
/// not left scrambled.
|
/// not left scrambled.
|
||||||
#[test]
|
#[test]
|
||||||
fn nav_file_unit_survives_decrypt_attempt() {
|
fn nav_file_unit_survives_decrypt_attempt() {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
unit[0] = b'M';
|
unit[0] = b'M';
|
||||||
unit[1] = b'P';
|
unit[1] = b'P';
|
||||||
unit[2] = b'L';
|
unit[2] = b'L';
|
||||||
@@ -528,7 +528,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < len {
|
while u < len {
|
||||||
v[u] |= 0xC0;
|
v[u] |= 0xC0;
|
||||||
u += aacs::ALIGNED_UNIT_LEN;
|
u += aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
@@ -561,7 +561,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
|
|
||||||
// base_lba 0, content = [(100,10)] ⇒ the unit at LBA 0 is OUTSIDE content.
|
// base_lba 0, content = [(100,10)] ⇒ the unit at LBA 0 is OUTSIDE content.
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
@@ -581,7 +581,7 @@ mod tests {
|
|||||||
decrypt_sectors_in_content(&mut buf2, &mut keys, 0, 100, &[(100, 10)]).unwrap();
|
decrypt_sectors_in_content(&mut buf2, &mut keys, 0, 100, &[(100, 10)]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped2,
|
dropped2,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"an undecryptable CONTENT unit IS counted as loss"
|
"an undecryptable CONTENT unit IS counted as loss"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -594,12 +594,12 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = scrambled_region(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = scrambled_region(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
// unit0 @ LBA 0 (clear/skip), unit1 @ LBA 3 (content). Content = [(3,3)].
|
// unit0 @ LBA 0 (clear/skip), unit1 @ LBA 3 (content). Content = [(3,3)].
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(3, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(3, 3)]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"only the in-content unit (unit1) is checked; clear unit0 is skipped"
|
"only the in-content unit (unit1) is checked; clear unit0 is skipped"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut keys_u = keys_g.clone();
|
let mut keys_u = keys_g.clone();
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut g = original.clone();
|
let mut g = original.clone();
|
||||||
let mut u = original.clone();
|
let mut u = original.clone();
|
||||||
let gated = decrypt_sectors_in_content(&mut g, &mut keys_g, 0, 0, &[(0, 3)]).unwrap();
|
let gated = decrypt_sectors_in_content(&mut g, &mut keys_g, 0, 0, &[(0, 3)]).unwrap();
|
||||||
@@ -662,7 +662,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -680,7 +680,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let original = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let original = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, 0, "a clear in-content unit is not ciphertext");
|
assert_eq!(dropped, 0, "a clear in-content unit is not ciphertext");
|
||||||
@@ -691,7 +691,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn content_gate_none_keys_is_noop() {
|
fn content_gate_none_keys_is_noop() {
|
||||||
let mut keys = DecryptKeys::None;
|
let mut keys = DecryptKeys::None;
|
||||||
let original = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let original = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let mut buf = original.clone();
|
let mut buf = original.clone();
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, 0);
|
assert_eq!(dropped, 0);
|
||||||
@@ -721,7 +721,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let u = aacs::ALIGNED_UNIT_LEN;
|
let u = aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let mut buf = vec![0u8; 3 * u];
|
let mut buf = vec![0u8; 3 * u];
|
||||||
buf[..u].copy_from_slice(&scrambled_region(u)); // unit0 @ LBA0 scrambled
|
buf[..u].copy_from_slice(&scrambled_region(u)); // unit0 @ LBA0 scrambled
|
||||||
buf[u..2 * u].copy_from_slice(&clear_ts_region(u)); // unit1 @ LBA3 clear
|
buf[u..2 * u].copy_from_slice(&clear_ts_region(u)); // unit1 @ LBA3 clear
|
||||||
@@ -738,10 +738,14 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = scrambled_region(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = scrambled_region(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
// unit0 @ LBA0 content, unit1 @ LBA3 out. Content = [(0,3)].
|
// unit0 @ LBA0 content, unit1 @ LBA3 out. Content = [(0,3)].
|
||||||
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
let dropped = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(0, 3)]).unwrap();
|
||||||
assert_eq!(dropped, aacs::ALIGNED_UNIT_LEN, "only unit0 counts");
|
assert_eq!(
|
||||||
|
dropped,
|
||||||
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
|
"only unit0 counts"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The trailing-partial reject is ALSO content-gated: a scrambled partial
|
/// The trailing-partial reject is ALSO content-gated: a scrambled partial
|
||||||
@@ -755,7 +759,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
// One full clear unit + a scrambled single-sector partial, all OUTSIDE
|
// One full clear unit + a scrambled single-sector partial, all OUTSIDE
|
||||||
// content → the partial must be tolerated (Ok), not DecryptFailed.
|
// content → the partial must be tolerated (Ok), not DecryptFailed.
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
buf.extend_from_slice(&scrambled_region(2048));
|
buf.extend_from_slice(&scrambled_region(2048));
|
||||||
// content far away → both the full unit and the partial are non-content.
|
// content far away → both the full unit and the partial are non-content.
|
||||||
let res = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(1000, 3)]);
|
let res = decrypt_sectors_in_content(&mut buf, &mut keys, 0, 0, &[(1000, 3)]);
|
||||||
@@ -776,7 +780,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
// One full scrambled unit + a 2048-byte (single-sector) CLEAR tail.
|
// One full scrambled unit + a 2048-byte (single-sector) CLEAR tail.
|
||||||
let unit = scrambled_region(aacs::ALIGNED_UNIT_LEN);
|
let unit = scrambled_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let tail = clear_ts_region(2048);
|
let tail = clear_ts_region(2048);
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
@@ -784,7 +788,7 @@ mod tests {
|
|||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("clear trailing partial is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("clear trailing partial is Ok");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[aacs::ALIGNED_UNIT_LEN..],
|
&buf[aacs::content::ALIGNED_UNIT_LEN..],
|
||||||
&tail[..],
|
&tail[..],
|
||||||
"clear trailing partial unit must be left unchanged"
|
"clear trailing partial unit must be left unchanged"
|
||||||
);
|
);
|
||||||
@@ -801,7 +805,7 @@ mod tests {
|
|||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
// One full unit + a 4096-byte (two-sector) SCRAMBLED tail.
|
// One full unit + a 4096-byte (two-sector) SCRAMBLED tail.
|
||||||
let unit = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let unit = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let tail = scrambled_region(4096);
|
let tail = scrambled_region(4096);
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
@@ -835,7 +839,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN * 2);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN * 2);
|
||||||
let snapshot = buf.clone();
|
let snapshot = buf.clone();
|
||||||
|
|
||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("exact-multiple buffer is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("exact-multiple buffer is Ok");
|
||||||
@@ -1092,7 +1096,7 @@ mod tests {
|
|||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
||||||
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1112,7 +1116,7 @@ mod tests {
|
|||||||
unit_keys: vec![],
|
unit_keys: vec![],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::content::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
||||||
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
}
|
}
|
||||||
@@ -1120,8 +1124,8 @@ mod tests {
|
|||||||
// ── Multi-CPS-unit key selection ──────────────────────────────────────
|
// ── Multi-CPS-unit key selection ──────────────────────────────────────
|
||||||
|
|
||||||
/// Encrypt an aligned unit with the AACS algorithm run in reverse so that
|
/// Encrypt an aligned unit with the AACS algorithm run in reverse so that
|
||||||
/// `aacs::decrypt_unit` with the same key recovers the plaintext. Mirrors
|
/// `aacs::content::decrypt_unit` with the same key recovers the plaintext. Mirrors
|
||||||
/// the `aacs_encrypt_unit` helper in `aacs::decrypt::tests`.
|
/// the `aacs_encrypt_unit` helper in `aacs::content::tests`.
|
||||||
fn aacs_encrypt_unit_for_test(unit: &mut [u8], unit_key: &[u8; 16]) {
|
fn aacs_encrypt_unit_for_test(unit: &mut [u8], unit_key: &[u8; 16]) {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
@@ -1129,14 +1133,14 @@ mod tests {
|
|||||||
// the per-unit key so the recovered plaintext header matches.
|
// the per-unit key so the recovered plaintext header matches.
|
||||||
unit[0] |= 0xC0;
|
unit[0] |= 0xC0;
|
||||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
||||||
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
|
let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header);
|
||||||
let mut k = [0u8; 16];
|
let mut k = [0u8; 16];
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
k[i] = derived[i] ^ header[i];
|
k[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::decrypt::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let num_blocks = (aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let num_blocks = (aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..num_blocks {
|
for i in 0..num_blocks {
|
||||||
let off = 16 + i * 16;
|
let off = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1153,9 +1157,9 @@ mod tests {
|
|||||||
/// (offset 4 + k*192) so `ts_sync_destroyed` reports false and
|
/// (offset 4 + k*192) so `ts_sync_destroyed` reports false and
|
||||||
/// `decrypt_unit` verifies it as clear after decryption.
|
/// `decrypt_unit` verifies it as clear after decryption.
|
||||||
fn clear_ts_unit() -> Vec<u8> {
|
fn clear_ts_unit() -> Vec<u8> {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
@@ -1171,7 +1175,7 @@ mod tests {
|
|||||||
/// garbage for content under key ≥ 1. The fix tries every key and
|
/// garbage for content under key ≥ 1. The fix tries every key and
|
||||||
/// accepts the one whose output passes the TS-sync verify.
|
/// accepts the one whose output passes the TS-sync verify.
|
||||||
///
|
///
|
||||||
/// Grounding: `for idx in try_order { … if aacs::decrypt_unit(&mut attempt, key) { … } }`
|
/// Grounding: `for idx in try_order { … if aacs::content::decrypt_unit(&mut attempt, key) { … } }`
|
||||||
/// Mutation: revert to the pre-fix `decrypt_unit_full(chunk, &uk, …)` where
|
/// Mutation: revert to the pre-fix `decrypt_unit_full(chunk, &uk, …)` where
|
||||||
/// `uk = raw_keys[unit_key_idx]` (always key 0) → the unit comes out as
|
/// `uk = raw_keys[unit_key_idx]` (always key 0) → the unit comes out as
|
||||||
/// garbled bytes that still look scrambled, failing the `!ts_sync_destroyed`
|
/// garbled bytes that still look scrambled, failing the `!ts_sync_destroyed`
|
||||||
@@ -1185,7 +1189,7 @@ mod tests {
|
|||||||
let mut unit = clear_ts_unit();
|
let mut unit = clear_ts_unit();
|
||||||
aacs_encrypt_unit_for_test(&mut unit, &key1);
|
aacs_encrypt_unit_for_test(&mut unit, &key1);
|
||||||
assert!(
|
assert!(
|
||||||
aacs::ts_sync_destroyed(&unit),
|
aacs::content::ts_sync_destroyed(&unit),
|
||||||
"encrypted unit must look scrambled before decrypt"
|
"encrypted unit must look scrambled before decrypt"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1199,13 +1203,13 @@ mod tests {
|
|||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("multi-CPS decrypt must succeed");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("multi-CPS decrypt must succeed");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf),
|
!aacs::content::ts_sync_destroyed(&buf),
|
||||||
"unit encrypted under key1 must be fully decrypted (TS syncs restored)"
|
"unit encrypted under key1 must be fully decrypted (TS syncs restored)"
|
||||||
);
|
);
|
||||||
// Every sync position must carry 0x47.
|
// Every sync position must carry 0x47.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
aacs::ts_sync_count(&buf),
|
aacs::content::ts_sync_count(&buf),
|
||||||
aacs::ts_packet_total(&buf),
|
aacs::content::ts_packet_total(&buf),
|
||||||
"all TS sync bytes must be restored after decrypting under key1"
|
"all TS sync bytes must be restored after decrypting under key1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1230,12 +1234,12 @@ mod tests {
|
|||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
decrypt_sectors(&mut buf, &mut keys, 0).expect("single-key disc must decrypt");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("single-key disc must decrypt");
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf),
|
!aacs::content::ts_sync_destroyed(&buf),
|
||||||
"single-key disc: TS syncs must be restored"
|
"single-key disc: TS syncs must be restored"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
aacs::ts_sync_count(&buf),
|
aacs::content::ts_sync_count(&buf),
|
||||||
aacs::ts_packet_total(&buf),
|
aacs::content::ts_packet_total(&buf),
|
||||||
"all TS sync bytes must be restored for single-key disc"
|
"all TS sync bytes must be restored for single-key disc"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1267,7 @@ mod tests {
|
|||||||
aacs_encrypt_unit_for_test(&mut unit, &real_key);
|
aacs_encrypt_unit_for_test(&mut unit, &real_key);
|
||||||
let ciphertext = unit.clone();
|
let ciphertext = unit.clone();
|
||||||
assert!(
|
assert!(
|
||||||
aacs::ts_sync_destroyed(&unit),
|
aacs::content::ts_sync_destroyed(&unit),
|
||||||
"encrypted unit must look scrambled going in"
|
"encrypted unit must look scrambled going in"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1277,7 +1281,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"the whole scrambled unit must be reported as dropped when no key validates"
|
"the whole scrambled unit must be reported as dropped when no key validates"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1306,7 +1310,7 @@ mod tests {
|
|||||||
aacs_encrypt_unit_for_test(&mut unit_b, &wrong);
|
aacs_encrypt_unit_for_test(&mut unit_b, &wrong);
|
||||||
let unit_b_ciphertext = unit_b.clone();
|
let unit_b_ciphertext = unit_b.clone();
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(2 * aacs::ALIGNED_UNIT_LEN);
|
let mut buf = Vec::with_capacity(2 * aacs::content::ALIGNED_UNIT_LEN);
|
||||||
buf.extend_from_slice(&unit_a);
|
buf.extend_from_slice(&unit_a);
|
||||||
buf.extend_from_slice(&unit_b);
|
buf.extend_from_slice(&unit_b);
|
||||||
|
|
||||||
@@ -1318,15 +1322,15 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
aacs::ALIGNED_UNIT_LEN,
|
aacs::content::ALIGNED_UNIT_LEN,
|
||||||
"exactly one unit's worth of bytes must be reported dropped"
|
"exactly one unit's worth of bytes must be reported dropped"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&buf[..aacs::ALIGNED_UNIT_LEN]),
|
!aacs::content::ts_sync_destroyed(&buf[..aacs::content::ALIGNED_UNIT_LEN]),
|
||||||
"the decryptable unit must come out clear"
|
"the decryptable unit must come out clear"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[aacs::ALIGNED_UNIT_LEN..],
|
&buf[aacs::content::ALIGNED_UNIT_LEN..],
|
||||||
&unit_b_ciphertext[..],
|
&unit_b_ciphertext[..],
|
||||||
"the undecryptable unit must be restored to ciphertext"
|
"the undecryptable unit must be restored to ciphertext"
|
||||||
);
|
);
|
||||||
|
|||||||
+26
-23
@@ -86,9 +86,9 @@ impl AacsCertUnlocker<'_> {
|
|||||||
// MKB generation (best-effort) — forwarded to each source's
|
// MKB generation (best-effort) — forwarded to each source's
|
||||||
// `host_certs(mkb)` so a source MAY select a generation-appropriate cert
|
// `host_certs(mkb)` so a source MAY select a generation-appropriate cert
|
||||||
// (the default impl ignores it). A read failure leaves it `None`.
|
// (the default impl ignores it). A read failure leaves it `None`.
|
||||||
let mkb_gen = aacs::read_mkb_from_drive(session.scsi_mut())
|
let mkb_gen = aacs::inf::read_mkb_from_drive(session.scsi_mut())
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|m| aacs::mkb_version(&m));
|
.and_then(|m| aacs::mkb::mkb_version(&m));
|
||||||
|
|
||||||
// Host certs are keysource-served, never compiled in — unioned from the
|
// Host certs are keysource-served, never compiled in — unioned from the
|
||||||
// explicit `DriveCredentials` and the key-source layer. With ZERO certs
|
// explicit `DriveCredentials` and the key-source layer. With ZERO certs
|
||||||
@@ -152,10 +152,10 @@ fn unlock_error_to_error(e: &CertUnlockFailure) -> Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a [`CertUnlockFailure`] to a structured [`crate::aacs::UnlockOutcome`]
|
/// Map a [`CertUnlockFailure`] to a structured [`crate::aacs::trace::UnlockOutcome`]
|
||||||
/// for the resolution trace (English-free).
|
/// for the resolution trace (English-free).
|
||||||
fn cert_unlock_outcome(e: &CertUnlockFailure) -> crate::aacs::UnlockOutcome {
|
fn cert_unlock_outcome(e: &CertUnlockFailure) -> crate::aacs::trace::UnlockOutcome {
|
||||||
use crate::aacs::UnlockOutcome;
|
use crate::aacs::trace::UnlockOutcome;
|
||||||
use freemkv_unlock::UnlockError;
|
use freemkv_unlock::UnlockError;
|
||||||
match e {
|
match e {
|
||||||
CertUnlockFailure::NoHostCert { mkb } => UnlockOutcome::NoUsableHostCert { mkb: *mkb },
|
CertUnlockFailure::NoHostCert { mkb } => UnlockOutcome::NoUsableHostCert { mkb: *mkb },
|
||||||
@@ -230,7 +230,10 @@ impl Disc {
|
|||||||
/// `mkb` is the disc's MKB generation when known, forwarded to each source's
|
/// `mkb` is the disc's MKB generation when known, forwarded to each source's
|
||||||
/// [`crate::KeySource::host_certs`] so a source MAY return only
|
/// [`crate::KeySource::host_certs`] so a source MAY return only
|
||||||
/// generation-appropriate certs (the default ignores it).
|
/// generation-appropriate certs (the default ignores it).
|
||||||
fn collect_host_certs(opts: &ScanOptions, mkb: Option<u32>) -> Vec<crate::aacs::HostCert> {
|
fn collect_host_certs(
|
||||||
|
opts: &ScanOptions,
|
||||||
|
mkb: Option<u32>,
|
||||||
|
) -> Vec<crate::aacs::types::HostCert> {
|
||||||
// Delegates to the shared cert primitive (the external freemkv-unlock-aacs
|
// Delegates to the shared cert primitive (the external freemkv-unlock-aacs
|
||||||
// plugin uses the same one). Kept as a thin Disc method so the existing
|
// plugin uses the same one). Kept as a thin Disc method so the existing
|
||||||
// collect_host_certs_* unit tests and call sites are unchanged.
|
// collect_host_certs_* unit tests and call sites are unchanged.
|
||||||
@@ -312,14 +315,14 @@ impl Disc {
|
|||||||
.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO)
|
.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO)
|
||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO_DUPLICATE))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_UNIT_KEY_RO_DUPLICATE))
|
||||||
.map_err(|_| Error::AacsNoKeys)?;
|
.map_err(|_| Error::AacsNoKeys)?;
|
||||||
let dh = aacs::disc_hash(&uk_ro_data);
|
let dh = aacs::inf::disc_hash(&uk_ro_data);
|
||||||
|
|
||||||
let cc = udf_fs
|
let cc = udf_fs
|
||||||
.read_file(reader, crate::aacs::PATH_CONTENT_CERT)
|
.read_file(reader, crate::aacs::PATH_CONTENT_CERT)
|
||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
||||||
.ok()
|
.ok()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(aacs::parse_content_cert);
|
.and_then(aacs::inf::parse_content_cert);
|
||||||
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
|
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
|
||||||
// No-cert default = UHD (V20 stride), matching `read_aacs_version` so the
|
// No-cert default = UHD (V20 stride), matching `read_aacs_version` so the
|
||||||
// scanned `AacsState.version` and the out-of-band fetch agree. A wrong
|
// scanned `AacsState.version` and the out-of-band fetch agree. A wrong
|
||||||
@@ -328,7 +331,7 @@ impl Disc {
|
|||||||
let version = cc
|
let version = cc
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.version.major())
|
.map(|c| c.version.major())
|
||||||
.unwrap_or(aacs::AACS_MAJOR_UHD);
|
.unwrap_or(aacs::mkb::AACS_MAJOR_UHD);
|
||||||
|
|
||||||
// Bus-encryption gate (wrong-keys guard). A bus-encrypted disc (Content
|
// Bus-encryption gate (wrong-keys guard). A bus-encrypted disc (Content
|
||||||
// Certificate bus-encryption bit set) carries bus encryption on its
|
// Certificate bus-encryption bit set) carries bus encryption on its
|
||||||
@@ -394,12 +397,12 @@ impl Disc {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mkb_ver = aacs::mkb_version(&mkb_bytes);
|
let mkb_ver = aacs::mkb::mkb_version(&mkb_bytes);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "scan_aacs_vid_only",
|
phase = "scan_aacs_vid_only",
|
||||||
disc_hash = %aacs::disc_hash_hex(&dh),
|
disc_hash = %aacs::inf::disc_hash_hex(&dh),
|
||||||
version,
|
version,
|
||||||
bus_encryption,
|
bus_encryption,
|
||||||
has_vid = handshake.is_some(),
|
has_vid = handshake.is_some(),
|
||||||
@@ -410,7 +413,7 @@ impl Disc {
|
|||||||
version,
|
version,
|
||||||
bus_encryption,
|
bus_encryption,
|
||||||
mkb_version: mkb_ver,
|
mkb_version: mkb_ver,
|
||||||
disc_hash: aacs::disc_hash_hex(&dh),
|
disc_hash: aacs::inf::disc_hash_hex(&dh),
|
||||||
key_source: KeyOrigin::ExternalUk,
|
key_source: KeyOrigin::ExternalUk,
|
||||||
vuk: None,
|
vuk: None,
|
||||||
unit_keys: vec![],
|
unit_keys: vec![],
|
||||||
@@ -687,14 +690,14 @@ mod tests {
|
|||||||
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.version,
|
st.version,
|
||||||
aacs::AACS_MAJOR_UHD,
|
aacs::mkb::AACS_MAJOR_UHD,
|
||||||
"no cert → default UHD (major 2)"
|
"no cert → default UHD (major 2)"
|
||||||
);
|
);
|
||||||
assert!(!st.bus_encryption);
|
assert!(!st.bus_encryption);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// disc_hash is SHA1 of the Unit_Key_RO.inf bytes, hex with 0x prefix
|
/// disc_hash is SHA1 of the Unit_Key_RO.inf bytes, hex with 0x prefix
|
||||||
/// and uppercase (aacs::disc_hash + disc_hash_hex). The state's
|
/// and uppercase (aacs::inf::disc_hash + disc_hash_hex). The state's
|
||||||
/// disc_hash must match independently computing it over the same bytes.
|
/// disc_hash must match independently computing it over the same bytes.
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_vid_only_disc_hash_is_sha1_of_unit_key_ro() {
|
fn resolve_vid_only_disc_hash_is_sha1_of_unit_key_ro() {
|
||||||
@@ -710,7 +713,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
let st = Disc::resolve_vid_only(&udf, &mut disc, None).expect("state");
|
||||||
let expected = aacs::disc_hash_hex(&aacs::disc_hash(&uk));
|
let expected = aacs::inf::disc_hash_hex(&aacs::inf::disc_hash(&uk));
|
||||||
assert_eq!(st.disc_hash, expected);
|
assert_eq!(st.disc_hash, expected);
|
||||||
assert!(st.disc_hash.starts_with("0x"));
|
assert!(st.disc_hash.starts_with("0x"));
|
||||||
// uk_ro must be stashed verbatim for the external resolver.
|
// uk_ro must be stashed verbatim for the external resolver.
|
||||||
@@ -746,7 +749,7 @@ mod tests {
|
|||||||
// Real record stream is the single 16-byte type-0x10 record.
|
// Real record stream is the single 16-byte type-0x10 record.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.mkb.len(),
|
st.mkb.len(),
|
||||||
aacs::mkb_content_len(&mkb),
|
aacs::mkb::mkb_content_len(&mkb),
|
||||||
"MKB must be trimmed to record-stream length, not the zero-pad"
|
"MKB must be trimmed to record-stream length, not the zero-pad"
|
||||||
);
|
);
|
||||||
assert_eq!(st.mkb.len(), 16);
|
assert_eq!(st.mkb.len(), 16);
|
||||||
@@ -943,7 +946,7 @@ mod tests {
|
|||||||
// disc_hash must be computed over the DUPLICATE bytes.
|
// disc_hash must be computed over the DUPLICATE bytes.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
st.disc_hash,
|
st.disc_hash,
|
||||||
aacs::disc_hash_hex(&aacs::disc_hash(&uk)),
|
aacs::inf::disc_hash_hex(&aacs::inf::disc_hash(&uk)),
|
||||||
"fallback must hash the DUPLICATE Unit_Key_RO.inf"
|
"fallback must hash the DUPLICATE Unit_Key_RO.inf"
|
||||||
);
|
);
|
||||||
assert_eq!(st.uk_ro, uk);
|
assert_eq!(st.uk_ro, uk);
|
||||||
@@ -965,8 +968,8 @@ mod tests {
|
|||||||
// the route fails gracefully (AacsNoHostCert), never panics.
|
// the route fails gracefully (AacsNoHostCert), never panics.
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
fn fake_cert(tag: u8) -> aacs::HostCert {
|
fn fake_cert(tag: u8) -> aacs::types::HostCert {
|
||||||
aacs::HostCert {
|
aacs::types::HostCert {
|
||||||
private_key: [tag; 20],
|
private_key: [tag; 20],
|
||||||
certificate: vec![tag; 92],
|
certificate: vec![tag; 92],
|
||||||
private_key_v2: None,
|
private_key_v2: None,
|
||||||
@@ -975,15 +978,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal in-test KeySource that yields no keys but a fixed cert list.
|
/// A minimal in-test KeySource that yields no keys but a fixed cert list.
|
||||||
struct CertSource(Vec<aacs::HostCert>);
|
struct CertSource(Vec<aacs::types::HostCert>);
|
||||||
impl crate::KeySource for CertSource {
|
impl crate::KeySource for CertSource {
|
||||||
fn get_uk(
|
fn get_uk(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &dyn crate::keysource::ResolveCtx,
|
_ctx: &dyn crate::keysource::ResolveCtx,
|
||||||
) -> Result<Vec<crate::aacs::UnitKey>> {
|
) -> Result<Vec<crate::aacs::types::UnitKey>> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
fn host_certs(&self, _mkb: Option<u32>) -> Vec<aacs::HostCert> {
|
fn host_certs(&self, _mkb: Option<u32>) -> Vec<aacs::types::HostCert> {
|
||||||
self.0.clone()
|
self.0.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,7 +1080,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cert_unlock_outcome_maps_to_structured_trace_step() {
|
fn cert_unlock_outcome_maps_to_structured_trace_step() {
|
||||||
use crate::aacs::UnlockOutcome;
|
use crate::aacs::trace::UnlockOutcome;
|
||||||
use freemkv_unlock::UnlockError;
|
use freemkv_unlock::UnlockError;
|
||||||
// The libfreemkv-side no-cert case carries the MKB generation.
|
// The libfreemkv-side no-cert case carries the MKB generation.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+8
-8
@@ -1083,13 +1083,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a clear 6144-byte AACS unit (TS syncs at the 192-byte BD-TS
|
/// Build a clear 6144-byte AACS unit (TS syncs at the 192-byte BD-TS
|
||||||
/// stride) then encrypt it under `unit_key` so `aacs::decrypt_unit`
|
/// stride) then encrypt it under `unit_key` so `aacs::content::decrypt_unit`
|
||||||
/// recovers it cleanly (zero decrypt loss). Mirrors the encrypt helper in
|
/// recovers it cleanly (zero decrypt loss). Mirrors the encrypt helper in
|
||||||
/// `sector/decrypting.rs` tests. `tag` distinguishes two units' payloads.
|
/// `sector/decrypting.rs` tests. `tag` distinguishes two units' payloads.
|
||||||
fn encrypt_aacs_unit(unit_key: &[u8; 16], tag: u8) -> Vec<u8> {
|
fn encrypt_aacs_unit(unit_key: &[u8; 16], tag: u8) -> Vec<u8> {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47; // TS sync
|
unit[off] = 0x47; // TS sync
|
||||||
@@ -1101,14 +1101,14 @@ mod tests {
|
|||||||
// Flag encrypted via CPI bits (byte 0) before key derivation.
|
// Flag encrypted via CPI bits (byte 0) before key derivation.
|
||||||
unit[0] |= 0xC0;
|
unit[0] |= 0xC0;
|
||||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
||||||
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
|
let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header);
|
||||||
let mut k = [0u8; 16];
|
let mut k = [0u8; 16];
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
k[i] = derived[i] ^ header[i];
|
k[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::decrypt::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let blocks = (crate::aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..blocks {
|
for i in 0..blocks {
|
||||||
let o = 16 + i * 16;
|
let o = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1124,7 +1124,7 @@ mod tests {
|
|||||||
|
|
||||||
/// The plaintext that `encrypt_aacs_unit(_, tag)` decrypts back to.
|
/// The plaintext that `encrypt_aacs_unit(_, tag)` decrypts back to.
|
||||||
fn clear_aacs_unit(tag: u8) -> Vec<u8> {
|
fn clear_aacs_unit(tag: u8) -> Vec<u8> {
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
@@ -1600,10 +1600,10 @@ mod tests {
|
|||||||
/// own batch starts are always unit-aligned; anchoring a later extent
|
/// own batch starts are always unit-aligned; anchoring a later extent
|
||||||
/// against the FIRST extent's base mis-aligns whenever the extents' starts
|
/// against the FIRST extent's base mis-aligns whenever the extents' starts
|
||||||
/// differ by a non-multiple of 3 sectors. This is the exact arithmetic the
|
/// differ by a non-multiple of 3 sectors. This is the exact arithmetic the
|
||||||
/// decrypt-on-read gate (`aacs::is_unit_aligned`) performs.
|
/// decrypt-on-read gate (`aacs::content::is_unit_aligned`) performs.
|
||||||
#[test]
|
#[test]
|
||||||
fn per_extent_base_is_aligned_first_extent_base_is_not() {
|
fn per_extent_base_is_aligned_first_extent_base_is_not() {
|
||||||
use crate::aacs::is_unit_aligned;
|
use crate::aacs::content::is_unit_aligned;
|
||||||
let ext_a_start = 7000u32; // first extent abs LBA
|
let ext_a_start = 7000u32; // first extent abs LBA
|
||||||
let ext_b_start = 7004u32; // second extent abs LBA (Δ4 — not mult of 3)
|
let ext_b_start = 7004u32; // second extent abs LBA (Δ4 — not mult of 3)
|
||||||
|
|
||||||
|
|||||||
+35
-34
@@ -1413,7 +1413,7 @@ impl KeyOrigin {
|
|||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct DriveCredentials {
|
pub struct DriveCredentials {
|
||||||
/// Host certificate(s) + private key(s) for the SCSI AACS handshake.
|
/// Host certificate(s) + private key(s) for the SCSI AACS handshake.
|
||||||
pub host_certs: Vec<crate::aacs::HostCert>,
|
pub host_certs: Vec<crate::aacs::types::HostCert>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for disc scanning.
|
/// Options for disc scanning.
|
||||||
@@ -1781,8 +1781,8 @@ impl Disc {
|
|||||||
Ok((inf, mkb, version))
|
Ok((inf, mkb, version))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AACS major version ([`crate::aacs::AACS_MAJOR_BD`] /
|
/// AACS major version ([`crate::aacs::mkb::AACS_MAJOR_BD`] /
|
||||||
/// [`crate::aacs::AACS_MAJOR_UHD`]) from the content certificate. Drives the
|
/// [`crate::aacs::mkb::AACS_MAJOR_UHD`]) from the content certificate. Drives the
|
||||||
/// `Unit_Key_RO.inf` parse stride (48-byte V10 vs 64-byte V20/V21), so the
|
/// `Unit_Key_RO.inf` parse stride (48-byte V10 vs 64-byte V20/V21), so the
|
||||||
/// out-of-band key-fetch path parses `enc_title_keys` at the right stride (a
|
/// out-of-band key-fetch path parses `enc_title_keys` at the right stride (a
|
||||||
/// server VUK then derives the correct unit keys).
|
/// server VUK then derives the correct unit keys).
|
||||||
@@ -1798,7 +1798,7 @@ impl Disc {
|
|||||||
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
.or_else(|_| udf_fs.read_file(reader, crate::aacs::PATH_CONTENT_CERT_ALT))
|
||||||
.ok()
|
.ok()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(crate::aacs::parse_content_cert)
|
.and_then(crate::aacs::inf::parse_content_cert)
|
||||||
{
|
{
|
||||||
Some(c) => c.version.major(),
|
Some(c) => c.version.major(),
|
||||||
None => {
|
None => {
|
||||||
@@ -1808,7 +1808,7 @@ impl Disc {
|
|||||||
"no readable AACS content certificate; defaulting to the V20/UHD \
|
"no readable AACS content certificate; defaulting to the V20/UHD \
|
||||||
Unit_Key_RO stride (a VUK-from-server path would otherwise mis-stride)"
|
Unit_Key_RO stride (a VUK-from-server path would otherwise mis-stride)"
|
||||||
);
|
);
|
||||||
crate::aacs::AACS_MAJOR_UHD
|
crate::aacs::mkb::AACS_MAJOR_UHD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1817,7 +1817,7 @@ impl Disc {
|
|||||||
///
|
///
|
||||||
/// `MKB_RO.inf` / `MKB_RW.inf` are allocated to a fixed ~128 MiB and
|
/// `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
|
/// 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`]
|
/// prefix, find the record-stream length via [`crate::aacs::mkb::mkb_content_len`]
|
||||||
/// and return exactly that, growing the prefix if the records run past it.
|
/// 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
|
/// 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
|
/// `read_file` `MAX_FILE_BYTES` cap that (since 0.31.0) rejected the padded
|
||||||
@@ -1833,13 +1833,13 @@ impl Disc {
|
|||||||
.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, want)
|
.read_file_prefix(reader, crate::aacs::PATH_MKB_RO, want)
|
||||||
.or_else(|_| udf_fs.read_file_prefix(reader, crate::aacs::PATH_MKB_RW, want))
|
.or_else(|_| udf_fs.read_file_prefix(reader, crate::aacs::PATH_MKB_RW, want))
|
||||||
.map_err(|_| Error::AacsNoKeys)?;
|
.map_err(|_| Error::AacsNoKeys)?;
|
||||||
let n = crate::aacs::mkb_content_len(&buf);
|
let n = crate::aacs::mkb::mkb_content_len(&buf);
|
||||||
// `n` strictly inside `buf` => the record walk reached the padding
|
// `n` strictly inside `buf` => the record walk reached the padding
|
||||||
// boundary (full content captured). `buf` shorter than `want` =>
|
// boundary (full content captured). `buf` shorter than `want` =>
|
||||||
// the whole file is already read. Otherwise the records may run
|
// the whole file is already read. Otherwise the records may run
|
||||||
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
||||||
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
||||||
return Ok(crate::aacs::trim_mkb(buf));
|
return Ok(crate::aacs::mkb::trim_mkb(buf));
|
||||||
}
|
}
|
||||||
want = (want * 2).min(MAX_BYTES);
|
want = (want * 2).min(MAX_BYTES);
|
||||||
}
|
}
|
||||||
@@ -2201,7 +2201,7 @@ fn aligned_unit_keys_validate(
|
|||||||
read_data_key: Option<&[u8; 16]>,
|
read_data_key: Option<&[u8; 16]>,
|
||||||
samples: &[Vec<u8>],
|
samples: &[Vec<u8>],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
use crate::aacs::decrypt::{ALIGNED_UNIT_LEN, aacs_unit_needs_decrypt, decrypt_unit_full};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, aacs_unit_needs_decrypt, decrypt_unit_full};
|
||||||
let scrambled: Vec<&[u8]> = samples
|
let scrambled: Vec<&[u8]> = samples
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.as_slice())
|
.map(|s| s.as_slice())
|
||||||
@@ -2274,7 +2274,7 @@ impl Disc {
|
|||||||
/// else (UDF filesystem, BDMV nav, PLAYLIST/CLIPINF) is always clear.
|
/// else (UDF filesystem, BDMV nav, PLAYLIST/CLIPINF) is always clear.
|
||||||
///
|
///
|
||||||
/// The in-read decrypt-verify gate (`DecryptingSectorSource`) uses this so it
|
/// The in-read decrypt-verify gate (`DecryptingSectorSource`) uses this so it
|
||||||
/// never consults [`ts_sync_destroyed`](crate::aacs::ts_sync_destroyed) about
|
/// never consults [`ts_sync_destroyed`](crate::aacs::content::ts_sync_destroyed) about
|
||||||
/// non-content bytes — filesystem data has no TS sync and would otherwise be
|
/// non-content bytes — filesystem data has no TS sync and would otherwise be
|
||||||
/// mistaken for ciphertext (the first-2-GB false-positive this fixes).
|
/// mistaken for ciphertext (the first-2-GB false-positive this fixes).
|
||||||
///
|
///
|
||||||
@@ -2567,9 +2567,9 @@ impl Disc {
|
|||||||
} else if self.encrypted && self.css.is_none() {
|
} else if self.encrypted && self.css.is_none() {
|
||||||
self.aacs = Some(AacsState {
|
self.aacs = Some(AacsState {
|
||||||
version: if self.format == DiscFormat::Uhd {
|
version: if self.format == DiscFormat::Uhd {
|
||||||
crate::aacs::AACS_MAJOR_UHD
|
crate::aacs::mkb::AACS_MAJOR_UHD
|
||||||
} else {
|
} else {
|
||||||
crate::aacs::AACS_MAJOR_BD
|
crate::aacs::mkb::AACS_MAJOR_BD
|
||||||
},
|
},
|
||||||
bus_encryption: self.format == DiscFormat::Uhd,
|
bus_encryption: self.format == DiscFormat::Uhd,
|
||||||
mkb_version: None,
|
mkb_version: None,
|
||||||
@@ -2667,7 +2667,7 @@ impl Disc {
|
|||||||
Key::Processing(pks) => supplied.processing_keys = pks,
|
Key::Processing(pks) => supplied.processing_keys = pks,
|
||||||
Key::Media(mks) => supplied.media_keys = mks,
|
Key::Media(mks) => supplied.media_keys = mks,
|
||||||
Key::Volume(vuk) => {
|
Key::Volume(vuk) => {
|
||||||
supplied.disc_entry = Some(crate::aacs::DiscEntry {
|
supplied.disc_entry = Some(crate::aacs::types::DiscEntry {
|
||||||
disc_hash: aacs.disc_hash.clone(),
|
disc_hash: aacs.disc_hash.clone(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
media_key: None,
|
media_key: None,
|
||||||
@@ -2685,8 +2685,8 @@ impl Disc {
|
|||||||
let uk_ro = aacs.uk_ro.clone();
|
let uk_ro = aacs.uk_ro.clone();
|
||||||
let version_u8 = aacs.version;
|
let version_u8 = aacs.version;
|
||||||
|
|
||||||
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
let provider_refs: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied];
|
||||||
let ctx = crate::aacs::ResolveContext {
|
let ctx = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &volume_id,
|
volume_id: &volume_id,
|
||||||
@@ -2700,7 +2700,7 @@ impl Disc {
|
|||||||
// reason-preserving wrapper threads the no-key cause out so the
|
// reason-preserving wrapper threads the no-key cause out so the
|
||||||
// decrypt gate can report E7017 (had derivation material but no VID)
|
// decrypt gate can report E7017 (had derivation material but no VID)
|
||||||
// vs E7022 (no usable material) instead of a flat AacsKeyRejected.
|
// vs E7022 (no usable material) instead of a flat AacsKeyRejected.
|
||||||
let resolved = crate::aacs::resolve_keys_with_reason(&ctx, version_u8)
|
let resolved = crate::aacs::resolve::resolve_keys_with_reason(&ctx, version_u8)
|
||||||
.map_err(|_reason| crate::error::Error::AacsKeyRejected)?;
|
.map_err(|_reason| crate::error::Error::AacsKeyRejected)?;
|
||||||
|
|
||||||
if resolved.unit_keys.is_empty() {
|
if resolved.unit_keys.is_empty() {
|
||||||
@@ -3203,7 +3203,7 @@ impl Disc {
|
|||||||
// decrypts and is AACS-keyed. Region read-starts are aligned DOWN to a
|
// decrypts and is AACS-keyed. Region read-starts are aligned DOWN to a
|
||||||
// unit boundary in the loop below; a fresh sweep starts at LBA 0 (already
|
// unit boundary in the loop below; a fresh sweep starts at LBA 0 (already
|
||||||
// aligned), so alignment only bites on resume NonTried regions.
|
// aligned), so alignment only bites on resume NonTried regions.
|
||||||
const UNIT_SECTORS: u16 = (crate::aacs::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
const UNIT_SECTORS: u16 = (crate::aacs::content::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
||||||
if decrypt_is_aacs && batch % UNIT_SECTORS != 0 {
|
if decrypt_is_aacs && batch % UNIT_SECTORS != 0 {
|
||||||
batch = batch.saturating_add(UNIT_SECTORS - (batch % UNIT_SECTORS));
|
batch = batch.saturating_add(UNIT_SECTORS - (batch % UNIT_SECTORS));
|
||||||
}
|
}
|
||||||
@@ -3285,7 +3285,7 @@ impl Disc {
|
|||||||
// sweep's NonTried region starts at 0, already unit-aligned; this only
|
// sweep's NonTried region starts at 0, already unit-aligned; this only
|
||||||
// shifts resume regions that begin mid-unit.
|
// shifts resume regions that begin mid-unit.
|
||||||
let mut pos = if decrypt_is_aacs {
|
let mut pos = if decrypt_is_aacs {
|
||||||
let unit_bytes = crate::aacs::ALIGNED_UNIT_LEN as u64;
|
let unit_bytes = crate::aacs::content::ALIGNED_UNIT_LEN as u64;
|
||||||
region_pos - (region_pos % unit_bytes)
|
region_pos - (region_pos % unit_bytes)
|
||||||
} else {
|
} else {
|
||||||
region_pos
|
region_pos
|
||||||
@@ -4240,8 +4240,8 @@ mod tests {
|
|||||||
/// `sector::decrypting::tests::aacs_unaligned_start_lba_rejected`.
|
/// `sector::decrypting::tests::aacs_unaligned_start_lba_rejected`.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_sweep_batch_and_region_are_unit_aligned() {
|
fn aacs_sweep_batch_and_region_are_unit_aligned() {
|
||||||
const UNIT_SECTORS: u16 = (crate::aacs::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
const UNIT_SECTORS: u16 = (crate::aacs::content::ALIGNED_UNIT_LEN / 2048) as u16; // 3
|
||||||
let unit_bytes = crate::aacs::ALIGNED_UNIT_LEN as u64; // 6144
|
let unit_bytes = crate::aacs::content::ALIGNED_UNIT_LEN as u64; // 6144
|
||||||
|
|
||||||
// (a) Batch rounding: ecc_sectors() for UHD/BD is 32, not a multiple of 3.
|
// (a) Batch rounding: ecc_sectors() for UHD/BD is 32, not a multiple of 3.
|
||||||
// The decrypting-AACS path rounds it up to the next multiple of 3 (33).
|
// The decrypting-AACS path rounds it up to the next multiple of 3 (33).
|
||||||
@@ -4700,7 +4700,7 @@ mod tests {
|
|||||||
// The resolver classifies a device-keys-but-zero-VID context as
|
// The resolver classifies a device-keys-but-zero-VID context as
|
||||||
// `VidUnavailable`; that reason rides on `aacs_error`.
|
// `VidUnavailable`; that reason rides on `aacs_error`.
|
||||||
let supplied = crate::aacs::provider::SuppliedKey {
|
let supplied = crate::aacs::provider::SuppliedKey {
|
||||||
device_keys: vec![crate::aacs::DeviceKey {
|
device_keys: vec![crate::aacs::types::DeviceKey {
|
||||||
key: [0x11; 16],
|
key: [0x11; 16],
|
||||||
node: 1,
|
node: 1,
|
||||||
uv: 1,
|
uv: 1,
|
||||||
@@ -4710,14 +4710,14 @@ mod tests {
|
|||||||
media_keys: Vec::new(),
|
media_keys: Vec::new(),
|
||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
let provider_refs: [&dyn crate::aacs::KeyProvider; 1] = [&supplied];
|
let provider_refs: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied];
|
||||||
// A minimal but parseable Unit_Key_RO.inf (uk_pos=32, zero unit keys)
|
// A minimal but parseable Unit_Key_RO.inf (uk_pos=32, zero unit keys)
|
||||||
// so resolution proceeds to the path-try logic and fails for lack of a
|
// so resolution proceeds to the path-try logic and fails for lack of a
|
||||||
// VID — not because the .inf failed to parse.
|
// VID — not because the .inf failed to parse.
|
||||||
let mut uk_ro = vec![0u8; 40];
|
let mut uk_ro = vec![0u8; 40];
|
||||||
uk_ro[0..4].copy_from_slice(&32u32.to_be_bytes()); // uk_pos = 32
|
uk_ro[0..4].copy_from_slice(&32u32.to_be_bytes()); // uk_pos = 32
|
||||||
// num_unit_keys = 0 (BE16) at uk_pos -> parses to an empty key file.
|
// num_unit_keys = 0 (BE16) at uk_pos -> parses to an empty key file.
|
||||||
let ctx = crate::aacs::ResolveContext {
|
let ctx = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &[0u8; 16], // the "no VID" sentinel
|
volume_id: &[0u8; 16], // the "no VID" sentinel
|
||||||
@@ -4725,8 +4725,8 @@ mod tests {
|
|||||||
mkb: None,
|
mkb: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crate::aacs::resolve_keys_with_reason(&ctx, 2).err(),
|
crate::aacs::resolve::resolve_keys_with_reason(&ctx, 2).err(),
|
||||||
Some(crate::aacs::ResolveFailure::VidUnavailable),
|
Some(crate::aacs::resolve::ResolveFailure::VidUnavailable),
|
||||||
"device keys + zero VID must classify as VidUnavailable"
|
"device keys + zero VID must classify as VidUnavailable"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4750,8 +4750,8 @@ mod tests {
|
|||||||
media_keys: Vec::new(),
|
media_keys: Vec::new(),
|
||||||
disc_entry: None,
|
disc_entry: None,
|
||||||
};
|
};
|
||||||
let provider_refs_none: [&dyn crate::aacs::KeyProvider; 1] = [&supplied_none];
|
let provider_refs_none: [&dyn crate::aacs::provider::KeyProvider; 1] = [&supplied_none];
|
||||||
let ctx_none = crate::aacs::ResolveContext {
|
let ctx_none = crate::aacs::resolve::ResolveContext {
|
||||||
unit_key_ro: &uk_ro,
|
unit_key_ro: &uk_ro,
|
||||||
content_cert: None,
|
content_cert: None,
|
||||||
volume_id: &[0u8; 16],
|
volume_id: &[0u8; 16],
|
||||||
@@ -4759,8 +4759,8 @@ mod tests {
|
|||||||
mkb: None,
|
mkb: None,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
crate::aacs::resolve_keys_with_reason(&ctx_none, 2).err(),
|
crate::aacs::resolve::resolve_keys_with_reason(&ctx_none, 2).err(),
|
||||||
Some(crate::aacs::ResolveFailure::NoMaterial),
|
Some(crate::aacs::resolve::ResolveFailure::NoMaterial),
|
||||||
"no key material must classify as NoMaterial"
|
"no key material must classify as NoMaterial"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5030,8 +5030,8 @@ mod tests {
|
|||||||
let vuk = [0x5au8; 16];
|
let vuk = [0x5au8; 16];
|
||||||
let enc0 = [0x12u8; 16];
|
let enc0 = [0x12u8; 16];
|
||||||
let enc1 = [0x34u8; 16];
|
let enc1 = [0x34u8; 16];
|
||||||
let exp0 = crate::aacs::decrypt_unit_key(&vuk, &enc0);
|
let exp0 = crate::aacs::derive::decrypt_unit_key(&vuk, &enc0);
|
||||||
let exp1 = crate::aacs::decrypt_unit_key(&vuk, &enc1);
|
let exp1 = crate::aacs::derive::decrypt_unit_key(&vuk, &enc1);
|
||||||
|
|
||||||
let mut disc = make_test_disc(1000, "UHD");
|
let mut disc = make_test_disc(1000, "UHD");
|
||||||
disc.encrypted = true;
|
disc.encrypted = true;
|
||||||
@@ -5116,7 +5116,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unit_key_validation_gates_on_real_ciphertext() {
|
fn unit_key_validation_gates_on_real_ciphertext() {
|
||||||
use crate::aacs::decrypt::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
||||||
|
|
||||||
// No samples -> nothing to disprove against -> accept (sample-less paths
|
// No samples -> nothing to disprove against -> accept (sample-less paths
|
||||||
// like resume / mapfile must be unaffected).
|
// like resume / mapfile must be unaffected).
|
||||||
@@ -5174,7 +5174,7 @@ mod tests {
|
|||||||
// CPS-unit-1 sectors then passed through as raw encrypted bytes into the
|
// CPS-unit-1 sectors then passed through as raw encrypted bytes into the
|
||||||
// ISO/MKV with no error surfaced. The gate must now reject a key set
|
// ISO/MKV with no error surfaced. The gate must now reject a key set
|
||||||
// that leaves any scrambled sample uncovered.
|
// that leaves any scrambled sample uncovered.
|
||||||
use crate::aacs::decrypt::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ts_sync_destroyed};
|
||||||
|
|
||||||
let mut clear = vec![0u8; ALIGNED_UNIT_LEN];
|
let mut clear = vec![0u8; ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
@@ -5220,7 +5220,8 @@ mod tests {
|
|||||||
/// unit algorithm — ECB-derive the per-unit key, then AES-CBC encrypt the
|
/// unit algorithm — ECB-derive the per-unit key, then AES-CBC encrypt the
|
||||||
/// body with the fixed AACS IV.
|
/// body with the fixed AACS IV.
|
||||||
fn encrypt_unit_for_test(clear: &[u8], uk: &[u8; 16]) -> Vec<u8> {
|
fn encrypt_unit_for_test(clear: &[u8], uk: &[u8; 16]) -> Vec<u8> {
|
||||||
use crate::aacs::decrypt::{AACS_IV, ALIGNED_UNIT_LEN};
|
use crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
|
use crate::aacs::crypto::AACS_IV;
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
let mut unit = clear[..ALIGNED_UNIT_LEN].to_vec();
|
let mut unit = clear[..ALIGNED_UNIT_LEN].to_vec();
|
||||||
|
|||||||
+12
-11
@@ -7,7 +7,7 @@
|
|||||||
//! grid (clips can start off the 6144 grid and fragment across UDF extents). So
|
//! grid (clips can start off the 6144 grid and fragment across UDF extents). So
|
||||||
//! this gate BUFFERS the disc-absolute read stream and re-ALIGNS it into
|
//! this gate BUFFERS the disc-absolute read stream and re-ALIGNS it into
|
||||||
//! clip-file units, then applies the standards-correct
|
//! clip-file units, then applies the standards-correct
|
||||||
//! [`crate::aacs::unit_is_clean_ts`] gate (libaacs `_verify_ts`, all-32 syncs).
|
//! [`crate::aacs::content::unit_is_clean_ts`] gate (libaacs `_verify_ts`, all-32 syncs).
|
||||||
//!
|
//!
|
||||||
//! FAIL-SAFE CONTRACT (this sits in the middle of every read, so it must never
|
//! FAIL-SAFE CONTRACT (this sits in the middle of every read, so it must never
|
||||||
//! break a good read): the gate can ONLY downgrade a unit it is *confident* is
|
//! break a good read): the gate can ONLY downgrade a unit it is *confident* is
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use crate::aacs::{self, ALIGNED_UNIT_LEN};
|
use crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
|
use crate::aacs::{self};
|
||||||
use crate::consts::SECTOR_BYTES_U64;
|
use crate::consts::SECTOR_BYTES_U64;
|
||||||
use crate::decrypt::DecryptKeys;
|
use crate::decrypt::DecryptKeys;
|
||||||
use crate::sector::KeyFetch;
|
use crate::sector::KeyFetch;
|
||||||
@@ -57,7 +58,7 @@ pub enum ContainerKind {
|
|||||||
Ts,
|
Ts,
|
||||||
/// HD-DVD `.evo` — MPEG-2 program stream (pack-start `00 00 01 BA`).
|
/// HD-DVD `.evo` — MPEG-2 program stream (pack-start `00 00 01 BA`).
|
||||||
/// NOT yet enabled by enumeration; present so adding HD-DVD is a one-mapping
|
/// NOT yet enabled by enumeration; present so adding HD-DVD is a one-mapping
|
||||||
/// change. See [`crate::aacs::unit_is_clean_ps`] for the (unvalidated) check.
|
/// change. See [`crate::aacs::content::unit_is_clean_ps`] for the (unvalidated) check.
|
||||||
Ps,
|
Ps,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +188,8 @@ impl UnitVerifier {
|
|||||||
/// The post-decrypt structural check for a clip's container.
|
/// The post-decrypt structural check for a clip's container.
|
||||||
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
|
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
|
||||||
match self.containers[clip as usize] {
|
match self.containers[clip as usize] {
|
||||||
ContainerKind::Ts => aacs::unit_is_clean_ts,
|
ContainerKind::Ts => aacs::content::unit_is_clean_ts,
|
||||||
ContainerKind::Ps => aacs::unit_is_clean_ps,
|
ContainerKind::Ps => aacs::content::unit_is_clean_ps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +302,8 @@ impl UnitVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Can this fully-assembled unit be decrypted + verified? `accept` is the
|
/// Can this fully-assembled unit be decrypted + verified? `accept` is the
|
||||||
/// container's strict structural check ([`aacs::unit_is_clean_ts`] for TS,
|
/// container's strict structural check ([`aacs::content::unit_is_clean_ts`] for TS,
|
||||||
/// [`aacs::unit_is_clean_ps`] for PS) — the only format-specific part; the
|
/// [`aacs::content::unit_is_clean_ps`] for PS) — the only format-specific part; the
|
||||||
/// AACS crypto is container-agnostic. Returns the 3-state [`Decryptability`].
|
/// AACS crypto is container-agnostic. Returns the 3-state [`Decryptability`].
|
||||||
fn decryptability(
|
fn decryptability(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -318,7 +319,7 @@ impl UnitVerifier {
|
|||||||
// ENCRYPTED units that no key opens are ever flagged. (A real bad READ of
|
// ENCRYPTED units that no key opens are ever flagged. (A real bad READ of
|
||||||
// clear content is still caught by the normal SCSI read-error path; this
|
// clear content is still caught by the normal SCSI read-error path; this
|
||||||
// gate just won't false-flag it.)
|
// gate just won't false-flag it.)
|
||||||
if !aacs::aacs_unit_encrypted(raw) {
|
if !aacs::content::aacs_unit_encrypted(raw) {
|
||||||
return if accept(raw) {
|
return if accept(raw) {
|
||||||
Decryptability::Decryptable
|
Decryptability::Decryptable
|
||||||
} else {
|
} else {
|
||||||
@@ -363,7 +364,7 @@ impl UnitVerifier {
|
|||||||
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
|
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
|
||||||
for k in &self.keys {
|
for k in &self.keys {
|
||||||
let mut scratch = *raw;
|
let mut scratch = *raw;
|
||||||
if aacs::decrypt_unit_checked(&mut scratch, k, accept) {
|
if aacs::content::decrypt_unit_checked(&mut scratch, k, accept) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,13 +524,13 @@ mod tests {
|
|||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
unit[0] |= 0xC0; // CPI flag => reads as encrypted
|
unit[0] |= 0xC0; // CPI flag => reads as encrypted
|
||||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
||||||
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
|
let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header);
|
||||||
let mut k = [0u8; 16];
|
let mut k = [0u8; 16];
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
k[i] = derived[i] ^ header[i];
|
k[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::decrypt::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
for i in 0..(ALIGNED_UNIT_LEN - 16) / 16 {
|
for i in 0..(ALIGNED_UNIT_LEN - 16) / 16 {
|
||||||
let off = 16 + i * 16;
|
let off = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
|
|||||||
+27
-24
@@ -3,12 +3,13 @@
|
|||||||
//! libfreemkv performs NO key lookup. An application resolves a disc's keys
|
//! libfreemkv performs NO key lookup. An application resolves a disc's keys
|
||||||
//! through one or more [`KeySource`]s, each an adapter over a backing store (a
|
//! through one or more [`KeySource`]s, each an adapter over a backing store (a
|
||||||
//! keydb file, a key server, the mapfile cache). A source's job is to return the
|
//! keydb file, a key server, the mapfile cache). A source's job is to return the
|
||||||
//! disc's terminal **Unit Keys** ([`crate::aacs::UnitKey`]). It knows what
|
//! disc's terminal **Unit Keys** ([`crate::aacs::types::UnitKey`]). It knows what
|
||||||
//! material it holds (a DK / MK / VUK / pre-decrypted UK) and what it must fetch
|
//! material it holds (a DK / MK / VUK / pre-decrypted UK) and what it must fetch
|
||||||
//! from the disc (VID, MKB, encrypted title keys, content samples) to get there;
|
//! from the disc (VID, MKB, encrypted title keys, content samples) to get there;
|
||||||
//! it orchestrates the derivation by calling libfreemkv's own boil-down crypto
|
//! it orchestrates the derivation by calling libfreemkv's own derivation
|
||||||
//! primitives ([`crate::aacs::mk_from_dk`] / [`crate::aacs::vuk_from_mk`] /
|
//! primitives ([`crate::aacs::derive::derive_media_key_from_dk`] /
|
||||||
//! [`crate::aacs::uk_from_vuk`]) through the [`ResolveCtx`] handed to it.
|
//! [`crate::aacs::derive::derive_vuk`] / [`crate::aacs::derive::decrypt_unit_key`])
|
||||||
|
//! through the [`ResolveCtx`] handed to it.
|
||||||
//!
|
//!
|
||||||
//! libfreemkv still OWNS the crypto: the boil-down primitives and the AES live
|
//! libfreemkv still OWNS the crypto: the boil-down primitives and the AES live
|
||||||
//! here. A source owns only PATH ORCHESTRATION — deciding which primitive to
|
//! here. A source owns only PATH ORCHESTRATION — deciding which primitive to
|
||||||
@@ -17,7 +18,8 @@
|
|||||||
//! keeping key *policy* (which store, which order, online vs local) out of the
|
//! keeping key *policy* (which store, which order, online vs local) out of the
|
||||||
//! library.
|
//! library.
|
||||||
|
|
||||||
use crate::aacs::{HostCert, UnitKey, Vid};
|
use crate::aacs::types::HostCert;
|
||||||
|
use crate::aacs::types::{UnitKey, Vid};
|
||||||
use crate::disc::Key;
|
use crate::disc::Key;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
@@ -74,8 +76,8 @@ pub trait ResolveCtx {
|
|||||||
/// Raw MKB bytes (may be empty when not captured).
|
/// Raw MKB bytes (may be empty when not captured).
|
||||||
fn mkb(&self) -> Result<&[u8], Error>;
|
fn mkb(&self) -> Result<&[u8], Error>;
|
||||||
/// The disc's encrypted title keys, parsed from `Unit_Key_RO.inf` the same
|
/// The disc's encrypted title keys, parsed from `Unit_Key_RO.inf` the same
|
||||||
/// way the library's resolver parses them ([`crate::aacs::parse_unit_key_ro`]),
|
/// way the library's resolver parses them ([`crate::aacs::inf::parse_unit_key_ro`]),
|
||||||
/// in on-disc order. Feed straight into [`crate::aacs::uk_from_vuk`].
|
/// in on-disc order. Feed straight into [`crate::aacs::boil::uk_from_vuk`].
|
||||||
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error>;
|
fn enc_title_keys(&self) -> Result<&[[u8; 16]], Error>;
|
||||||
/// Up to `n` encrypted on-disc content sample units, for a source that
|
/// Up to `n` encrypted on-disc content sample units, for a source that
|
||||||
/// validates a candidate server-side against real ciphertext.
|
/// validates a candidate server-side against real ciphertext.
|
||||||
@@ -113,7 +115,8 @@ impl<'a> DiscInputsCtx<'a> {
|
|||||||
/// title keys — the parse failure is swallowed here, not surfaced as an
|
/// title keys — the parse failure is swallowed here, not surfaced as an
|
||||||
/// error.
|
/// error.
|
||||||
pub fn new(inputs: &'a DiscInputs) -> Self {
|
pub fn new(inputs: &'a DiscInputs) -> Self {
|
||||||
use crate::aacs::{AacsVersion, parse_unit_key_ro};
|
use crate::aacs::inf::parse_unit_key_ro;
|
||||||
|
use crate::aacs::mkb::AacsVersion;
|
||||||
let enc_keys = if inputs.unit_key_ro.is_empty() {
|
let enc_keys = if inputs.unit_key_ro.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -164,8 +167,8 @@ impl ResolveCtx for DiscInputsCtx<'_> {
|
|||||||
/// holds, orchestrates the derivation down to Unit Keys using the library's
|
/// holds, orchestrates the derivation down to Unit Keys using the library's
|
||||||
/// boil-down crypto primitives — never re-implementing AES. A source that holds
|
/// boil-down crypto primitives — never re-implementing AES. A source that holds
|
||||||
/// pre-decrypted Unit Keys returns them directly; one that holds a VUK calls
|
/// pre-decrypted Unit Keys returns them directly; one that holds a VUK calls
|
||||||
/// [`crate::aacs::uk_from_vuk`]; one that holds device keys calls
|
/// [`crate::aacs::boil::uk_from_vuk`]; one that holds device keys calls
|
||||||
/// [`crate::aacs::mk_from_dk`] → [`crate::aacs::vuk_from_mk`] → `uk_from_vuk`.
|
/// [`crate::aacs::boil::mk_from_dk`] → [`crate::aacs::boil::vuk_from_mk`] → `uk_from_vuk`.
|
||||||
///
|
///
|
||||||
/// Returning an empty `Vec` means "no key for this disc from this source"; an
|
/// Returning an empty `Vec` means "no key for this disc from this source"; an
|
||||||
/// `Err` means the source itself failed (I/O, parse, network). The caller
|
/// `Err` means the source itself failed (I/O, parse, network). The caller
|
||||||
@@ -209,7 +212,7 @@ pub fn resolve_and_apply(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`resolve_and_apply`] but also returns a structured
|
/// Like [`resolve_and_apply`] but also returns a structured
|
||||||
/// [`crate::aacs::ResolutionTrace`] recording, per source, what happened — for
|
/// [`crate::aacs::trace::ResolutionTrace`] recording, per source, what happened — for
|
||||||
/// applications to render. ZERO English; the trace is typed enums only.
|
/// applications to render. ZERO English; the trace is typed enums only.
|
||||||
///
|
///
|
||||||
/// One-shot per source: each source's [`KeySource::get_uk`] is called exactly
|
/// One-shot per source: each source's [`KeySource::get_uk`] is called exactly
|
||||||
@@ -219,8 +222,8 @@ pub fn resolve_and_apply(
|
|||||||
/// success — so a wrong/partial key set is rejected and the loop continues.
|
/// success — so a wrong/partial key set is rejected and the loop continues.
|
||||||
///
|
///
|
||||||
/// CPS-unit numbering: a source returns Unit Keys carrying the POSITIONAL index
|
/// CPS-unit numbering: a source returns Unit Keys carrying the POSITIONAL index
|
||||||
/// from [`crate::aacs::uk_from_vuk`]; the library's canonical CPS-unit number is
|
/// from [`crate::aacs::boil::uk_from_vuk`]; the library's canonical CPS-unit number is
|
||||||
/// `position + 1` (matching [`crate::aacs::parse_unit_key_ro`]'s `(i + 1)`), so
|
/// `position + 1` (matching [`crate::aacs::inf::parse_unit_key_ro`]'s `(i + 1)`), so
|
||||||
/// the committed `AacsState.unit_keys` is byte-identical to the library-resolved
|
/// the committed `AacsState.unit_keys` is byte-identical to the library-resolved
|
||||||
/// path. The number is cosmetic for descramble (the decrypt path strips it and
|
/// path. The number is cosmetic for descramble (the decrypt path strips it and
|
||||||
/// tries every key) but is kept faithful to the resolver's convention.
|
/// tries every key) but is kept faithful to the resolver's convention.
|
||||||
@@ -228,10 +231,10 @@ pub fn resolve_and_apply_traced(
|
|||||||
sources: &[Box<dyn KeySource>],
|
sources: &[Box<dyn KeySource>],
|
||||||
inputs: &DiscInputs,
|
inputs: &DiscInputs,
|
||||||
disc: &mut crate::Disc,
|
disc: &mut crate::Disc,
|
||||||
) -> (bool, crate::aacs::ResolutionTrace) {
|
) -> (bool, crate::aacs::trace::ResolutionTrace) {
|
||||||
use crate::aacs::trace::{KeyNode, KeyOutcome, KeyStep};
|
use crate::aacs::trace::{KeyNode, KeyOutcome, KeyStep};
|
||||||
|
|
||||||
let mut trace = crate::aacs::ResolutionTrace::new();
|
let mut trace = crate::aacs::trace::ResolutionTrace::new();
|
||||||
|
|
||||||
// The ctx parses Unit_Key_RO.inf at the stride for `inputs.version` (the
|
// The ctx parses Unit_Key_RO.inf at the stride for `inputs.version` (the
|
||||||
// disc's own AACS major), so the stride is the disc's single source of truth.
|
// disc's own AACS major), so the stride is the disc's single source of truth.
|
||||||
@@ -341,7 +344,7 @@ pub fn key_fetch(
|
|||||||
/// `start_lba`), which the library owns. A key source is *handed* these bytes
|
/// `start_lba`), which the library owns. A key source is *handed* these bytes
|
||||||
/// via `DiscInputs.samples`; it never reads the disc itself.
|
/// via `DiscInputs.samples`; it never reads the disc itself.
|
||||||
///
|
///
|
||||||
/// "Encrypted" is decided by [`crate::aacs::ts_sync_destroyed`] — the SAME
|
/// "Encrypted" is decided by [`crate::aacs::content::ts_sync_destroyed`] — the SAME
|
||||||
/// predicate the decrypt gate uses — so all sides agree. A clip opens with clear
|
/// predicate the decrypt gate uses — so all sides agree. A clip opens with clear
|
||||||
/// navigation units (PAT/PMT, menus); only the feature body is scrambled, and a
|
/// navigation units (PAT/PMT, menus); only the feature body is scrambled, and a
|
||||||
/// clear unit proves nothing, so this collects only scrambled ones — probing
|
/// clear unit proves nothing, so this collects only scrambled ones — probing
|
||||||
@@ -352,7 +355,7 @@ pub fn read_encrypted_units(
|
|||||||
title: &crate::disc::DiscTitle,
|
title: &crate::disc::DiscTitle,
|
||||||
n: usize,
|
n: usize,
|
||||||
) -> Vec<Vec<u8>> {
|
) -> Vec<Vec<u8>> {
|
||||||
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
||||||
const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap
|
const CHUNK_UNITS: u32 = 15; // 45 sectors/read — under the drive transfer cap
|
||||||
// Probe several evenly-spaced points across EACH extent rather than only the
|
// Probe several evenly-spaced points across EACH extent rather than only the
|
||||||
// midpoint-and-forward: a title whose encrypted feature starts late, or whose
|
// midpoint-and-forward: a title whose encrypted feature starts late, or whose
|
||||||
@@ -413,7 +416,7 @@ pub fn read_encrypted_units(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::aacs::UnitKey;
|
use crate::aacs::types::UnitKey;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
// ── KeySource default-method behaviour ────────────────────────────────────
|
// ── KeySource default-method behaviour ────────────────────────────────────
|
||||||
@@ -454,7 +457,7 @@ mod tests {
|
|||||||
let inputs = DiscInputs {
|
let inputs = DiscInputs {
|
||||||
disc_hash: "0xABC".into(),
|
disc_hash: "0xABC".into(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_BD,
|
version: crate::aacs::mkb::AACS_MAJOR_BD,
|
||||||
mkb: vec![1, 2, 3],
|
mkb: vec![1, 2, 3],
|
||||||
unit_key_ro: uk_ro,
|
unit_key_ro: uk_ro,
|
||||||
samples: vec![vec![9u8; 4], vec![8u8; 4], vec![7u8; 4]],
|
samples: vec![vec![9u8; 4], vec![8u8; 4], vec![7u8; 4]],
|
||||||
@@ -510,7 +513,7 @@ mod tests {
|
|||||||
let inputs = DiscInputs {
|
let inputs = DiscInputs {
|
||||||
disc_hash: "0x00".into(),
|
disc_hash: "0x00".into(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_UHD,
|
version: crate::aacs::mkb::AACS_MAJOR_UHD,
|
||||||
mkb: Vec::new(),
|
mkb: Vec::new(),
|
||||||
unit_key_ro: Vec::new(),
|
unit_key_ro: Vec::new(),
|
||||||
samples: Vec::new(),
|
samples: Vec::new(),
|
||||||
@@ -531,7 +534,7 @@ mod tests {
|
|||||||
DiscInputs {
|
DiscInputs {
|
||||||
disc_hash: String::new(),
|
disc_hash: String::new(),
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
version: crate::aacs::AACS_MAJOR_UHD,
|
version: crate::aacs::mkb::AACS_MAJOR_UHD,
|
||||||
mkb: Vec::new(),
|
mkb: Vec::new(),
|
||||||
unit_key_ro: Vec::new(),
|
unit_key_ro: Vec::new(),
|
||||||
samples: Vec::new(),
|
samples: Vec::new(),
|
||||||
@@ -621,7 +624,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cb = key_fetch(empty_inputs(), make);
|
let cb = key_fetch(empty_inputs(), make);
|
||||||
let samples = vec![vec![0xEEu8; crate::aacs::ALIGNED_UNIT_LEN]];
|
let samples = vec![vec![0xEEu8; crate::aacs::content::ALIGNED_UNIT_LEN]];
|
||||||
let got = cb(&samples);
|
let got = cb(&samples);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got,
|
got,
|
||||||
@@ -643,7 +646,7 @@ mod tests {
|
|||||||
/// finds the early scrambled band.
|
/// finds the early scrambled band.
|
||||||
#[test]
|
#[test]
|
||||||
fn read_encrypted_units_finds_scrambled_content_off_the_midpoint() {
|
fn read_encrypted_units_finds_scrambled_content_off_the_midpoint() {
|
||||||
use crate::aacs::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
use crate::aacs::content::{ALIGNED_UNIT_LEN, ALIGNED_UNIT_SECTORS, ts_sync_destroyed};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
|
|
||||||
@@ -725,7 +728,7 @@ mod tests {
|
|||||||
/// prior single-key fixtures passed regardless of stride.
|
/// prior single-key fixtures passed regardless of stride.
|
||||||
#[test]
|
#[test]
|
||||||
fn disc_inputs_ctx_parses_unit_keys_at_the_version_stride() {
|
fn disc_inputs_ctx_parses_unit_keys_at_the_version_stride() {
|
||||||
use crate::aacs::{AACS_MAJOR_BD, AACS_MAJOR_UHD};
|
use crate::aacs::mkb::{AACS_MAJOR_BD, AACS_MAJOR_UHD};
|
||||||
const UK_POS: usize = 64;
|
const UK_POS: usize = 64;
|
||||||
let mut inf = vec![0u8; 200];
|
let mut inf = vec![0u8; 200];
|
||||||
inf[0..4].copy_from_slice(&(UK_POS as u32).to_be_bytes()); // uk_pos
|
inf[0..4].copy_from_slice(&(UK_POS as u32).to_be_bytes()); // uk_pos
|
||||||
|
|||||||
+1
-1
@@ -366,7 +366,7 @@ impl TsDemuxer {
|
|||||||
|
|
||||||
// P3/B1 CONCEALMENT MARKER. The decrypt layer fills an undecryptable
|
// P3/B1 CONCEALMENT MARKER. The decrypt layer fills an undecryptable
|
||||||
// aligned unit with NULL-TS packets (PID 0x1FFF) that carry an
|
// aligned unit with NULL-TS packets (PID 0x1FFF) that carry an
|
||||||
// adaptation-field discontinuity_indicator (see `aacs::fill_null_ts_unit`).
|
// adaptation-field discontinuity_indicator (see `aacs::content::fill_null_ts_unit`).
|
||||||
// This is the authoritative loss signal — unlike a tracked PID's 4-bit
|
// This is the authoritative loss signal — unlike a tracked PID's 4-bit
|
||||||
// continuity_counter it is CC-INDEPENDENT, so it survives a loss that is
|
// continuity_counter it is CC-INDEPENDENT, so it survives a loss that is
|
||||||
// an exact multiple of 16 packets and a loss at the very start of a PID
|
// an exact multiple of 16 packets and a loss at the very start of a PID
|
||||||
|
|||||||
+46
-41
@@ -146,7 +146,7 @@ pub struct DecryptingSectorSource<S: SectorSource> {
|
|||||||
scratch: Vec<u8>,
|
scratch: Vec<u8>,
|
||||||
/// MUX loss-concealment switch (P3 / Edit-2). When `true`, a content unit
|
/// MUX loss-concealment switch (P3 / Edit-2). When `true`, a content unit
|
||||||
/// that genuinely won't decrypt is NOT a read failure: it is overwritten with
|
/// that genuinely won't decrypt is NOT a read failure: it is overwritten with
|
||||||
/// valid NULL TS packets ([`crate::aacs::fill_null_ts_unit`]), tallied into
|
/// valid NULL TS packets ([`crate::aacs::content::fill_null_ts_unit`]), tallied into
|
||||||
/// [`decrypt_dropped`](Self::decrypt_dropped), logged loud with its LBA, and
|
/// [`decrypt_dropped`](Self::decrypt_dropped), logged loud with its LBA, and
|
||||||
/// the read returns `Ok` so the mux KEEPS GOING (it can never abort over an
|
/// the read returns `Ok` so the mux KEEPS GOING (it can never abort over an
|
||||||
/// undecryptable unit). This is the spec's "decrypt-verify is a RIP gate, not
|
/// undecryptable unit). This is the spec's "decrypt-verify is a RIP gate, not
|
||||||
@@ -197,7 +197,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
/// (sorted/merged `(start_lba, sector_count)` — see
|
/// (sorted/merged `(start_lba, sector_count)` — see
|
||||||
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).
|
||||||
/// Units outside content (UDF filesystem / BDMV nav) pass through untouched,
|
/// Units outside content (UDF filesystem / BDMV nav) pass through untouched,
|
||||||
/// so [`ts_sync_destroyed`](crate::aacs::ts_sync_destroyed) is never consulted
|
/// so [`ts_sync_destroyed`](crate::aacs::content::ts_sync_destroyed) is never consulted
|
||||||
/// about non-content bytes. Whole-disc readers (sweep / patch) set this; the
|
/// about non-content bytes. Whole-disc readers (sweep / patch) set this; the
|
||||||
/// mux leaves it unset because it only ever reads title extents.
|
/// mux leaves it unset because it only ever reads title extents.
|
||||||
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
|
pub fn with_content_ranges(mut self, ranges: Arc<[(u32, u32)]>) -> Self {
|
||||||
@@ -301,14 +301,14 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
content: Option<&[(u32, u32)]>,
|
content: Option<&[(u32, u32)]>,
|
||||||
prev_dropped: usize,
|
prev_dropped: usize,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
// Gather up to MAX_FETCH_SAMPLES still-scrambled aligned units — the
|
// Gather up to MAX_FETCH_SAMPLES still-scrambled aligned units — the
|
||||||
// exact on-disc ciphertext no held key could open. A trailing partial
|
// exact on-disc ciphertext no held key could open. A trailing partial
|
||||||
// unit (chunks_exact remainder) can't be a whole scrambled unit, so
|
// unit (chunks_exact remainder) can't be a whole scrambled unit, so
|
||||||
// skipping it is correct.
|
// skipping it is correct.
|
||||||
let mut samples: Vec<Vec<u8>> = Vec::new();
|
let mut samples: Vec<Vec<u8>> = Vec::new();
|
||||||
for chunk in buf.chunks_exact(unit_len) {
|
for chunk in buf.chunks_exact(unit_len) {
|
||||||
if crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if crate::aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
samples.push(chunk.to_vec());
|
samples.push(chunk.to_vec());
|
||||||
if samples.len() >= MAX_FETCH_SAMPLES {
|
if samples.len() >= MAX_FETCH_SAMPLES {
|
||||||
break;
|
break;
|
||||||
@@ -391,7 +391,7 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
content: Option<&[(u32, u32)]>,
|
content: Option<&[(u32, u32)]>,
|
||||||
keys: &DecryptKeys,
|
keys: &DecryptKeys,
|
||||||
) {
|
) {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let unit_sectors = (unit_len / 2048) as u32;
|
let unit_sectors = (unit_len / 2048) as u32;
|
||||||
// Only AACS produces decrypt-verify failures; None / CSS never reach here
|
// Only AACS produces decrypt-verify failures; None / CSS never reach here
|
||||||
// with a non-zero dropped count.
|
// with a non-zero dropped count.
|
||||||
@@ -415,12 +415,12 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
// A unit that decrypted is no longer sync-destroyed; a CPI-clear or
|
// A unit that decrypted is no longer sync-destroyed; a CPI-clear or
|
||||||
// non-content unit is gated out. Only undecryptable in-content units
|
// non-content unit is gated out. Only undecryptable in-content units
|
||||||
// that are flagged encrypted carry signal.
|
// that are flagged encrypted carry signal.
|
||||||
if !in_content || !crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if !in_content || !crate::aacs::content::aacs_unit_needs_decrypt(chunk) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let all_zero = chunk.iter().all(|&b| b == 0);
|
let all_zero = chunk.iter().all(|&b| b == 0);
|
||||||
let ts_sync = crate::aacs::ts_sync_count(chunk);
|
let ts_sync = crate::aacs::content::ts_sync_count(chunk);
|
||||||
let ts_total = crate::aacs::ts_packet_total(chunk);
|
let ts_total = crate::aacs::content::ts_packet_total(chunk);
|
||||||
let mut seen = [false; 256];
|
let mut seen = [false; 256];
|
||||||
for &b in chunk {
|
for &b in chunk {
|
||||||
seen[b as usize] = true;
|
seen[b as usize] = true;
|
||||||
@@ -431,10 +431,10 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
|
|||||||
for (_, k) in unit_keys.iter() {
|
for (_, k) in unit_keys.iter() {
|
||||||
let mut attempt = chunk.to_vec();
|
let mut attempt = chunk.to_vec();
|
||||||
if let Some(ref rdk_key) = rdk {
|
if let Some(ref rdk_key) = rdk {
|
||||||
crate::aacs::decrypt_bus(&mut attempt, rdk_key);
|
crate::aacs::content::decrypt_bus(&mut attempt, rdk_key);
|
||||||
}
|
}
|
||||||
crate::aacs::decrypt_unit(&mut attempt, k);
|
crate::aacs::content::decrypt_unit(&mut attempt, k);
|
||||||
let s = crate::aacs::ts_sync_count(&attempt);
|
let s = crate::aacs::content::ts_sync_count(&attempt);
|
||||||
if s > best_sync {
|
if s > best_sync {
|
||||||
best_sync = s;
|
best_sync = s;
|
||||||
}
|
}
|
||||||
@@ -497,7 +497,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// units (else its readable units are wrongly rejected → "Decryption
|
// units (else its readable units are wrongly rejected → "Decryption
|
||||||
// failed" on exactly those titles).
|
// failed" on exactly those titles).
|
||||||
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
if matches!(self.keys, DecryptKeys::Aacs { .. })
|
||||||
&& !crate::aacs::is_unit_aligned(lba, self.unit_base)
|
&& !crate::aacs::content::is_unit_aligned(lba, self.unit_base)
|
||||||
{
|
{
|
||||||
return Err(crate::error::Error::DecryptFailed);
|
return Err(crate::error::Error::DecryptFailed);
|
||||||
}
|
}
|
||||||
@@ -585,7 +585,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// rip stays fail-loud. Ciphertext is never passed downstream: it is
|
// rip stays fail-loud. Ciphertext is never passed downstream: it is
|
||||||
// replaced by null packets, not emitted.
|
// replaced by null packets, not emitted.
|
||||||
if self.tolerate_decrypt_loss && !self.verify_only {
|
if self.tolerate_decrypt_loss && !self.verify_only {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
let mut concealed = 0usize;
|
let mut concealed = 0usize;
|
||||||
let mut first_lba = lba;
|
let mut first_lba = lba;
|
||||||
for (i, chunk) in buf[..n].chunks_mut(unit_len).enumerate() {
|
for (i, chunk) in buf[..n].chunks_mut(unit_len).enumerate() {
|
||||||
@@ -604,11 +604,12 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// un-restored ciphertext. In-content gating already happened in
|
// un-restored ciphertext. In-content gating already happened in
|
||||||
// `decrypt_buf`, which restored only failed units to ciphertext;
|
// `decrypt_buf`, which restored only failed units to ciphertext;
|
||||||
// clear nav and decrypted tails pass through clean.
|
// clear nav and decrypted tails pass through clean.
|
||||||
if crate::aacs::aacs_unit_still_ciphertext(chunk) {
|
if crate::aacs::content::aacs_unit_still_ciphertext(chunk) {
|
||||||
if concealed == 0 {
|
if concealed == 0 {
|
||||||
first_lba = lba + (i as u32) * crate::aacs::ALIGNED_UNIT_SECTORS;
|
first_lba =
|
||||||
|
lba + (i as u32) * crate::aacs::content::ALIGNED_UNIT_SECTORS;
|
||||||
}
|
}
|
||||||
crate::aacs::fill_null_ts_unit(chunk);
|
crate::aacs::content::fill_null_ts_unit(chunk);
|
||||||
concealed += 1;
|
concealed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,8 +632,10 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// flags, loudly. Cryptographically unreachable in practice.
|
// flags, loudly. Cryptographically unreachable in practice.
|
||||||
let mut forced = 0usize;
|
let mut forced = 0usize;
|
||||||
for chunk in buf[..n].chunks_mut(unit_len) {
|
for chunk in buf[..n].chunks_mut(unit_len) {
|
||||||
if chunk.len() == unit_len && crate::aacs::aacs_unit_needs_decrypt(chunk) {
|
if chunk.len() == unit_len
|
||||||
crate::aacs::fill_null_ts_unit(chunk);
|
&& crate::aacs::content::aacs_unit_needs_decrypt(chunk)
|
||||||
|
{
|
||||||
|
crate::aacs::content::fill_null_ts_unit(chunk);
|
||||||
forced += 1;
|
forced += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1253,12 +1256,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
/// Build a clear 6144-byte AACS unit (TS syncs at the BD-TS stride) then
|
||||||
/// encrypt it under `unit_key` so `aacs::decrypt_unit` recovers it. Mirrors
|
/// encrypt it under `unit_key` so `aacs::content::decrypt_unit` recovers it. Mirrors
|
||||||
/// the encrypt helper in `crate::decrypt`'s tests.
|
/// the encrypt helper in `crate::decrypt`'s tests.
|
||||||
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
fn encrypt_aacs_unit(unit_key: &[u8; 16]) -> Vec<u8> {
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||||
let mut unit = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < unit.len() {
|
while off < unit.len() {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
@@ -1267,14 +1270,14 @@ mod tests {
|
|||||||
// CPI bits on byte 0 so it reads as encrypted; set before key derivation.
|
// CPI bits on byte 0 so it reads as encrypted; set before key derivation.
|
||||||
unit[0] |= 0xC0;
|
unit[0] |= 0xC0;
|
||||||
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
let header: [u8; 16] = unit[..16].try_into().unwrap();
|
||||||
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
|
let derived = crate::aacs::crypto::aes_ecb_encrypt(unit_key, &header);
|
||||||
let mut k = [0u8; 16];
|
let mut k = [0u8; 16];
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
k[i] = derived[i] ^ header[i];
|
k[i] = derived[i] ^ header[i];
|
||||||
}
|
}
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
let cipher = Aes128::new(GenericArray::from_slice(&k));
|
||||||
let mut prev = crate::aacs::decrypt::AACS_IV;
|
let mut prev = crate::aacs::crypto::AACS_IV;
|
||||||
let blocks = (crate::aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let blocks = (crate::aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..blocks {
|
for i in 0..blocks {
|
||||||
let o = 16 + i * 16;
|
let o = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -1347,7 +1350,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"the undecryptable unit is tallied as loss before the read errors"
|
"the undecryptable unit is tallied as loss before the read errors"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1358,7 +1361,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
2 * crate::aacs::ALIGNED_UNIT_LEN as u64,
|
2 * crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"loss must accumulate across reads"
|
"loss must accumulate across reads"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1392,7 +1395,7 @@ mod tests {
|
|||||||
// One unit encrypted under real_key, plus one trailing CLEAR (TS-sync)
|
// One unit encrypted under real_key, plus one trailing CLEAR (TS-sync)
|
||||||
// unit so we can confirm conceal touches ONLY the undecryptable unit.
|
// unit so we can confirm conceal touches ONLY the undecryptable unit.
|
||||||
let enc = encrypt_aacs_unit(&real_key);
|
let enc = encrypt_aacs_unit(&real_key);
|
||||||
let mut clear = vec![0u8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut clear = vec![0u8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut o = 4;
|
let mut o = 4;
|
||||||
while o < clear.len() {
|
while o < clear.len() {
|
||||||
clear[o] = 0x47;
|
clear[o] = 0x47;
|
||||||
@@ -1441,13 +1444,13 @@ mod tests {
|
|||||||
// The undecryptable unit is tallied as loss.
|
// The undecryptable unit is tallied as loss.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"the concealed unit is still counted as loss"
|
"the concealed unit is still counted as loss"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit 0 is now valid NULL TS packets — sync 0x47 at every 192-byte
|
// Unit 0 is now valid NULL TS packets — sync 0x47 at every 192-byte
|
||||||
// stride (offset 4), PID 0x1FFF — and carries no ciphertext.
|
// stride (offset 4), PID 0x1FFF — and carries no ciphertext.
|
||||||
let unit0 = &buf[..crate::aacs::ALIGNED_UNIT_LEN];
|
let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
while off + 192 <= unit0.len() {
|
while off + 192 <= unit0.len() {
|
||||||
assert_eq!(unit0[off + 4], 0x47, "null packet sync at {off}");
|
assert_eq!(unit0[off + 4], 0x47, "null packet sync at {off}");
|
||||||
@@ -1456,12 +1459,13 @@ mod tests {
|
|||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
!crate::aacs::ts_sync_destroyed(unit0),
|
!crate::aacs::content::ts_sync_destroyed(unit0),
|
||||||
"concealed unit reads as well-formed TS, not scrambled"
|
"concealed unit reads as well-formed TS, not scrambled"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit 1 (clear) passed through untouched.
|
// Unit 1 (clear) passed through untouched.
|
||||||
let unit1 = &buf[crate::aacs::ALIGNED_UNIT_LEN..2 * crate::aacs::ALIGNED_UNIT_LEN];
|
let unit1 = &buf
|
||||||
|
[crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read");
|
assert_eq!(unit1, &clear[..], "the clear unit is left exactly as read");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1496,7 +1500,7 @@ mod tests {
|
|||||||
// The byte-exact expected post-decrypt form of unit B (independent decrypt).
|
// The byte-exact expected post-decrypt form of unit B (independent decrypt).
|
||||||
let mut expected_tail = good_tail.clone();
|
let mut expected_tail = good_tail.clone();
|
||||||
assert!(
|
assert!(
|
||||||
crate::aacs::decrypt_unit(&mut expected_tail, &good_key),
|
crate::aacs::content::decrypt_unit(&mut expected_tail, &good_key),
|
||||||
"padding-tail must decrypt under good_key"
|
"padding-tail must decrypt under good_key"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1542,12 +1546,12 @@ mod tests {
|
|||||||
// ONLY the genuinely-undecryptable unit A is tallied / concealed.
|
// ONLY the genuinely-undecryptable unit A is tallied / concealed.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loss.load(Ordering::Relaxed),
|
loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"exactly one unit (the undecryptable one) is counted as loss"
|
"exactly one unit (the undecryptable one) is counted as loss"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unit A → NULL TS (concealed).
|
// Unit A → NULL TS (concealed).
|
||||||
let unit0 = &buf[..crate::aacs::ALIGNED_UNIT_LEN];
|
let unit0 = &buf[..crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
while off + 192 <= unit0.len() {
|
while off + 192 <= unit0.len() {
|
||||||
assert_eq!(unit0[off + 4], 0x47, "unit A null packet sync at {off}");
|
assert_eq!(unit0[off + 4], 0x47, "unit A null packet sync at {off}");
|
||||||
@@ -1562,7 +1566,8 @@ mod tests {
|
|||||||
// Unit B → the GOOD decrypted padding tail, byte-for-byte intact (NOT
|
// Unit B → the GOOD decrypted padding tail, byte-for-byte intact (NOT
|
||||||
// overwritten with NULL TS). This is the silent-data-loss the old
|
// overwritten with NULL TS). This is the silent-data-loss the old
|
||||||
// majority-vote predicate caused.
|
// majority-vote predicate caused.
|
||||||
let unit1 = &buf[crate::aacs::ALIGNED_UNIT_LEN..2 * crate::aacs::ALIGNED_UNIT_LEN];
|
let unit1 = &buf
|
||||||
|
[crate::aacs::content::ALIGNED_UNIT_LEN..2 * crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unit1,
|
unit1,
|
||||||
&expected_tail[..],
|
&expected_tail[..],
|
||||||
@@ -1587,8 +1592,8 @@ mod tests {
|
|||||||
/// `mux::ts` reads as a concealed gap.
|
/// `mux::ts` reads as a concealed gap.
|
||||||
#[test]
|
#[test]
|
||||||
fn null_ts_fill_is_well_formed_and_invisible_to_real_pids() {
|
fn null_ts_fill_is_well_formed_and_invisible_to_real_pids() {
|
||||||
let mut unit = vec![0xAAu8; crate::aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0xAAu8; crate::aacs::content::ALIGNED_UNIT_LEN];
|
||||||
crate::aacs::fill_null_ts_unit(&mut unit);
|
crate::aacs::content::fill_null_ts_unit(&mut unit);
|
||||||
// 32 packets, each: sync 0x47, PID 0x1FFF, adaptation-only (0b10) with a
|
// 32 packets, each: sync 0x47, PID 0x1FFF, adaptation-only (0b10) with a
|
||||||
// discontinuity_indicator in the adaptation field.
|
// discontinuity_indicator in the adaptation field.
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
@@ -1679,7 +1684,7 @@ mod tests {
|
|||||||
"callback must be invoked once with the failing unit"
|
"callback must be invoked once with the failing unit"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
crate::aacs::ts_sync_destroyed(&got[0]),
|
crate::aacs::content::ts_sync_destroyed(&got[0]),
|
||||||
"the sample handed to the callback is the still-scrambled ciphertext"
|
"the sample handed to the callback is the still-scrambled ciphertext"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1703,7 +1708,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
nocb_loss.load(Ordering::Relaxed),
|
nocb_loss.load(Ordering::Relaxed),
|
||||||
crate::aacs::ALIGNED_UNIT_LEN as u64,
|
crate::aacs::content::ALIGNED_UNIT_LEN as u64,
|
||||||
"without a fetch callback the undecryptable unit is loss"
|
"without a fetch callback the undecryptable unit is loss"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1784,7 +1789,7 @@ mod tests {
|
|||||||
"fetch fired for BOTH units — the dry result for A did not latch off B"
|
"fetch fired for BOTH units — the dry result for A did not latch off B"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!crate::aacs::ts_sync_destroyed(&buf2),
|
!crate::aacs::content::ts_sync_destroyed(&buf2),
|
||||||
"unit B is decrypted after its on-demand fetch"
|
"unit B is decrypted after its on-demand fetch"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1899,7 +1904,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < bytes {
|
while u < bytes {
|
||||||
buf[u] |= 0xC0;
|
buf[u] |= 0xC0;
|
||||||
u += crate::aacs::ALIGNED_UNIT_LEN;
|
u += crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
@@ -1998,7 +2003,7 @@ mod tests {
|
|||||||
let mut u = 0;
|
let mut u = 0;
|
||||||
while u < b {
|
while u < b {
|
||||||
buf[u] |= 0xC0; // CPI bits → reads as encrypted
|
buf[u] |= 0xC0; // CPI bits → reads as encrypted
|
||||||
u += crate::aacs::ALIGNED_UNIT_LEN;
|
u += crate::aacs::content::ALIGNED_UNIT_LEN;
|
||||||
}
|
}
|
||||||
Ok(b)
|
Ok(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ impl fu::scsi::ScsiTransport for ScsiAdapter<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Map libfreemkv's host certs (keysource-collected) to the unlock contract's.
|
/// Map libfreemkv's host certs (keysource-collected) to the unlock contract's.
|
||||||
pub(crate) fn map_host_certs(certs: &[crate::aacs::HostCert]) -> Vec<fu::HostCert> {
|
pub(crate) fn map_host_certs(certs: &[crate::aacs::types::HostCert]) -> Vec<fu::HostCert> {
|
||||||
certs
|
certs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| fu::HostCert {
|
.map(|c| fu::HostCert {
|
||||||
|
|||||||
+41
-41
@@ -84,9 +84,9 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
||||||
let mut plain = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut plain = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut offset = 4;
|
let mut offset = 4;
|
||||||
while offset < aacs::ALIGNED_UNIT_LEN {
|
while offset < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
plain[offset] = 0x47; // TS sync byte
|
plain[offset] = 0x47; // TS sync byte
|
||||||
offset += 192;
|
offset += 192;
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
// Step 3: AES-CBC encrypt bytes 16..6144
|
// Step 3: AES-CBC encrypt bytes 16..6144
|
||||||
let cipher = Aes128::new(GenericArray::from_slice(&encrypt_key));
|
let cipher = Aes128::new(GenericArray::from_slice(&encrypt_key));
|
||||||
let mut prev = aacs_iv;
|
let mut prev = aacs_iv;
|
||||||
let num_blocks = (aacs::ALIGNED_UNIT_LEN - 16) / 16;
|
let num_blocks = (aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
|
||||||
for i in 0..num_blocks {
|
for i in 0..num_blocks {
|
||||||
let off = 16 + i * 16;
|
let off = 16 + i * 16;
|
||||||
for j in 0..16 {
|
for j in 0..16 {
|
||||||
@@ -131,29 +131,29 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify it looks encrypted (body TS syncs scrambled)
|
// Verify it looks encrypted (body TS syncs scrambled)
|
||||||
assert!(aacs::ts_sync_destroyed(&plain));
|
assert!(aacs::content::ts_sync_destroyed(&plain));
|
||||||
|
|
||||||
// Now decrypt
|
// Now decrypt
|
||||||
let result = aacs::decrypt_unit(&mut plain, &unit_key);
|
let result = aacs::content::decrypt_unit(&mut plain, &unit_key);
|
||||||
assert!(
|
assert!(
|
||||||
result,
|
result,
|
||||||
"decrypt_unit should return true on valid encrypted unit"
|
"decrypt_unit should return true on valid encrypted unit"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&plain),
|
!aacs::content::ts_sync_destroyed(&plain),
|
||||||
"decrypted unit should read as clear (TS syncs restored)"
|
"decrypted unit should read as clear (TS syncs restored)"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
|
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
|
||||||
let mut sync_count = 0;
|
let mut sync_count = 0;
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
if plain[off] == 0x47 {
|
if plain[off] == 0x47 {
|
||||||
sync_count += 1;
|
sync_count += 1;
|
||||||
}
|
}
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
let expected_syncs = (aacs::ALIGNED_UNIT_LEN - 4) / 192 + 1;
|
let expected_syncs = (aacs::content::ALIGNED_UNIT_LEN - 4) / 192 + 1;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sync_count, expected_syncs,
|
sync_count, expected_syncs,
|
||||||
"TS sync bytes not recovered: got {}, expected {}",
|
"TS sync bytes not recovered: got {}, expected {}",
|
||||||
@@ -176,11 +176,11 @@ fn aacs_disc_hash_deterministic() {
|
|||||||
let data1 = b"Unit_Key_RO.inf test data for deterministic hashing";
|
let data1 = b"Unit_Key_RO.inf test data for deterministic hashing";
|
||||||
let data2 = b"Different data should produce different hash";
|
let data2 = b"Different data should produce different hash";
|
||||||
|
|
||||||
let hash1a = aacs::disc_hash(data1);
|
let hash1a = aacs::inf::disc_hash(data1);
|
||||||
let hash1b = aacs::disc_hash(data1);
|
let hash1b = aacs::inf::disc_hash(data1);
|
||||||
assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");
|
assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");
|
||||||
|
|
||||||
let hash2 = aacs::disc_hash(data2);
|
let hash2 = aacs::inf::disc_hash(data2);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
hash1a, hash2,
|
hash1a, hash2,
|
||||||
"different inputs should produce different hashes"
|
"different inputs should produce different hashes"
|
||||||
@@ -190,7 +190,7 @@ fn aacs_disc_hash_deterministic() {
|
|||||||
assert_eq!(hash1a.len(), 20);
|
assert_eq!(hash1a.len(), 20);
|
||||||
|
|
||||||
// Verify disc_hash_hex formatting
|
// Verify disc_hash_hex formatting
|
||||||
let hex = aacs::disc_hash_hex(&hash1a);
|
let hex = aacs::inf::disc_hash_hex(&hash1a);
|
||||||
assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
|
assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hex.len(),
|
hex.len(),
|
||||||
@@ -225,7 +225,7 @@ fn aacs_decrypt_unit_key_roundtrip() {
|
|||||||
encrypted_uk.copy_from_slice(&block);
|
encrypted_uk.copy_from_slice(&block);
|
||||||
|
|
||||||
// Decrypt with the public API
|
// Decrypt with the public API
|
||||||
let decrypted = aacs::decrypt_unit_key(&vuk, &encrypted_uk);
|
let decrypted = aacs::derive::decrypt_unit_key(&vuk, &encrypted_uk);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decrypted, original_unit_key,
|
decrypted, original_unit_key,
|
||||||
"decrypt_unit_key did not recover original unit key"
|
"decrypt_unit_key did not recover original unit key"
|
||||||
@@ -246,7 +246,7 @@ fn aacs_vuk_derivation_roundtrip() {
|
|||||||
0x30,
|
0x30,
|
||||||
];
|
];
|
||||||
|
|
||||||
let vuk = aacs::derive_vuk(&media_key, &volume_id);
|
let vuk = aacs::derive::derive_vuk(&media_key, &volume_id);
|
||||||
|
|
||||||
// VUK should be non-zero and different from both inputs
|
// VUK should be non-zero and different from both inputs
|
||||||
assert_ne!(vuk, [0u8; 16], "VUK should not be all zeros");
|
assert_ne!(vuk, [0u8; 16], "VUK should not be all zeros");
|
||||||
@@ -254,7 +254,7 @@ fn aacs_vuk_derivation_roundtrip() {
|
|||||||
assert_ne!(vuk, volume_id, "VUK should differ from volume_id");
|
assert_ne!(vuk, volume_id, "VUK should differ from volume_id");
|
||||||
|
|
||||||
// Verify determinism
|
// Verify determinism
|
||||||
let vuk2 = aacs::derive_vuk(&media_key, &volume_id);
|
let vuk2 = aacs::derive::derive_vuk(&media_key, &volume_id);
|
||||||
assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
|
assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,14 +263,14 @@ fn aacs_vuk_derivation_roundtrip() {
|
|||||||
fn aacs_ts_sync_destroyed_detection() {
|
fn aacs_ts_sync_destroyed_detection() {
|
||||||
// A clear unit: TS sync (0x47) intact at every 192-byte packet → not
|
// A clear unit: TS sync (0x47) intact at every 192-byte packet → not
|
||||||
// scrambled. (Flag bits play no role.)
|
// scrambled. (Flag bits play no role.)
|
||||||
let mut clear = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut clear = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
clear[off] = 0x47;
|
clear[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&clear),
|
!aacs::content::ts_sync_destroyed(&clear),
|
||||||
"clear unit (syncs intact) must not be scrambled"
|
"clear unit (syncs intact) must not be scrambled"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -279,21 +279,21 @@ fn aacs_ts_sync_destroyed_detection() {
|
|||||||
flagged[0] = 0xC0; // copy-control bits
|
flagged[0] = 0xC0; // copy-control bits
|
||||||
flagged[7] = 0xC0; // TSC bits
|
flagged[7] = 0xC0; // TSC bits
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&flagged),
|
!aacs::content::ts_sync_destroyed(&flagged),
|
||||||
"flag bits must not be read as encryption"
|
"flag bits must not be read as encryption"
|
||||||
);
|
);
|
||||||
|
|
||||||
// A scrambled body (syncs destroyed) → scrambled.
|
// A scrambled body (syncs destroyed) → scrambled.
|
||||||
let scrambled = vec![0x99u8; aacs::ALIGNED_UNIT_LEN];
|
let scrambled = vec![0x99u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert!(
|
assert!(
|
||||||
aacs::ts_sync_destroyed(&scrambled),
|
aacs::content::ts_sync_destroyed(&scrambled),
|
||||||
"unit with no intact TS syncs must read as scrambled"
|
"unit with no intact TS syncs must read as scrambled"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Too short
|
// Too short
|
||||||
let short = vec![0xFFu8; 100];
|
let short = vec![0xFFu8; 100];
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::ts_sync_destroyed(&short),
|
!aacs::content::ts_sync_destroyed(&short),
|
||||||
"short buffer should not be detected"
|
"short buffer should not be detected"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -303,10 +303,10 @@ fn aacs_ts_sync_destroyed_detection() {
|
|||||||
/// A clear unit (TS syncs intact) should pass through decrypt_unit unchanged.
|
/// A clear unit (TS syncs intact) should pass through decrypt_unit unchanged.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_decrypt_unit_unencrypted_passthrough() {
|
fn aacs_decrypt_unit_unencrypted_passthrough() {
|
||||||
let mut unit = vec![0x42u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0x42u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
// Intact TS syncs every 192 bytes → not scrambled → passthrough.
|
// Intact TS syncs every 192 bytes → not scrambled → passthrough.
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
@@ -315,8 +315,8 @@ fn aacs_decrypt_unit_unencrypted_passthrough() {
|
|||||||
let original = unit.clone();
|
let original = unit.clone();
|
||||||
let key = [0xAA; 16];
|
let key = [0xAA; 16];
|
||||||
|
|
||||||
assert!(!aacs::ts_sync_destroyed(&unit));
|
assert!(!aacs::content::ts_sync_destroyed(&unit));
|
||||||
let result = aacs::decrypt_unit(&mut unit, &key);
|
let result = aacs::content::decrypt_unit(&mut unit, &key);
|
||||||
assert!(result, "clear unit should return true");
|
assert!(result, "clear unit should return true");
|
||||||
assert_eq!(unit, original, "clear unit should be unchanged");
|
assert_eq!(unit, original, "clear unit should be unchanged");
|
||||||
}
|
}
|
||||||
@@ -370,17 +370,17 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
|||||||
0x10,
|
0x10,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut plaintext = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
// TS sync bytes every 192 bytes starting at offset 4
|
// TS sync bytes every 192 bytes starting at offset 4
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
plaintext[off] = 0x47;
|
plaintext[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
// Fill the rest with a recognisable pattern (prime modulus avoids artefacts).
|
// Fill the rest with a recognisable pattern (prime modulus avoids artefacts).
|
||||||
// Index-arithmetic — clearer with a counted loop than an enumerate chain.
|
// Index-arithmetic — clearer with a counted loop than an enumerate chain.
|
||||||
#[allow(clippy::needless_range_loop)]
|
#[allow(clippy::needless_range_loop)]
|
||||||
for i in 16..aacs::ALIGNED_UNIT_LEN {
|
for i in 16..aacs::content::ALIGNED_UNIT_LEN {
|
||||||
if plaintext[i] == 0 {
|
if plaintext[i] == 0 {
|
||||||
plaintext[i] = (i % 251) as u8;
|
plaintext[i] = (i % 251) as u8;
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
|||||||
ref_aes_cbc_encrypt(
|
ref_aes_cbc_encrypt(
|
||||||
&dk,
|
&dk,
|
||||||
&CROSS_AACS_IV,
|
&CROSS_AACS_IV,
|
||||||
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
|
&mut plaintext[16..aacs::content::ALIGNED_UNIT_LEN],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sanity: ciphertext should differ
|
// Sanity: ciphertext should differ
|
||||||
@@ -413,7 +413,7 @@ fn aacs_cross_validation_encrypt_then_decrypt() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// -- Decrypt with the library --
|
// -- Decrypt with the library --
|
||||||
let ok = aacs::decrypt_unit(&mut plaintext, &unit_key);
|
let ok = aacs::content::decrypt_unit(&mut plaintext, &unit_key);
|
||||||
assert!(
|
assert!(
|
||||||
ok,
|
ok,
|
||||||
"decrypt_unit returned false (TS sync verification failed)"
|
"decrypt_unit returned false (TS sync verification failed)"
|
||||||
@@ -436,9 +436,9 @@ fn aacs_cross_validation_alternate_key() {
|
|||||||
0x08,
|
0x08,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut plaintext = vec![0xFFu8; aacs::ALIGNED_UNIT_LEN];
|
let mut plaintext = vec![0xFFu8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
plaintext[off] = 0x47;
|
plaintext[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
@@ -455,10 +455,10 @@ fn aacs_cross_validation_alternate_key() {
|
|||||||
ref_aes_cbc_encrypt(
|
ref_aes_cbc_encrypt(
|
||||||
&dk,
|
&dk,
|
||||||
&CROSS_AACS_IV,
|
&CROSS_AACS_IV,
|
||||||
&mut plaintext[16..aacs::ALIGNED_UNIT_LEN],
|
&mut plaintext[16..aacs::content::ALIGNED_UNIT_LEN],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(aacs::decrypt_unit(&mut plaintext, &unit_key));
|
assert!(aacs::content::decrypt_unit(&mut plaintext, &unit_key));
|
||||||
|
|
||||||
// Decryption clears no flag, so the unit round-trips byte-for-byte.
|
// Decryption clears no flag, so the unit round-trips byte-for-byte.
|
||||||
assert_eq!(&plaintext[..], &expected[..]);
|
assert_eq!(&plaintext[..], &expected[..]);
|
||||||
@@ -473,15 +473,15 @@ fn aacs_bus_decrypt_cross_validation() {
|
|||||||
0x00,
|
0x00,
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut plaintext = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
#[allow(clippy::needless_range_loop)]
|
#[allow(clippy::needless_range_loop)]
|
||||||
for i in 0..aacs::ALIGNED_UNIT_LEN {
|
for i in 0..aacs::content::ALIGNED_UNIT_LEN {
|
||||||
plaintext[i] = ((i * 3 + 17) & 0xFF) as u8;
|
plaintext[i] = ((i * 3 + 17) & 0xFF) as u8;
|
||||||
}
|
}
|
||||||
let expected = plaintext.clone();
|
let expected = plaintext.clone();
|
||||||
|
|
||||||
// Encrypt per-sector: AES-CBC encrypt bytes 16..2048 of each 2048-byte sector
|
// Encrypt per-sector: AES-CBC encrypt bytes 16..2048 of each 2048-byte sector
|
||||||
for sector_start in (0..aacs::ALIGNED_UNIT_LEN).step_by(2048) {
|
for sector_start in (0..aacs::content::ALIGNED_UNIT_LEN).step_by(2048) {
|
||||||
ref_aes_cbc_encrypt(
|
ref_aes_cbc_encrypt(
|
||||||
&read_data_key,
|
&read_data_key,
|
||||||
&CROSS_AACS_IV,
|
&CROSS_AACS_IV,
|
||||||
@@ -490,7 +490,7 @@ fn aacs_bus_decrypt_cross_validation() {
|
|||||||
}
|
}
|
||||||
assert_ne!(&plaintext[16..32], &expected[16..32]);
|
assert_ne!(&plaintext[16..32], &expected[16..32]);
|
||||||
|
|
||||||
aacs::decrypt_bus(&mut plaintext, &read_data_key);
|
aacs::content::decrypt_bus(&mut plaintext, &read_data_key);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plaintext, expected,
|
plaintext, expected,
|
||||||
"bus decrypt did not recover original plaintext"
|
"bus decrypt did not recover original plaintext"
|
||||||
@@ -738,7 +738,7 @@ fn aacs_parse_unit_key_ro_minimal() {
|
|||||||
data[key_pos + i] = (0xA0 + i) as u8;
|
data[key_pos + i] = (0xA0 + i) as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = aacs::parse_unit_key_ro(&data, aacs::AacsVersion::V10);
|
let result = aacs::inf::parse_unit_key_ro(&data, aacs::mkb::AacsVersion::V10);
|
||||||
assert!(
|
assert!(
|
||||||
result.is_some(),
|
result.is_some(),
|
||||||
"parse_unit_key_ro should succeed on valid data"
|
"parse_unit_key_ro should succeed on valid data"
|
||||||
@@ -751,6 +751,6 @@ fn aacs_parse_unit_key_ro_minimal() {
|
|||||||
assert_eq!(ukf.disc_hash.len(), 20);
|
assert_eq!(ukf.disc_hash.len(), 20);
|
||||||
|
|
||||||
// disc_hash should be deterministic
|
// disc_hash should be deterministic
|
||||||
let hash = aacs::disc_hash(&data);
|
let hash = aacs::inf::disc_hash(&data);
|
||||||
assert_eq!(ukf.disc_hash, hash);
|
assert_eq!(ukf.disc_hash, hash);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use libfreemkv::{aacs, decrypt::DecryptKeys};
|
|||||||
#[test]
|
#[test]
|
||||||
fn decrypt_sectors_with_aacs_keys_works() {
|
fn decrypt_sectors_with_aacs_keys_works() {
|
||||||
// Build an encrypted aligned unit
|
// Build an encrypted aligned unit
|
||||||
let mut unit = vec![0xFFu8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0xFFu8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
|
|
||||||
// Set encryption flag (bits 6-7 of byte 0)
|
// Set encryption flag (bits 6-7 of byte 0)
|
||||||
unit[0] |= 0xC0;
|
unit[0] |= 0xC0;
|
||||||
@@ -19,7 +19,7 @@ fn decrypt_sectors_with_aacs_keys_works() {
|
|||||||
for (i, byte) in unit
|
for (i, byte) in unit
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.take(aacs::ALIGNED_UNIT_LEN)
|
.take(aacs::content::ALIGNED_UNIT_LEN)
|
||||||
.skip(1)
|
.skip(1)
|
||||||
{
|
{
|
||||||
*byte = ((i * 3 + 7) & 0xFF) as u8;
|
*byte = ((i * 3 + 7) & 0xFF) as u8;
|
||||||
@@ -28,7 +28,7 @@ fn decrypt_sectors_with_aacs_keys_works() {
|
|||||||
let unit_key: [u8; 16] = [0xAAu8; 16];
|
let unit_key: [u8; 16] = [0xAAu8; 16];
|
||||||
|
|
||||||
// Encrypt the unit using AACS algorithm
|
// Encrypt the unit using AACS algorithm
|
||||||
aacs::decrypt_unit(&mut unit, &unit_key); // decrypt_unit is idempotent on already-encrypted data
|
aacs::content::decrypt_unit(&mut unit, &unit_key); // decrypt_unit is idempotent on already-encrypted data
|
||||||
|
|
||||||
// Now we have encrypted data - create DecryptKeys with actual keys
|
// Now we have encrypted data - create DecryptKeys with actual keys
|
||||||
let mut keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
@@ -83,23 +83,23 @@ fn decrypt_sectors_with_css_keys_works() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn aacs_encryption_flag_detection() {
|
fn aacs_encryption_flag_detection() {
|
||||||
// A clear unit: TS syncs (0x47) intact at every 192-byte packet.
|
// A clear unit: TS syncs (0x47) intact at every 192-byte packet.
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
let mut off = 4;
|
let mut off = 4;
|
||||||
while off < aacs::ALIGNED_UNIT_LEN {
|
while off < aacs::content::ALIGNED_UNIT_LEN {
|
||||||
unit[off] = 0x47;
|
unit[off] = 0x47;
|
||||||
off += 192;
|
off += 192;
|
||||||
}
|
}
|
||||||
// Encryption is the scrambled body (TS syncs destroyed), NOT a flag bit.
|
// Encryption is the scrambled body (TS syncs destroyed), NOT a flag bit.
|
||||||
assert!(!aacs::ts_sync_destroyed(&unit));
|
assert!(!aacs::content::ts_sync_destroyed(&unit));
|
||||||
|
|
||||||
// Flag bits on a synced unit do not make it look encrypted.
|
// Flag bits on a synced unit do not make it look encrypted.
|
||||||
unit[0] = 0xC0;
|
unit[0] = 0xC0;
|
||||||
unit[7] = 0xC0;
|
unit[7] = 0xC0;
|
||||||
assert!(!aacs::ts_sync_destroyed(&unit));
|
assert!(!aacs::content::ts_sync_destroyed(&unit));
|
||||||
|
|
||||||
// Scrambled body (syncs gone) → encrypted.
|
// Scrambled body (syncs gone) → encrypted.
|
||||||
let scrambled = vec![0x99u8; aacs::ALIGNED_UNIT_LEN];
|
let scrambled = vec![0x99u8; aacs::content::ALIGNED_UNIT_LEN];
|
||||||
assert!(aacs::ts_sync_destroyed(&scrambled));
|
assert!(aacs::content::ts_sync_destroyed(&scrambled));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.
|
/// Test: DecryptKeys::is_encrypted() correctly identifies encrypted state.
|
||||||
|
|||||||
Reference in New Issue
Block a user