docs: scrub AACS internals + key-coverage claims from changelog; condense pre-1.0 history
This commit is contained in:
+91
-2694
File diff suppressed because it is too large
Load Diff
@@ -125,9 +125,9 @@ Streams implement a single unified `pes::Stream` trait (re-exported as `PesStrea
|
|||||||
|
|
||||||
### Keys
|
### Keys
|
||||||
|
|
||||||
DVDs (CSS) decrypt out of the box. The library uses 31 compiled-in 1999-era public player keys (for disc-key cracking) combined with Frank Stevenson's known-plaintext title-key attack — no external key file needed.
|
DVDs (CSS) decrypt out of the box, with no external key file needed.
|
||||||
|
|
||||||
Blu-rays and UHD (AACS) require a `keydb.cfg` at `~/.config/freemkv/keydb.cfg` (or passed via `ScanOptions`). The file holds all DKs, PKs, host certs, and per-disc VUKs. No AACS key material is compiled into the binary.
|
Blu-rays and UHD (AACS) require a `keydb.cfg` at `~/.config/freemkv/keydb.cfg` (or passed via `ScanOptions`). No AACS key material is compiled into the binary.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
+50
-264
@@ -2,191 +2,45 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
AACS (Advanced Access Content System) is the encryption layer used by Blu-ray and UHD 4K discs to protect content. libfreemkv implements AACS decryption to enable transparent disc access.
|
AACS (Advanced Access Content System) is the encryption layer used by Blu-ray
|
||||||
|
and UHD 4K discs to protect content. libfreemkv implements AACS decryption so
|
||||||
|
disc access is transparent to the application.
|
||||||
|
|
||||||
There are two major versions:
|
There are two major versions:
|
||||||
|
|
||||||
- **AACS 1.0** -- Used by standard Blu-ray discs. Relies on a custom 160-bit elliptic curve for bus authentication and AES-128 for content encryption. Processing keys and device keys can derive the media key from the disc's Media Key Block (MKB).
|
- **AACS 1.0** -- Used by standard Blu-ray discs.
|
||||||
|
- **AACS 2.0 / 2.1** -- Used by UHD 4K Blu-ray discs. Adds a per-sector bus
|
||||||
- **AACS 2.0** -- Used by UHD 4K Blu-ray discs. Adds a per-sector bus encryption layer (read_data_key) on top of the standard content encryption. Uses P-256/SHA-256 for its native handshake, though drives accept AACS 1.0 host certificates for backward compatibility.
|
encryption layer on top of the standard content encryption. UHD drives accept
|
||||||
|
AACS 1.0 host credentials for backward compatibility.
|
||||||
Both versions use AES-128-CBC for content decryption with a fixed initialization vector. The fundamental key hierarchy is the same: a Volume Unique Key (VUK) decrypts per-title unit keys, which in turn decrypt the content stream.
|
|
||||||
|
All versions use AES-128 for content decryption. The library reads the keys it
|
||||||
|
needs from `keydb.cfg`, walks the disc's Media Key Block (MKB) to resolve the
|
||||||
## Architecture
|
disc's key, and decrypts the content stream. AACS-encrypted discs therefore
|
||||||
|
require a `keydb.cfg`; CSS-protected DVDs do not (see the CSS notes in the
|
||||||
AACS support is split across two modules:
|
library docs).
|
||||||
|
|
||||||
### `aacs.rs` -- Keys and Decryption
|
## How it works (feature level)
|
||||||
|
|
||||||
Handles everything related to key resolution and content decryption:
|
When a disc is scanned, the library:
|
||||||
|
|
||||||
- KEYDB.cfg parsing (device keys, processing keys, host certificates, per-disc entries)
|
1. Reads the disc's AACS key-input files from the `/AACS/` directory.
|
||||||
- Disc hash computation (SHA-1 of `Unit_Key_RO.inf`)
|
2. Resolves the disc's key from `keydb.cfg` — either directly from a per-disc
|
||||||
- VUK resolution chain (4 paths, described below)
|
entry, or by walking the MKB with the keys present in the keydb.
|
||||||
- MKB record parsing and media key derivation
|
3. Performs the drive-level SCSI authentication handshake needed to obtain the
|
||||||
- Subset-difference tree traversal (AACS-G3 key derivation)
|
Volume ID and, for UHD, the bus-decryption key.
|
||||||
- Unit_Key_RO.inf parsing and unit key decryption
|
4. Decrypts the content stream as titles are read.
|
||||||
- Content Certificate parsing (AACS version detection)
|
|
||||||
- Aligned unit decryption (AES-128-CBC)
|
A resolved key is verified against actual disc content before it is applied, so
|
||||||
- Bus decryption (AACS 2.0 read_data_key layer)
|
a stale or wrong key fails loudly rather than producing silent garbage. If no
|
||||||
|
usable key is available for an AACS-encrypted disc, the library surfaces a
|
||||||
### `aacs_handshake.rs` -- SCSI Authentication
|
specific error (the E70xx family) describing which part of the chain was
|
||||||
|
missing, and a missing `keydb.cfg` surfaces as `Error::KeydbLoad` with the
|
||||||
Handles the drive-level SCSI authentication protocol:
|
sentinel path `<no keydb in search paths>`.
|
||||||
|
|
||||||
- ECDH key agreement on the AACS 160-bit curve
|
|
||||||
- ECDSA signing and verification
|
|
||||||
- Bus key derivation
|
|
||||||
- AGID management (allocate/invalidate)
|
|
||||||
- Volume ID retrieval (encrypted with bus key, verified by AES-CMAC)
|
|
||||||
- Read Data Key retrieval (for AACS 2.0 bus decryption)
|
|
||||||
- AACS LA public key certificate verification
|
|
||||||
|
|
||||||
|
|
||||||
## Key Resolution Chain
|
|
||||||
|
|
||||||
When a disc is scanned, `resolve_keys()` attempts four paths in priority order. The first path that succeeds is used.
|
|
||||||
|
|
||||||
### Path 1: KEYDB VUK Lookup (fastest)
|
|
||||||
|
|
||||||
```
|
|
||||||
Unit_Key_RO.inf --> SHA-1 --> disc_hash --> KEYDB lookup --> VUK
|
|
||||||
```
|
|
||||||
|
|
||||||
The disc hash is computed as the SHA-1 digest of the raw `Unit_Key_RO.inf` file from the disc's `/AACS/` directory. This hash is used as the lookup key in `KEYDB.cfg`. If a matching entry contains a VUK (`V` field), it is used directly.
|
|
||||||
|
|
||||||
This is the fast path and resolves the vast majority of discs in a well-maintained KEYDB.
|
|
||||||
|
|
||||||
### Path 2: KEYDB Media Key + Volume ID
|
|
||||||
|
|
||||||
```
|
|
||||||
KEYDB media_key + Volume ID (from SCSI handshake) --> VUK derivation
|
|
||||||
```
|
|
||||||
|
|
||||||
If the disc hash is not in the KEYDB but a KEYDB entry has a matching Volume ID (`I` field) and a media key (`M` field), the VUK is derived:
|
|
||||||
|
|
||||||
```
|
|
||||||
VUK = AES-128-ECB-DECRYPT(media_key, volume_id) XOR volume_id
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires a successful SCSI handshake to obtain the Volume ID.
|
|
||||||
|
|
||||||
### Path 3: MKB + Processing Keys
|
|
||||||
|
|
||||||
```
|
|
||||||
MKB (from disc) + processing_keys (from KEYDB) --> media_key --> VUK
|
|
||||||
```
|
|
||||||
|
|
||||||
Processing keys are pre-computed keys that work against specific MKB versions. For each processing key, the library:
|
|
||||||
|
|
||||||
1. Parses the MKB to extract the Verify Media Key Record (`mk_dv`), subset-difference index, and conditional values (cvalues).
|
|
||||||
2. Tries each processing key against each UV/cvalue pair: `mk = AES-DEC(pk, cvalue) XOR cvalue`.
|
|
||||||
3. Validates the derived media key: `AES-ECB(mk, mk_dv)` must produce 12 leading zero bytes.
|
|
||||||
4. Derives VUK from the validated media key and Volume ID.
|
|
||||||
|
|
||||||
### Path 4: MKB + Device Keys (Subset-Difference Tree)
|
|
||||||
|
|
||||||
```
|
|
||||||
MKB + device_keys --> subset-difference tree traversal --> processing_key --> media_key --> VUK
|
|
||||||
```
|
|
||||||
|
|
||||||
The most complex path. Each device key has an associated node number, UV value, and mask parameters that position it in the AACS subset-difference tree. The library:
|
|
||||||
|
|
||||||
1. Finds the subset-difference entry in the MKB that applies to the device key's node.
|
|
||||||
2. Traverses the tree using AACS-G3 key derivation: `aesg3(key, inc) = AES-DEC(key, seed) XOR seed`, where `seed[15]` is incremented by `inc`. Each tree node produces a left child (inc=0), a processing key (inc=1), and a right child (inc=2).
|
|
||||||
3. At each level, selects left or right based on the UV bit at the current position.
|
|
||||||
4. The resulting processing key is validated against the MKB cvalue to derive the media key.
|
|
||||||
5. VUK is derived from the media key and Volume ID.
|
|
||||||
|
|
||||||
|
|
||||||
## Content Decryption
|
|
||||||
|
|
||||||
### Aligned Units
|
|
||||||
|
|
||||||
AACS encrypts content in aligned units of 6144 bytes (3 sectors of 2048 bytes each). The encryption flag is signaled by the copy_permission_indicator bits in byte 0 of the unit (`unit[0] & 0xC0 != 0`).
|
|
||||||
|
|
||||||
### Per-Unit Key Derivation
|
|
||||||
|
|
||||||
Each aligned unit has its own decryption key derived from the CPS unit key:
|
|
||||||
|
|
||||||
1. **Derive**: AES-128-ECB encrypt the first 16 bytes of the unit (plaintext TP_extra_header) with the unit key.
|
|
||||||
2. **XOR**: XOR the encrypted result with the original 16 bytes to produce the per-unit decryption key.
|
|
||||||
3. **Decrypt**: AES-128-CBC decrypt bytes 16 through 6143 using the per-unit key and the fixed AACS IV.
|
|
||||||
4. **Clear flag**: Clear the encryption indicator bits (`unit[0] &= !0xC0`).
|
|
||||||
|
|
||||||
### Fixed IV
|
|
||||||
|
|
||||||
All AES-CBC operations in AACS use the same fixed initialization vector, defined in the AACS specification.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
|
|
||||||
After decryption, the library verifies correctness by checking for MPEG-TS sync bytes (0x47) at the expected 192-byte packet boundaries within the unit. Blu-ray transport stream packets are 192 bytes: 4-byte TP_extra_header followed by a 188-byte TS packet.
|
|
||||||
|
|
||||||
|
|
||||||
## Bus Encryption
|
|
||||||
|
|
||||||
### AACS 1.0
|
|
||||||
|
|
||||||
Standard Blu-ray discs do not use bus encryption. Content is read directly from the disc and decrypted using the unit key.
|
|
||||||
|
|
||||||
### AACS 2.0
|
|
||||||
|
|
||||||
UHD 4K discs add a per-sector bus encryption layer. The drive encrypts data as it is read from the disc, and the host must decrypt it before applying AACS content decryption.
|
|
||||||
|
|
||||||
Bus encryption uses a **read_data_key** obtained during the SCSI handshake. For each 2048-byte sector within an aligned unit, bytes 16 through 2047 are AES-128-CBC encrypted with the read_data_key and the fixed AACS IV. The first 16 bytes of each sector remain plaintext.
|
|
||||||
|
|
||||||
The full decryption pipeline for AACS 2.0:
|
|
||||||
|
|
||||||
1. **Bus decrypt**: For each sector, AES-128-CBC decrypt bytes 16..2047 with the read_data_key.
|
|
||||||
2. **Content decrypt**: Standard per-unit key derivation and AES-128-CBC decryption as described above.
|
|
||||||
|
|
||||||
|
|
||||||
## SCSI Handshake
|
|
||||||
|
|
||||||
The AACS SCSI authentication handshake establishes a shared bus key between host and drive, then uses it to securely transfer the Volume ID and read data keys.
|
|
||||||
|
|
||||||
### Protocol Flow
|
|
||||||
|
|
||||||
1. **Invalidate AGIDs**: Send REPORT KEY with format 0x3F for AGIDs 0-3 to clear stale sessions.
|
|
||||||
2. **Allocate AGID**: REPORT KEY format 0x00 returns a fresh Authentication Grant ID.
|
|
||||||
3. **Send host credentials**: SEND KEY format 0x01 transmits the host nonce (20 random bytes) and host certificate (92 bytes).
|
|
||||||
4. **Receive drive credentials**: REPORT KEY format 0x01 returns the drive nonce and drive certificate.
|
|
||||||
5. **Receive drive key**: REPORT KEY format 0x02 returns the drive's ephemeral EC key point and ECDSA signature over `host_nonce || drive_key_point`.
|
|
||||||
6. **Verify drive key**: The signature is verified against the drive's public key (extracted from its certificate). AACS 1.0 certificates are verified against the AACS LA public key.
|
|
||||||
7. **Send host key**: The host generates an ephemeral key pair, signs `drive_nonce || host_key_point` with the host private key, and sends via SEND KEY format 0x02.
|
|
||||||
8. **Compute bus key**: ECDH shared secret = `host_private_key * drive_key_point`. The bus key is the low 128 bits of the shared point's x-coordinate.
|
|
||||||
|
|
||||||
### Post-Authentication Reads
|
|
||||||
|
|
||||||
- **Volume ID**: REPORT DISC STRUCTURE format 0x80. Returns 16-byte VID encrypted with the bus key, plus an AES-CMAC MAC for integrity verification.
|
|
||||||
- **Read Data Keys**: REPORT DISC STRUCTURE format 0x84. Returns the read_data_key and write_data_key, each AES-ECB encrypted with the bus key.
|
|
||||||
|
|
||||||
### Elliptic Curve
|
|
||||||
|
|
||||||
AACS 1.0 uses a custom 160-bit Weierstrass curve (`y^2 = x^3 + ax + b mod p`) with 20-byte field elements. The library implements full EC arithmetic: point addition, doubling, scalar multiplication, modular inverse, ECDSA sign/verify, and ECDH key agreement.
|
|
||||||
|
|
||||||
|
|
||||||
## AACS 2.0 Status
|
|
||||||
|
|
||||||
AACS 2.0 discs are detected via the Content Certificate file (`Content000.cer` or `Content001.cer`). A certificate type byte of 0x01 indicates AACS 2.0.
|
|
||||||
|
|
||||||
AACS 2.0 drives are identified by their drive certificate type (0x11). These drives natively use P-256/SHA-256, but accept AACS 1.0 host certificates for backward compatibility.
|
|
||||||
|
|
||||||
Current implementation status:
|
|
||||||
|
|
||||||
- AACS 2.0 detection: **implemented** (Content Certificate parsing, drive cert type check)
|
|
||||||
- AACS 1.0 handshake with AACS 2.0 drives: **implemented** (backward compatibility mode)
|
|
||||||
- Full P-256 AACS 2.0 handshake: **not yet implemented** (prepared but rarely needed since drives accept AACS 1.0 host certs)
|
|
||||||
- Bus decryption with read_data_key: **implemented**
|
|
||||||
- Content decryption: **implemented** (same as AACS 1.0)
|
|
||||||
|
|
||||||
In practice, AACS 2.0 UHD discs work through the backward-compatible AACS 1.0 handshake path, with the addition of read_data_key bus decryption.
|
|
||||||
|
|
||||||
|
|
||||||
## API Usage
|
## API Usage
|
||||||
|
|
||||||
AACS decryption is transparent to the application. The `Disc::scan()` method handles everything automatically:
|
AACS decryption is transparent to the application. `Disc::scan()` handles
|
||||||
|
everything automatically:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use libfreemkv::{Drive, Disc};
|
use libfreemkv::{Drive, Disc};
|
||||||
@@ -203,7 +57,6 @@ if disc.encrypted {
|
|||||||
if let Some(ref aacs) = disc.aacs {
|
if let Some(ref aacs) = disc.aacs {
|
||||||
println!("AACS {}.0", aacs.version);
|
println!("AACS {}.0", aacs.version);
|
||||||
println!("Key source: {}", aacs.key_source.name());
|
println!("Key source: {}", aacs.key_source.name());
|
||||||
println!("Disc hash: {}", aacs.disc_hash);
|
|
||||||
if let Some(mkb_ver) = aacs.mkb_version {
|
if let Some(mkb_ver) = aacs.mkb_version {
|
||||||
println!("MKB version: {}", mkb_ver);
|
println!("MKB version: {}", mkb_ver);
|
||||||
}
|
}
|
||||||
@@ -215,108 +68,41 @@ if disc.encrypted {
|
|||||||
// Read content -- decryption is automatic
|
// Read content -- decryption is automatic
|
||||||
let mut reader = disc.open_title(&mut session, 0).unwrap();
|
let mut reader = disc.open_title(&mut session, 0).unwrap();
|
||||||
while let Some(unit) = reader.read_unit().unwrap() {
|
while let Some(unit) = reader.read_unit().unwrap() {
|
||||||
// unit is 6144 bytes of decrypted content
|
// decrypted content
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The application never touches keys, never calls decryption functions, and never manages handshakes. All of that is internal to `Disc::scan()` and `ContentReader::read_unit()`.
|
The application never touches keys, never calls decryption functions, and never
|
||||||
|
manages handshakes. All of that is internal to `Disc::scan()` and the content
|
||||||
|
reader.
|
||||||
|
|
||||||
### KEYDB Location
|
### KEYDB Location
|
||||||
|
|
||||||
`ScanOptions` controls where the KEYDB is loaded from. If no explicit path is set, the library checks:
|
`ScanOptions` controls where the keydb is loaded from. If no explicit path is
|
||||||
|
set, the library checks the standard config locations. To specify an explicit
|
||||||
1. `~/.config/aacs/KEYDB.cfg`
|
path:
|
||||||
2. `/etc/aacs/KEYDB.cfg`
|
|
||||||
|
|
||||||
To specify an explicit path:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let opts = ScanOptions::with_keydb("/path/to/KEYDB.cfg");
|
let opts = ScanOptions::with_keydb("/path/to/keydb.cfg");
|
||||||
let disc = Disc::scan(&mut session, &opts).unwrap();
|
let disc = Disc::scan(&mut session, &opts).unwrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
### AacsState
|
### AacsState
|
||||||
|
|
||||||
After a successful scan, `disc.aacs` contains an `AacsState` with:
|
After a successful scan, `disc.aacs` contains an `AacsState`:
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `version` | `u8` | AACS version (1 or 2) |
|
| `version` | `u8` | AACS version (1 or 2) |
|
||||||
| `bus_encryption` | `bool` | Whether bus encryption is active |
|
| `bus_encryption` | `bool` | Whether bus encryption is active |
|
||||||
| `mkb_version` | `Option<u32>` | MKB version from disc |
|
| `mkb_version` | `Option<u32>` | MKB version from disc |
|
||||||
| `disc_hash` | `String` | SHA-1 of Unit_Key_RO.inf (hex with 0x prefix) |
|
| `disc_hash` | `String` | Identifier for the disc's key-input files |
|
||||||
| `key_source` | `KeySource` | How keys were resolved |
|
| `key_source` | `KeySource` | How the disc's key was resolved |
|
||||||
| `vuk` | `[u8; 16]` | Volume Unique Key |
|
|
||||||
| `unit_keys` | `Vec<(u32, [u8; 16])>` | Decrypted unit keys (CPS unit number, key) |
|
|
||||||
| `read_data_key` | `Option<[u8; 16]>` | AACS 2.0 bus decryption key |
|
|
||||||
| `volume_id` | `[u8; 16]` | Volume ID from SCSI handshake |
|
|
||||||
|
|
||||||
### KeySource
|
## keydb.cfg
|
||||||
|
|
||||||
| Variant | Description |
|
`keydb.cfg` is the single source of AACS key material. It is a text file (lines
|
||||||
|---------|-------------|
|
starting with `;` or `#` are comments) holding the host credentials and per-disc
|
||||||
| `KeyDb` | VUK found directly in KEYDB by disc hash |
|
entries the library uses to resolve a disc. autorip can auto-download and
|
||||||
| `KeyDbDerived` | Media key + Volume ID from KEYDB, VUK derived |
|
refresh it from a configured URL. The library does not ship any AACS keys
|
||||||
| `ProcessingKey` | MKB + processing keys from KEYDB |
|
compiled into the binary.
|
||||||
| `DeviceKey` | MKB + device keys, subset-difference tree traversal |
|
|
||||||
|
|
||||||
|
|
||||||
## KEYDB.cfg Format Reference
|
|
||||||
|
|
||||||
The KEYDB.cfg file contains all cryptographic material needed for AACS decryption. Lines starting with `;` or `#` are comments.
|
|
||||||
|
|
||||||
### Device Keys
|
|
||||||
|
|
||||||
```
|
|
||||||
| DK | DEVICE_KEY 0x<key> | DEVICE_NODE 0x<node> | KEY_UV 0x<uv> | KEY_U_MASK_SHIFT 0x<shift>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `key`: 16-byte AES device key (hex)
|
|
||||||
- `node`: Device node number in the subset-difference tree (hex)
|
|
||||||
- `uv`: UV value for tree positioning (hex)
|
|
||||||
- `shift`: U mask shift value (hex)
|
|
||||||
|
|
||||||
### Processing Keys
|
|
||||||
|
|
||||||
```
|
|
||||||
| PK | 0x<key>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `key`: 16-byte pre-computed processing key (hex)
|
|
||||||
|
|
||||||
### Host Certificate
|
|
||||||
|
|
||||||
```
|
|
||||||
| HC | HOST_PRIV_KEY 0x<privkey> | HOST_CERT 0x<cert>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `privkey`: 20-byte ECDSA private key (hex)
|
|
||||||
- `cert`: 92-byte AACS host certificate (hex)
|
|
||||||
|
|
||||||
The host certificate is used for SCSI authentication. It contains the host's public key and is signed by the AACS Licensing Administrator.
|
|
||||||
|
|
||||||
### Disc Entries
|
|
||||||
|
|
||||||
```
|
|
||||||
0x<disc_hash> = <title> | D | <date> | M | 0x<media_key> | I | 0x<disc_id> | V | 0x<vuk> | U | <unit_keys>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `disc_hash`: 20-byte SHA-1 of Unit_Key_RO.inf (hex)
|
|
||||||
- `title`: Human-readable disc title
|
|
||||||
- `D`: Date tag, followed by release/rip date
|
|
||||||
- `M`: Media key tag, followed by 16-byte media key (hex)
|
|
||||||
- `I`: Disc ID tag, followed by 16-byte Volume ID (hex)
|
|
||||||
- `V`: VUK tag, followed by 16-byte Volume Unique Key (hex)
|
|
||||||
- `U`: Unit keys tag, followed by space-separated `<unit_num>-0x<key>` pairs
|
|
||||||
|
|
||||||
All fields after the title are optional. A minimal entry needs only the disc hash and VUK:
|
|
||||||
|
|
||||||
```
|
|
||||||
0x<disc_hash> = <title> | V | 0x<vuk>
|
|
||||||
```
|
|
||||||
|
|
||||||
Inline comments are supported with `;`:
|
|
||||||
|
|
||||||
```
|
|
||||||
0x<disc_hash> = <title> | V | 0x<vuk> ; MKBv77
|
|
||||||
```
|
|
||||||
|
|||||||
Reference in New Issue
Block a user