aacs: extract crypto.rs (shared AES primitives + constants)

Relocate the shared low-level primitives into a single crypto module:
aes_ecb_encrypt/decrypt, aes_cbc_decrypt, aes_g (from content/variant) and
aesg3 + AESG3_SEED (from keys), plus AACS_IV. Fixes the scatter where AES-G
lived in the 2.1 file and AES-G3 in keys. Relocation only — no rename, no
logic change (logic-hash identical to baseline; 277 items; 2210 tests green).
This commit is contained in:
Matthew Jackson
2026-07-04 13:37:53 -07:00
parent 31b0ba323a
commit f55d8f7acd
11 changed files with 138 additions and 119 deletions
+1 -1
View File
@@ -275,7 +275,7 @@ pub fn resolve_candidate(
#[cfg(test)]
mod tests {
use super::*;
use crate::aacs::content::aes_ecb_encrypt;
use crate::aacs::crypto::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
+11 -59
View File
@@ -1,15 +1,17 @@
//! 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::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 ──────────────────────────────────────────────────────────
/// 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,
];
/// Size of an AACS aligned unit (3 × 2048-byte sectors). [BD] §3.10.1.
pub const ALIGNED_UNIT_LEN: usize = 6144;
@@ -47,58 +49,6 @@ use crate::consts::BD_SOURCE_PACKET_BYTES;
/// TS sync byte.
const TS_SYNC: u8 = 0x47;
// ── AES primitives ──────────────────────────────────────────────────────────
/// 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];
}
}
}
// ── Content decryption ──────────────────────────────────────────────────────
/// True if a 6144-byte aligned unit's MPEG-TS sync structure is DESTROYED — it
@@ -587,7 +537,9 @@ pub fn decrypt_unit_full(
#[cfg(test)]
mod tests {
use super::super::crypto::aes_ecb_decrypt;
use super::*;
use aes::cipher::BlockEncrypt; // test fixtures build ciphertext directly
#[test]
fn test_aes_ecb_roundtrip() {
+102
View File
@@ -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
}
+9 -30
View File
@@ -1,6 +1,6 @@
//! AACS key resolution — VUK derivation, MKB processing, disc hash, unit key parsing.
use super::content::aes_ecb_decrypt;
use super::crypto::{aes_ecb_decrypt, aesg3};
use super::types::DeviceKey;
// ── AACS version ────────────────────────────────────────────────────────────
@@ -600,27 +600,6 @@ pub fn mkb_is_uhd(mkb: &[u8]) -> Option<bool> {
// ── AACS-G3 key derivation (subset-difference tree) ─────────────────────────
/// AACS-G3 seed constant (`s0`). [C] §3.2.2.
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(super) fn aesg3(key: &[u8; 16], inc: u8) -> [u8; 16] {
let mut seed = AESG3_SEED;
seed[15] = seed[15].wrapping_add(inc);
let mut out = aes_ecb_decrypt(key, &seed);
for i in 0..16 {
out[i] ^= seed[i];
}
out
}
/// 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;
@@ -1536,7 +1515,7 @@ mod tests {
// keeps the crypto covered in libfreemkv. `aes_ecb_encrypt` is
// pub(crate), reachable here but not from keysources — the reason this
// half stays.
use super::super::content::aes_ecb_encrypt;
use super::super::crypto::aes_ecb_encrypt;
let vuk = [0x5Au8; 16];
// A few representative "decrypted" unit keys.
for expected_uk in [[0x11u8; 16], [0x22u8; 16], [0xCDu8; 16]] {
@@ -1726,7 +1705,7 @@ mod tests {
// whose derived Media Key satisfies a synthetic verify record; confirm
// the scan ACCEPTS it against caller-supplied SD/cvalue tables and
// REJECTS a 1-byte corruption.
use super::super::content::aes_ecb_encrypt as enc;
use super::super::crypto::aes_ecb_encrypt as enc;
let pk: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
@@ -1772,7 +1751,7 @@ mod tests {
// recovers mk. Catches the bugs that landed pre-fix:
// * uv XOR step was missing → mk wrong whenever uv != 0
// * AES-128E + 12-zero check instead of AES-128D + magic
use super::super::content::{aes_ecb_decrypt as dec, aes_ecb_encrypt as enc};
use super::super::crypto::{aes_ecb_decrypt as dec, aes_ecb_encrypt as enc};
let pk: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
@@ -2191,7 +2170,7 @@ mod tests {
// The keyless-disc case: this disc's own hash/VID are NOT in keydb, but its
// Media Key IS — filed under a sibling disc that shares its MKB. Path
// 2.5 must km_verifies that MK against the MKB and resolve.
use super::super::content::aes_ecb_encrypt as enc;
use super::super::crypto::aes_ecb_encrypt as enc;
let km = [0x11u8; 16];
let vid = [0x22u8; 16];
// MKB: 0x10 type/version + 0x86 verify record whose mk_dv decrypts under
@@ -2272,7 +2251,7 @@ mod tests {
// Independently compute AES-ECB-D(mk, vid) XOR vid and confirm
// derive_vuk produces the same 16 bytes. A mutation that dropped the
// XOR-VID step, or used encrypt instead of decrypt, fails this.
use super::super::content::aes_ecb_decrypt as dec;
use super::super::crypto::aes_ecb_decrypt as dec;
let mk = [
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E, 0x1F,
@@ -2292,7 +2271,7 @@ mod tests {
// The encrypted unit key in Unit_Key_RO.inf is AES-ECB-E(VUK, uk);
// decrypt_unit_key must be the matching ECB-decrypt. Round-trip via
// encrypt to pin the relation.
use super::super::content::aes_ecb_encrypt as enc;
use super::super::crypto::aes_ecb_encrypt as enc;
let vuk = [0x9Eu8; 16];
let uk = [0x3Cu8; 16];
let enc_uk = enc(&vuk, &uk);
@@ -2692,7 +2671,7 @@ mod tests {
fn resolve_keys_v21_path4_resolves_by_hash() {
// resolve_keys_v21 must hit path 4 (hash→VUK) and stamp version V21,
// deriving unit keys from the VUK.
use super::super::content::aes_ecb_encrypt as enc;
use super::super::crypto::aes_ecb_encrypt as enc;
let data = build_unit_key_ro(1, 64);
// The single encrypted key in build_unit_key_ro is [0x10;16].
let hash = disc_hash(&data);
@@ -2842,7 +2821,7 @@ mod tests {
// - 0x86 Verify Media Key: mk_dv = AES-E(mk, magic || pad)
// and a DK with node=4, uv=2, u_mask_shift=3 so dev_key_v_mask ==
// v_mask: the calc_pk_from_dk loop is a no-op and Kp == aesg3(dk, 1).
use super::super::content::aes_ecb_encrypt as enc;
use super::super::crypto::aes_ecb_encrypt as enc;
let dk_bytes: [u8; 16] = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
+1
View File
@@ -27,6 +27,7 @@
pub mod boil;
pub mod content;
pub mod crypto;
pub mod host_certs;
pub mod keys;
pub mod provider;
+4 -20
View File
@@ -72,7 +72,7 @@
//! that final gate — the per-match magic check no longer protects the
//! variant path.
use super::content::aes_ecb_decrypt;
use super::crypto::{aes_ecb_decrypt, aes_g};
use super::types::DeviceKey;
// ── Public constants ──────────────────────────────────────────────────────
@@ -203,23 +203,6 @@ pub(crate) fn variant_key_data(records: &[MkbRecord]) -> Option<&[u8]> {
.map(|r| r.body.as_slice())
}
// ── AES-G ────────────────────────────────────────────────────────────────
/// 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.
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) ──────────────────────────
// `calc_v_mask` and `calc_pk_from_dk` (and the AES-G3 seed step they ride
@@ -564,7 +547,8 @@ mod tests {
// These three live in `super::keys` now (consolidated SD-walk helpers);
// `use super::*` does not re-export the parent module's private `use`
// imports, so pull them in directly for the tests below.
use super::super::keys::{aesg3, calc_pk_from_dk};
use super::super::crypto::aesg3;
use super::super::keys::calc_pk_from_dk;
#[test]
fn calc_pk_from_dk_terminates_on_nonconvergent_mask() {
@@ -743,7 +727,7 @@ mod tests {
///
/// Returns (records, dk, planted_kp, planted_kmp).
fn synthetic_variant_setup(kmp15: u8) -> (Vec<MkbRecord>, DeviceKey, [u8; 16], [u8; 16]) {
use crate::aacs::content::aes_ecb_encrypt;
use crate::aacs::crypto::aes_ecb_encrypt;
// Build header.
let mut mkb = vec![