Files
libfreemkv/docs/aacs-crypto.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

4.2 KiB

AACS Crypto Primitives — Implementation Notes

Rationale and background for the internal (non-public) primitives in src/aacs/crypto.rs. See AACS Encryption for the public-facing key resolution and decryption story.

aes_cbc_encrypt

Forward direction of aes_cbc_decrypt, and its exact inverse ([C] §2.1.2, AES-128CBCE). data.len() must be a multiple of 16; the debug_assert! documents/enforces that contract.

Constructs the cipher ONCE for the whole slice. Driving this from the single-block aes_ecb_encrypt instead rebuilds the AES key schedule per 16-byte block, which for a 6144-byte aligned unit is 383 redundant key expansions.

aes_cbc_decrypt

AES-128-CBC decrypt in-place with the fixed AACS IV ([C] §2.1.2, AES-128CBCD). data.len() must be a multiple of 16; any trailing partial block is silently ignored. All callers pass aligned regions (6128 and 2032 bytes), and the debug_assert! documents/enforces that contract.

This doc was once orphaned onto aes_cbc_encrypt above it, when that function was inserted directly after it with no separating blank line: rustdoc rendered the crate's only forward-direction AACS primitive as "decrypt" and cited the spec's DECRYPT clause for it, while this function had no doc at all. encrypt_unit_is_the_exact_inverse_of_decrypt_unit in content.rs pins the directions behaviourally, so a maintainer "fixing" the contradiction by swapping the two bodies fails the suite instead of shipping a second decryptor behind an already-set encrypted flag.

cbc_decrypt_blocks

Split out of aes_cbc_decrypt so a caller that decrypts several regions under one loop-invariant key expands the schedule once. decrypt_bus (super::content::decrypt_bus) is that caller: bus encryption ([C] §4.2 / the AACS 2.0 Read Data Key) covers bytes 16..2048 of EVERY 2048-byte sector, so a 6144-byte aligned unit is three regions under one read_data_key — three key schedules where one suffices, on the per-unit decrypt hot path of a whole 90 GB read.

aes_g

AES-G(x1, x2) = AES-128D(x1, x2) XOR x2 ([C] §2.1.3, note: uses AES-128D). 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::derive::derive_vuk for the classical VUK form — the math is identical, this exposes it as a neutral primitive for the variant chain.

aesg3

AACS-G3 ([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. Shared with super::variant (its variant chain runs the same SD tree); a single definition keeps the two walks byte-identical.

Test: S0

The AACS-G3 seed s0, transcribed independently from [C] §3.2.2 rather than read from AESG3_SEED — a test that sourced the seed from the production constant would assert that constant against itself and would still pass if it were edited.

Test: aesg3_inverts_to_the_spec_seed_under_aes_encrypt

aesg3 is the node function of the AACS subset-difference tree: every Processing Key the DK walk produces (aesg3(node_key, 1)) and every descent step (aesg3(., 0) / aesg3(., 2)) is one call. A body that returned a fixed block would make every device key in the crate derive the SAME Processing Key, and a ^ that became | or & would derive a wrong-but-plausible one — in both cases the MKB walk simply stops finding Media Keys, with no error to say why.

Pinned through the spec relation rather than a re-implementation: [C] §3.2.2 defines AES-G3 as AES-128D(k, s) XOR s for s = s0 + inc (added into the last seed byte), so applying the FORWARD primitive aes_ecb_encrypt — a different function from the one under test — to aesg3(k, inc) XOR s must reproduce s exactly.

Test: aesg3_yields_three_distinct_subkeys_for_the_three_increments

The Triple Generator's three outputs ([C] §3.2.2: left = inc 0, the Processing Key = inc 1, right = inc 2) are the two child node keys and the Processing Key of ONE tree node. They must be three different keys — if inc were ignored, a descent would revisit its own parent and the walk would derive the same key at every level of the tree.