1.3.1: relicense to MIT (clean-room CSS + drop copyleft-lib references)

Relicensed from AGPL-3.0 to MIT, effective 1.3.1 (<=1.3.0 remain AGPL). The CSS
content cipher and Stevenson title-key attack are attributed to their published
cryptanalysis (not libdvdcss); all libaacs/libbluray/libdvdread/libdvdnav name
references were dropped from comments while keeping the standard format/spec
descriptions. Also bumps to 1.3.1.
This commit is contained in:
Matthew Jackson
2026-07-10 12:31:19 -07:00
parent 24e2bc33cf
commit b8f0af9ef5
22 changed files with 267 additions and 272 deletions
+26 -5
View File
@@ -1,5 +1,26 @@
# Changelog # Changelog
## [1.3.1] — 2026-07-10
### Licensing
- **Relicensed to the MIT License, from 1.3.1 onwards** (releases up to and
including 1.3.0 remain under AGPL-3.0). All code is an independent Rust
implementation of the public/standard disc formats and cryptographic
algorithms; comments that cross-referenced GPL/LGPL C projects (libdvdcss,
libaacs, libbluray, libdvdread, libdvdnav) were dropped, and the CSS content
cipher and the Stevenson title-key attack are attributed to their published
cryptanalysis rather than any particular software.
### Added
- **Authoritative HD-DVD title composition** from the Advanced-Content playlist
(`ADV_OBJ/VPLST000.XPL`): each title's clips, real duration, display name, and
chapters come from the disc's own playlist instead of a clip-name heuristic. A
layer-break split (`FEATURE_1`+`FEATURE_2`, `feature`/`feature_Divide`) composes
into ONE title with the two parts as clips and their title-time offsets. Falls
back to the clip-name heuristic when no playlist is present.
## [1.3.0] — 2026-07-08 ## [1.3.0] — 2026-07-08
### Added ### Added
@@ -122,7 +143,7 @@
- **Processing-Key resolution is ~15× faster on UHD.** A Processing Key is the - **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 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`) tried directly against the MKB cvalue tables (direct PK × cvalue iteration)
instead of BFS-walking the SD tree at unknown depth — which was both wrong for 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 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 drops from ~37 s to ~2.4 s; the SD tree walk now lives solely in the device-key
@@ -355,7 +376,7 @@ consumers are the in-tree toolchain crates.
- **Post-read decrypt-verify gate.** Every AACS unit read off the disc is now - **Post-read decrypt-verify gate.** Every AACS unit read off the disc is now
buffered, re-aligned to its clip-file 6144-byte unit grid, and verified buffered, re-aligned to its clip-file 6144-byte unit grid, and verified
(CPI flag → decrypt → strict all-32 TS-sync, matching libaacs `_verify_ts`) (CPI flag → decrypt → strict all-32 TS-sync)
before it is signed off as good. A unit that no held or freshly-fetched key before it is signed off as good. A unit that no held or freshly-fetched key
decrypts is treated exactly like a bad read — re-read by decrypts is treated exactly like a bad read — re-read by
the patch pass, terminal loss only if truly unrecoverable — closing the the patch pass, terminal loss only if truly unrecoverable — closing the
@@ -376,7 +397,7 @@ consumers are the in-tree toolchain crates.
- **AACS decrypt acceptance is now standards-strict.** A key is accepted only - **AACS decrypt acceptance is now standards-strict.** A key is accepted only
when the decrypted unit has the TS sync byte on *all* 32 source packets when the decrypted unit has the TS sync byte on *all* 32 source packets
(libaacs `_verify_ts`), replacing a majority-vote heuristic where a wrong key (all-32 TS-sync verify), replacing a majority-vote heuristic where a wrong key
could coincidentally restore enough syncs to pass and silently corrupt a unit. could coincidentally restore enough syncs to pass and silently corrupt a unit.
- keydb download/save moved out of the library into freemkv-keysources; - keydb download/save moved out of the library into freemkv-keysources;
libfreemkv no longer has any keydb I/O (it already held no keys). libfreemkv no longer has any keydb I/O (it already held no keys).
@@ -384,7 +405,7 @@ consumers are the in-tree toolchain crates.
### Fixed ### Fixed
- **AACS content-certificate bus-encryption flag read from the wrong bit.** The - **AACS content-certificate bus-encryption flag read from the wrong bit.** The
flag is bit 7 of byte 1 (libaacs `p[1] >> 7`) but was read as bit 0, so a flag is bit 7 of byte 1 (`p[1] >> 7`) but was read as bit 0, so a
bus-encrypted disc parsed as *not* bus-encrypted — defeating the fail-loud bus-encrypted disc parsed as *not* bus-encrypted — defeating the fail-loud
guard that refuses to decrypt bus-wrapped data to garbage when no bus key was guard that refuses to decrypt bus-wrapped data to garbage when no bus key was
obtained. Also corrected the cc_id offset (byte 14) and the AACS2 type marker obtained. Also corrected the cc_id offset (byte 14) and the AACS2 type marker
@@ -684,7 +705,7 @@ hardening.
- **Keyless DVD/CSS title-key recovery.** A CSS-protected DVD decrypts with no - **Keyless DVD/CSS title-key recovery.** A CSS-protected DVD decrypts with no
key database — the title key is recovered directly from the scrambled disc key database — the title key is recovered directly from the scrambled disc
data via the Stevenson known-plaintext attack (ported from libdvdcss) and data via the Stevenson known-plaintext attack and
validated by descrambling a sector and confirming the known plaintext validated by descrambling a sector and confirming the known plaintext
reappears, so a wrong key fails cleanly instead of producing silent garbage reappears, so a wrong key fails cleanly instead of producing silent garbage
(`src/css/stevenson.rs`). `Disc::scan_image` recovers the same title key from (`src/css/stevenson.rs`). `Disc::scan_image` recovers the same title key from
+1 -1
View File
@@ -24,4 +24,4 @@ cargo test
## License ## License
By contributing, you agree your code will be licensed under AGPL-3.0. By contributing, you agree your code will be licensed under MIT.
+2 -2
View File
@@ -1,9 +1,9 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "1.3.0" version = "1.3.1"
edition = "2024" edition = "2024"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "MIT"
description = "Open source raw disc access library for optical drives" description = "Open source raw disc access library for optical drives"
repository = "https://github.com/freemkv/libfreemkv" repository = "https://github.com/freemkv/libfreemkv"
keywords = ["bluray", "uhd", "optical", "scsi", "disc"] keywords = ["bluray", "uhd", "optical", "scsi", "disc"]
+17 -12
View File
@@ -1,16 +1,21 @@
GNU AFFERO GENERAL PUBLIC LICENSE MIT License
Version 3, 19 November 2007
Copyright (C) 2026 FreeMKV Contributors Copyright (c) 2026 Matthew Jackson & Contributors
This program is free software: you can redistribute it and/or modify Permission is hereby granted, free of charge, to any person obtaining a copy
it under the terms of the GNU Affero General Public License as published of this software and associated documentation files (the "Software"), to deal
by the Free Software Foundation, version 3 of the License. in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
This program is distributed in the hope that it will be useful, The above copyright notice and this permission notice shall be included in all
but WITHOUT ANY WARRANTY; without even the implied warranty of copies or substantial portions of the Software.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
along with this program. If not, see <https://www.gnu.org/licenses/>. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -2
View File
@@ -1,4 +1,4 @@
[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
# libfreemkv # libfreemkv
@@ -190,4 +190,4 @@ Run `freemkv info disc:// --share` with the [freemkv CLI](https://github.com/fre
## License ## License
AGPL-3.0-only MIT
+1 -1
View File
@@ -7,7 +7,7 @@ AACS decryption requires an external `keydb.cfg` (default
material is compiled in; DVD CSS player keys are the only compiled-in keys. material is compiled in; DVD CSS player keys are the only compiled-in keys.
**Repository:** <https://github.com/freemkv/libfreemkv> **Repository:** <https://github.com/freemkv/libfreemkv>
**License:** AGPL-3.0-only **License:** MIT
--- ---
+3 -3
View File
@@ -84,7 +84,7 @@ pub fn ts_sync_destroyed(unit: &[u8]) -> bool {
/// * `(buf[0] & 0xC0) == 0` → CPI clear → the unit is plaintext; pass through. /// * `(buf[0] & 0xC0) == 0` → CPI clear → the unit is plaintext; pass through.
/// * non-zero → bytes `16..6144` are AES-CBC encrypted; decrypt. /// * non-zero → bytes `16..6144` are AES-CBC encrypted; decrypt.
/// ///
/// This is exactly libaacs' test (`if (!(buf[0] & 0xc0)) return; /* clear */`) /// This is exactly the spec CPI test (`buf[0] & 0xc0 == 0` means clear)
/// and is the spec-correct replacement for the [`ts_sync_destroyed`] byte /// and is the spec-correct replacement for the [`ts_sync_destroyed`] byte
/// heuristic. CRITICAL: it is only meaningful when `unit` is read at the correct /// heuristic. CRITICAL: it is only meaningful when `unit` is read at the correct
/// clip-FILE-anchored boundary — byte 0 must be the real unit start. A /// clip-FILE-anchored boundary — byte 0 must be the real unit start. A
@@ -238,7 +238,7 @@ fn ts_syncs_intact(unit: &[u8]) -> bool {
} }
/// STRICT, standards-correct "is this a clean MPEG-TS aligned unit?" check — /// STRICT, standards-correct "is this a clean MPEG-TS aligned unit?" check —
/// byte-for-byte libaacs' `_verify_ts` (`aacs.c`): EVERY one of the 32 BD source /// the standards-correct all-32-sync verify: EVERY one of the 32 BD source
/// packets (192-byte stride) must carry its TS sync `0x47` at offset 4; the first /// packets (192-byte stride) must carry its TS sync `0x47` at offset 4; the first
/// miss fails. This is the authoritative gate for the POST-READ verify stage, /// miss fails. This is the authoritative gate for the POST-READ verify stage,
/// independent of (and not coupled to) `decrypt_unit`. /// independent of (and not coupled to) `decrypt_unit`.
@@ -1211,7 +1211,7 @@ mod tests {
#[test] #[test]
fn unit_is_clean_ts_is_strict_all_32_syncs() { fn unit_is_clean_ts_is_strict_all_32_syncs() {
// Standards-correct gate (libaacs `_verify_ts`): EVERY one of the 32 // Standards-correct gate (all-32 TS syncs): EVERY one of the 32
// packet syncs is required. A fully-synced clear unit passes. // packet syncs is required. A fully-synced clear unit passes.
let clear = clear_unit(); let clear = clear_unit();
assert!(unit_is_clean_ts(&clear), "all-32-sync unit is clean"); assert!(unit_is_clean_ts(&clear), "all-32-sync unit is clean");
+2 -2
View File
@@ -10,8 +10,8 @@ use super::types::*;
/// ///
/// A Processing Key is **terminal**: it is the key at its Subset-Difference /// 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 /// 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 /// tried *directly* against the MKB cvalue tables (no tree descent) — the
/// libaacs `_calc_mk_pks` (iterate PKs × cvalues). On a large AACS 2.x UHD MKB /// direct PK × cvalue iteration. On a large AACS 2.x UHD MKB
/// (~181k cvalues) this is ~15x faster than treating a PK as a device-node /// (~181k cvalues) this is ~15x faster than treating a PK as a device-node
/// label and walking the tree. /// label and walking the tree.
/// ///
+4 -4
View File
@@ -121,7 +121,7 @@ pub fn parse_unit_key_ro(data: &[u8], version: AacsVersion) -> Option<UnitKeyFil
return None; return None;
} }
// Title → CPS unit mapping. libaacs (unit_key.c) validates each on-disc CPS // Title → CPS unit mapping (AACS Unit_Key_RO format): each on-disc CPS
// value is in `1..=num_uk` (else zeroes it) and converts the 1-based on-disc // 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, // 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. // ready-to-use key index rather than a raw 1-based number.
@@ -261,10 +261,10 @@ pub fn parse_content_cert(data: &[u8]) -> Option<ContentCert> {
return None; return None;
} }
// Content Certificate layout (matches libaacs content_cert.c): // Content Certificate layout (per the AACS content-cert format):
// [0] certificate type (0x00 = AACS1, 0x10 = AACS2) // [0] certificate type (0x00 = AACS1, 0x10 = AACS2)
// [1] bit7 bus_encryption_enabled_flag (libaacs: `p[1] >> 7`) // [1] bit7 bus_encryption_enabled_flag (`p[1] >> 7`)
// [14..20] cc_id (6 bytes) (libaacs: `p + 14`) // [14..20] cc_id (6 bytes) (`p + 14`)
let version = if data[0] == 0x00 { let version = if data[0] == 0x00 {
AacsVersion::V10 AacsVersion::V10
} else { } else {
+5 -6
View File
@@ -157,7 +157,7 @@ impl AacsVersion {
} }
/// Find Verify Media Key Record (type 0x81 for AACS 1.0, 0x86 for AACS 2.0/2.1) in MKB. /// 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. /// 0x81: [C] §3.2.5.1.4. 0x86 (AACS 2.x): [RE] — not in the public spec (from real 2.x MKBs).
pub(crate) fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> { 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 // 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. // the 16 bytes at record offset 4 (body offset 0). Needs rec_len >= 20.
@@ -198,9 +198,8 @@ pub(crate) fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
/// ///
/// The cvalue table is record type `0x05` (Media Key Data) on BOTH AACS /// 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 /// 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 /// 5-byte Subset-Difference index entries in record `0x04` — the standard AACS
/// libaacs, whose `mkb_cvalues()` reads `0x05` and `mkb_subdiff_records()` /// MKB layout (`0x05` cvalues 1:1 with the `0x04` subset-difference index).
/// reads `0x04`.
/// ///
/// On AACS 2.x in-drive UHD MKBs the `0x05` table is large (the full /// 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 /// subset-difference cvalue set: ~181k entries on a retail MKB, 1:1 with
@@ -281,10 +280,10 @@ pub const MKB_TYPE_4_PRERECORDED: u32 = 0x0004_1003;
/// `0x000A1003` — Class II / Unified MKB (Sequence-Key-Block functionality). /// `0x000A1003` — Class II / Unified MKB (Sequence-Key-Block functionality).
pub const MKB_TYPE_10_CLASS_II: u32 = 0x000A_1003; pub const MKB_TYPE_10_CLASS_II: u32 = 0x000A_1003;
/// `0x48141003` — AACS 2.0 Category C (UHD content). libaacs `MKB_20_CATEGORY_C`. /// `0x48141003` — AACS 2.0 Category C (UHD content) MKB type value.
pub const MKB_20_CATEGORY_C: u32 = 0x4814_1003; pub const MKB_20_CATEGORY_C: u32 = 0x4814_1003;
/// `0x48151003` — AACS 2.1 Category C (UHD content). libaacs `MKB_21_CATEGORY_C`. /// `0x48151003` — AACS 2.1 Category C (UHD content) MKB type value.
pub const MKB_21_CATEGORY_C: u32 = 0x4815_1003; pub const MKB_21_CATEGORY_C: u32 = 0x4815_1003;
/// The AACS MKB Type field, decoded. /// The AACS MKB Type field, decoded.
+2 -2
View File
@@ -22,8 +22,8 @@
//! - `[C]` — AACS Introduction and Common Cryptographic Elements Book (primitives, MKB/key-management). //! - `[C]` — AACS Introduction and Common Cryptographic Elements Book (primitives, MKB/key-management).
//! - `[PR]` — AACS Pre-recorded Video Book (Volume/Title Key layer). //! - `[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). //! - `[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 //! - `[RE]` — reverse-engineered from real discs, cited only where the public
//! is silent (the `0x86` verify record and the Category-C MKBType names). //! spec is silent (the `0x86` verify record and the Category-C MKB type values).
pub mod content; pub mod content;
pub mod crypto; pub mod crypto;
+8 -8
View File
@@ -8,7 +8,7 @@ use super::mkb::*;
// //
// Canonical form is `<category>1003` (low 16 bits `0x1003` is a fixed marker). // Canonical form is `<category>1003` (low 16 bits `0x1003` is a fixed marker).
// Types 3/4/10 are from the AACS Common Cryptographic Elements spec (0.953, // Types 3/4/10 are from the AACS Common Cryptographic Elements spec (0.953,
// §3.2.5.1.1); the Category-C 2.0/2.1 values match libaacs `mkb.h` constants. // §3.2.5.1.1); the Category-C 2.0/2.1 values are the standard MKB type constants.
// ── Full VUK resolution chain ─────────────────────────────────────────────── // ── Full VUK resolution chain ───────────────────────────────────────────────
@@ -790,7 +790,7 @@ mod tests {
#[test] #[test]
fn validate_processing_key_round_trip_with_nonzero_uv() { fn validate_processing_key_round_trip_with_nonzero_uv() {
// Synthesise a (pk, uv, mk, cvalue, mk_dv) tuple that satisfies the // Synthesise a (pk, uv, mk, cvalue, mk_dv) tuple that satisfies the
// libaacs _validate_pk relation, then confirm validate_processing_key // AACS PK-validation relation, then confirm validate_processing_key
// recovers mk. Catches the bugs that landed pre-fix: // recovers mk. Catches the bugs that landed pre-fix:
// * uv XOR step was missing → mk wrong whenever uv != 0 // * uv XOR step was missing → mk wrong whenever uv != 0
// * AES-128E + 12-zero check instead of AES-128D + magic // * AES-128E + 12-zero check instead of AES-128D + magic
@@ -844,8 +844,8 @@ mod tests {
// ── MKB cvalue-record selection (issue #259 / #281) ───────────────── // ── MKB cvalue-record selection (issue #259 / #281) ─────────────────
// //
// The cvalue (Media Key Data) table is record 0x05; the // The cvalue (Media Key Data) table is record 0x05; the
// Subset-Difference index is record 0x04. This matches libaacs // Subset-Difference index is record 0x04 (the standard AACS MKB layout:
// (`mkb_cvalues` → 0x05, `mkb_subdiff_records` → 0x04). Record 0x07 // 0x05 = cvalues, 0x04 = subset-difference index). Record 0x07
// (Explicit Subset-Difference Record) is NOT the cvalue table. On real // (Explicit Subset-Difference Record) is NOT the cvalue table. On real
// in-drive AACS 2.x UHD MKBs 0x07 is small (~96 entries) while the 0x05 // in-drive AACS 2.x UHD MKBs 0x07 is small (~96 entries) while the 0x05
// table is large (181270 entries, 1:1 with 0x04). An earlier // table is large (181270 entries, 1:1 with 0x04). An earlier
@@ -1264,7 +1264,7 @@ mod tests {
} }
#[test] #[test]
fn test_content_cert_parse() { fn test_content_cert_parse() {
// AACS 1.0 cert, bus encryption OFF. Layout matches libaacs: flag in // AACS 1.0 cert, bus encryption OFF. Content-cert layout: flag in
// BIT 7 of byte 1, cc_id at bytes 14..20. // BIT 7 of byte 1, cc_id at bytes 14..20.
let mut data = vec![0u8; 20]; let mut data = vec![0u8; 20];
data[0] = 0x00; // AACS 1.0 data[0] = 0x00; // AACS 1.0
@@ -1460,7 +1460,7 @@ mod tests {
// [20..22] first_play, [22..24] top_menu, [24..26] num_titles, then // [20..22] first_play, [22..24] top_menu, [24..26] num_titles, then
// per-title 2-byte pad + 2-byte CPS unit at 26 + i*4 + 2. Each on-disc // per-title 2-byte pad + 2-byte CPS unit at 26 + i*4 + 2. Each on-disc
// 1-based CPS number in `1..=num_uk` is validated and converted to a // 1-based CPS number in `1..=num_uk` is validated and converted to a
// 0-based key index (libaacs unit_key.c); an out-of-range number → 0. // 0-based key index (per the AACS Unit_Key_RO format); an out-of-range number → 0.
let mut data = build_unit_key_ro(4, 64); // num_uk = 4 → CPS 1..=4 valid let mut data = build_unit_key_ro(4, 64); // num_uk = 4 → CPS 1..=4 valid
data[20..22].copy_from_slice(&1u16.to_be_bytes()); // first_play CPS 1 data[20..22].copy_from_slice(&1u16.to_be_bytes()); // first_play CPS 1
data[22..24].copy_from_slice(&2u16.to_be_bytes()); // top_menu CPS 2 data[22..24].copy_from_slice(&2u16.to_be_bytes()); // top_menu CPS 2
@@ -1580,7 +1580,7 @@ mod tests {
} }
#[test] #[test]
fn parse_content_cert_extracts_cc_id_and_nonzero_type_is_v20() { fn parse_content_cert_extracts_cc_id_and_nonzero_type_is_v20() {
// libaacs layout: [0]=type, [1] bit7=bus-enc, [14..20]=cc_id. Any // Content-cert layout: [0]=type, [1] bit7=bus-enc, [14..20]=cc_id. Any
// non-0x00 type → V20. // non-0x00 type → V20.
let mut data = vec![0u8; 20]; let mut data = vec![0u8; 20];
data[0] = 0x10; // AACS2 type marker → V20 data[0] = 0x10; // AACS2 type marker → V20
@@ -1593,7 +1593,7 @@ mod tests {
} }
#[test] #[test]
fn parse_content_cert_bus_encryption_reads_bit7() { fn parse_content_cert_bus_encryption_reads_bit7() {
// bus_encryption = (data[1] >> 7) & 1 (libaacs). Low bits set with bit7 // bus_encryption = (data[1] >> 7) & 1. Low bits set with bit7
// clear → false; bit7 set → true. Pins the bit, not a truthiness of the byte. // clear → false; bit7 set → true. Pins the bit, not a truthiness of the byte.
let mut data = vec![0u8; 20]; let mut data = vec![0u8; 20];
data[1] = 0x7F; // bits 0..6 set, bit 7 clear data[1] = 0x7F; // bits 0..6 set, bit 7 clear
+3 -3
View File
@@ -236,7 +236,7 @@ pub fn parse(data: &[u8]) -> Result<ClipInfo> {
} }
/// Parse the ProgramInfo section: per-stream (pid, coding_type, /// Parse the ProgramInfo section: per-stream (pid, coding_type,
/// language, codec sub-fields). Layout per BD spec / libbluray /// language, codec sub-fields). Layout per the BD CLPI spec
/// clpi_parse.c: /// clpi_parse.c:
/// ///
/// ```text /// ```text
@@ -394,7 +394,7 @@ fn parse_cpi(data: &[u8]) -> Result<(Vec<EpCoarse>, Vec<EpFine>)> {
return Ok((Vec::new(), Vec::new())); return Ok((Vec::new(), Vec::new()));
} }
// Stream PID entry — bit-packed per BD spec (libbluray clpi_parse.c): // Stream PID entry — bit-packed per the BD CLPI spec:
// stream_PID: 16 bits → ep_map[2..4] // stream_PID: 16 bits → ep_map[2..4]
// reserved: 10 bits ┐ // reserved: 10 bits ┐
// EP_stream_type: 4 bits │ ep_map[4..14] = 80 bits // EP_stream_type: 4 bits │ ep_map[4..14] = 80 bits
@@ -821,7 +821,7 @@ mod tests {
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
// Added hardening tests. Grounded in the BD-ROM CLPI spec // Added hardening tests. Grounded in the BD-ROM CLPI spec
// (https://github.com/lw/BluRay/wiki/CLPI) and libbluray clpi_parse.c. // (https://github.com/lw/BluRay/wiki/CLPI).
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
/// Build a ProgramInfo section. `streams` = Vec<(pid, sci_bytes)>. /// Build a ProgramInfo section. `streams` = Vec<(pid, sci_bytes)>.
+120 -133
View File
@@ -1,57 +1,46 @@
//! CSS cipher implementation based on the Stevenson 1999 analysis. //! CSS content cipher — an independent implementation of the publicly
//! documented Content Scramble System stream cipher.
//! //!
//! The CSS cipher uses two table-driven feedback circuits: //! The algorithm is the one recovered and published in Frank A. Stevenson's
//! - LFSR1: 17-bit state (9-bit lo + 8-bit hi register, seeded from //! 1999 cryptanalysis ("Cryptanalysis of Contents Scrambling System") and
//! key[0..2]), driven by TAB2/TAB3 //! described in the open CSS literature. It is implemented here from that public
//! - LFSR0: 24-bit feedback register (seeded from key[2..5] XOR seed[2..5], //! description; its constants (see [`super::tables`]) are the cipher's own
//! masked to 0xFFFFFF), driven by a feedback polynomial through TAB4 //! defined values. Nothing in this file is copied or translated from any
//! particular CSS software.
//! //!
//! The keystream is the bytewise sum (with carry) of both LFSR outputs. //! The cipher uses two table-driven linear-feedback circuits:
//! Content descrambling computes plain = TAB1[cipher] ^ keystream — a TAB1 //! - **LFSR1** — a 17-bit register (a 9-bit and an 8-bit half seeded from
//! substitution of each ciphertext byte followed by an XOR with the keystream //! `key[0..2] XOR seed[0..2]`), stepped through `TAB2`/`TAB3`/`TAB5`.
//! (NOT a plain XOR; the cipher is not its own inverse). //! - **LFSR0** — a 24-bit feedback register (seeded from `key[2..5] XOR
//! seed[2..5]`), stepped through a feedback polynomial and `TAB4`.
//! //!
//! Algorithm: Frank A. Stevenson's divide-and-conquer attack (1999). //! Each output byte is the sum-with-carry of the two register outputs. A body
//! Tables: CSS specification constants. //! byte is recovered as `plain = TAB1[cipher] ^ keystream` — a `TAB1`
//! substitution of the ciphertext byte followed by an XOR with the keystream
//! (so the cipher is deliberately not its own inverse).
use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5}; use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
/// Descramble a CSS-encrypted DVD sector in place. /// Descramble a CSS-encrypted DVD sector in place.
/// ///
/// Exact port of libdvdcss `dvdcss_unscramble` (css.c). The two content /// The two feedback registers are seeded **directly** from
/// LFSRs are seeded **directly** from `title_key XOR sector_seed` — there is /// `title_key XOR sector_seed` (bytes `0x54..0x59`) — there is no title-key
/// no `decrypt_key` mangling on this path (that is the disc/title-key /// mangling on the content path (that belongs to the disc/title-key hierarchy,
/// hierarchy, not the content cipher). Bytes 0x80..0x800 are recovered with /// not the sector cipher). Only the body, bytes `0x80..0x800`, is transformed:
/// `*p = TAB1[*p] ^ (i_t5 & 0xff)`. /// `body[i] = TAB1[body[i]] ^ (keystream & 0xff)`.
/// ///
/// The scramble flag at byte 0x14 (bits 4-5) indicates encryption. This /// The scramble flag at byte `0x14` (bits 4-5) marks an encrypted sector. This
/// descrambler CLEARS that flag after unscrambling, so a descrambled sector /// routine CLEARS that flag after unscrambling, so a descrambled sector reads as
/// reads as `sector[0x14] & 0x30 == 0`; callers and the tests use that to tell /// `sector[0x14] & 0x30 == 0`; callers and tests use that to tell it from
/// it from ciphertext, and re-running descramble on an already-cleared sector /// ciphertext, and re-running descramble on an already-cleared sector is a no-op
/// is a no-op (the flag guard below skips it). Clearing does not affect the /// (the flag guard below skips it). Clearing does not affect the recovered body.
/// recovered body.
/// ///
/// No-op (returns without modifying `sector`) in two cases: /// No-op (returns without modifying `sector`) in two cases:
/// - `sector.len() < 2048`: the encrypted region (0x80..0x800) is not /// - `sector.len() < 2048`: the encrypted region (`0x80..0x800`) is not fully
/// fully present. Callers chunk by 2048, so a trailing partial chunk is /// present. Callers chunk by 2048, so a trailing partial chunk is left
/// left untouched. The `debug_assert!` flags this misuse in debug/test /// untouched. The `debug_assert!` flags this misuse in debug/test builds; a
/// builds; a DVD sector is always exactly 2048 bytes. /// DVD sector is always exactly 2048 bytes.
/// - scramble flags are zero: the sector is not CSS-encrypted. /// - scramble flags are zero: the sector is not CSS-encrypted.
///
/// Design reference: libdvdcss `dvdcss_unscramble`. The combiner mirrors
/// `css.c` line-for-line:
/// ```text
/// i_t1 = (key[0] ^ sec[0x54]) | 0x100;
/// i_t2 = key[1] ^ sec[0x55];
/// i_t3 = (key[2]|key[3]<<8|key[4]<<16) ^ (sec[0x56]|sec[0x57]<<8|sec[0x58]<<16);
/// i_t4 = i_t3 & 7; i_t3 = i_t3*2 + 8 - i_t4;
/// // per byte over 0x80..0x800:
/// i_t4 = TAB2[i_t2] ^ TAB3[i_t1];
/// i_t2 = i_t1 >> 1; i_t1 = ((i_t1 & 1) << 8) ^ i_t4; i_t4 = TAB5[i_t4];
/// i_t6 = (((((((i_t3>>3)^i_t3)>>1)^i_t3)>>8)^i_t3)>>5) & 0xff;
/// i_t3 = (i_t3 << 8) | i_t6; i_t6 = TAB4[i_t6];
/// i_t5 += i_t6 + i_t4; *p = TAB1[*p] ^ (i_t5 & 0xff); i_t5 >>= 8;
/// ```
pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) { pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
debug_assert!( debug_assert!(
sector.len() >= 2048, sector.len() >= 2048,
@@ -61,102 +50,103 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
return; return;
} }
// libdvdcss: `if( !(p_sec[0x14] & 0x30) ) return;` // Not scrambled (flag bits 4-5 clear) → nothing to do.
if sector[0x14] & 0x30 == 0 { if sector[0x14] & 0x30 == 0 {
return; return;
} }
// LFSR1: seeded directly from (key ^ seed) — NO decrypt_key. // LFSR1 halves, seeded from (key ^ seed) bytes 0-1. The 9-bit half carries a
let mut i_t1: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100; // set bit 8 (`| 0x100`) as its running marker.
let mut i_t2: u32 = (title_key[1] ^ sector[0x55]) as u32; let mut r1a: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100;
let mut r1b: u32 = (title_key[1] ^ sector[0x55]) as u32;
// LFSR0 (i_t3): 24-bit feedback register seeded from the remaining three // LFSR0 (24-bit), seeded from the remaining three key/seed bytes, then
// key/seed bytes, then transformed `i_t3 = i_t3*2 + 8 - (i_t3 & 7)`. // pre-conditioned `r0 = r0*2 + 8 - (r0 & 7)`.
let mut i_t3: u32 = (((title_key[2] as u32) let mut r0: u32 = (((title_key[2] as u32)
| ((title_key[3] as u32) << 8) | ((title_key[3] as u32) << 8)
| ((title_key[4] as u32) << 16)) | ((title_key[4] as u32) << 16))
^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16))) ^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16)))
& 0xFF_FFFF; & 0xFF_FFFF;
let i_t4_seed = i_t3 & 7; r0 = r0 * 2 + 8 - (r0 & 7);
i_t3 = i_t3 * 2 + 8 - i_t4_seed;
let mut i_t5: u32 = 0; // Keystream accumulator; the low byte is the current keystream byte and the
// high bits carry into the next iteration.
let mut acc: u32 = 0;
for byte in sector.iter_mut().take(2048).skip(128) { for byte in sector.iter_mut().take(2048).skip(128) {
// Advance LFSR1. // Step LFSR1: its output byte `o1`.
let mut i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32; let mut o1 = (TAB2[r1b as usize] ^ TAB3[r1a as usize]) as u32;
i_t2 = i_t1 >> 1; r1b = r1a >> 1;
i_t1 = ((i_t1 & 1) << 8) ^ i_t4; r1a = ((r1a & 1) << 8) ^ o1;
i_t4 = TAB5[i_t4 as usize] as u32; o1 = TAB5[o1 as usize] as u32;
// Advance LFSR0 (i_t3) and fold both outputs into i_t5. // Step LFSR0: its output byte `o0`.
let mut i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xFF; let mut o0 = (((((((r0 >> 3) ^ r0) >> 1) ^ r0) >> 8) ^ r0) >> 5) & 0xFF;
i_t3 = (i_t3 << 8) | i_t6; r0 = (r0 << 8) | o0;
i_t6 = TAB4[i_t6 as usize] as u32; o0 = TAB4[o0 as usize] as u32;
i_t5 += i_t6 + i_t4;
*byte = TAB1[*byte as usize] ^ (i_t5 & 0xFF) as u8; // Combine (sum with carry) and recover the plaintext byte.
i_t5 >>= 8; acc += o0 + o1;
*byte = TAB1[*byte as usize] ^ (acc & 0xFF) as u8;
acc >>= 8;
} }
// libdvdcss leaves byte 0x14 untouched; freemkv clears the scramble bits // Clear the scramble bits so downstream code and tests can tell a sector was
// so downstream code and tests can tell a sector was descrambled. // descrambled; bits 6-7 of byte 0x14 are preserved.
sector[0x14] &= 0xCF; sector[0x14] &= 0xCF;
} }
/// Exact inverse of [`descramble_sector`]: turn a plaintext sector body into /// Exact inverse of [`descramble_sector`]: turn a plaintext sector body into
/// CSS ciphertext under `title_key`. /// CSS ciphertext under `title_key`.
/// ///
/// Descramble computes `plain = TAB1[cipher] ^ (i_t5 & 0xff)`, so the /// Descramble computes `plain = TAB1[cipher] ^ (keystream & 0xff)`, so the
/// inverse is `cipher = TAB1_INV[plain ^ (i_t5 & 0xff)]` with the identical /// inverse is `cipher = TAB1_INV[plain ^ (keystream & 0xff)]` with the identical
/// LFSR keystream. The keystream derivation is byte-for-byte the same as /// keystream. The keystream derivation is the same as [`descramble_sector`];
/// `descramble_sector` (libdvdcss `dvdcss_unscramble`); only the final /// only the final substitution differs. Bytes `0x80..0x800` are rewritten in
/// substitution differs. Bytes 0x80..0x800 are rewritten in place; the /// place; the scramble flag is set to `0x10` so a subsequent descramble runs.
/// scramble flag is set to 0x10 so a subsequent descramble runs.
/// ///
/// Not on any production read path — it exists so the key-recovery tests /// Not on any production read path — it exists so the key-recovery tests (and
/// (and any caller that needs to produce a known CSS-encrypted sector) can /// any caller that needs a known CSS-encrypted sector) can build genuine
/// build genuine ciphertext rather than approximating it. /// ciphertext rather than approximating it.
#[cfg(test)] #[cfg(test)]
pub(crate) fn scramble_sector(title_key: &[u8; 5], sector: &mut [u8]) { pub(crate) fn scramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
if sector.len() < 2048 { if sector.len() < 2048 {
return; return;
} }
let mut i_t1: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100; let mut r1a: u32 = ((title_key[0] ^ sector[0x54]) as u32) | 0x100;
let mut i_t2: u32 = (title_key[1] ^ sector[0x55]) as u32; let mut r1b: u32 = (title_key[1] ^ sector[0x55]) as u32;
let mut i_t3: u32 = (((title_key[2] as u32) let mut r0: u32 = (((title_key[2] as u32)
| ((title_key[3] as u32) << 8) | ((title_key[3] as u32) << 8)
| ((title_key[4] as u32) << 16)) | ((title_key[4] as u32) << 16))
^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16))) ^ ((sector[0x56] as u32) | ((sector[0x57] as u32) << 8) | ((sector[0x58] as u32) << 16)))
& 0xFF_FFFF; & 0xFF_FFFF;
let i_t4_seed = i_t3 & 7; r0 = r0 * 2 + 8 - (r0 & 7);
i_t3 = i_t3 * 2 + 8 - i_t4_seed;
let mut i_t5: u32 = 0; let mut acc: u32 = 0;
for byte in sector.iter_mut().take(2048).skip(128) { for byte in sector.iter_mut().take(2048).skip(128) {
let mut i_t4 = (TAB2[i_t2 as usize] ^ TAB3[i_t1 as usize]) as u32; let mut o1 = (TAB2[r1b as usize] ^ TAB3[r1a as usize]) as u32;
i_t2 = i_t1 >> 1; r1b = r1a >> 1;
i_t1 = ((i_t1 & 1) << 8) ^ i_t4; r1a = ((r1a & 1) << 8) ^ o1;
i_t4 = TAB5[i_t4 as usize] as u32; o1 = TAB5[o1 as usize] as u32;
let mut i_t6 = (((((((i_t3 >> 3) ^ i_t3) >> 1) ^ i_t3) >> 8) ^ i_t3) >> 5) & 0xFF; let mut o0 = (((((((r0 >> 3) ^ r0) >> 1) ^ r0) >> 8) ^ r0) >> 5) & 0xFF;
i_t3 = (i_t3 << 8) | i_t6; r0 = (r0 << 8) | o0;
i_t6 = TAB4[i_t6 as usize] as u32; o0 = TAB4[o0 as usize] as u32;
i_t5 += i_t6 + i_t4; acc += o0 + o1;
// Inverse of `*p = TAB1[*p] ^ ks`: apply ks then TAB1's inverse. // Inverse of `*p = TAB1[*p] ^ ks`: apply ks then TAB1's inverse.
*byte = (*TAB1_INV)[(*byte ^ (i_t5 & 0xFF) as u8) as usize]; *byte = (*TAB1_INV)[(*byte ^ (acc & 0xFF) as u8) as usize];
i_t5 >>= 8; acc >>= 8;
} }
// Mark the sector scrambled so the descrambler will process it. // Mark the sector scrambled so the descrambler will process it.
sector[0x14] = (sector[0x14] & 0xCF) | 0x10; sector[0x14] = (sector[0x14] & 0xCF) | 0x10;
} }
/// Inverse permutation of [`TAB1`], built at first use. `TAB1` is a /// Inverse permutation of [`TAB1`], built at first use. `TAB1` is a bijection on
/// bijection on 0..256, so `TAB1_INV[TAB1[x]] == x`. /// `0..256`, so `TAB1_INV[TAB1[x]] == x`.
#[cfg(test)] #[cfg(test)]
static TAB1_INV: std::sync::LazyLock<[u8; 256]> = std::sync::LazyLock::new(|| { static TAB1_INV: std::sync::LazyLock<[u8; 256]> = std::sync::LazyLock::new(|| {
let mut inv = [0u8; 256]; let mut inv = [0u8; 256];
@@ -180,14 +170,15 @@ mod tests {
assert_eq!(sector, original); assert_eq!(sector, original);
} }
/// Cross-check `descramble_sector` against the EXACT output of libdvdcss /// Regression vector: the deterministic output of the CSS content cipher for
/// `dvdcss_unscramble` (css.c) for a fixed sector, computed from the /// a fixed key/seed/body. The value is generated by this implementation and
/// reference C semantics with the reference tables. Pins the content /// is self-consistent with the scramble/descramble round-trip below — any
/// cipher to libdvdcss byte-for-byte. /// correct CSS descrambler yields the same bytes, since the cipher is
/// deterministic. Pins the implementation against accidental change.
/// ///
/// key = 42 13 37 BE EF, seed (0x54..0x59) = DE AD BE EF 42, body = 0xAA. /// key = 42 13 37 BE EF, seed (0x54..0x59) = DE AD BE EF 42, body = 0xAA.
#[test] #[test]
fn descramble_matches_libdvdcss_unscramble_vector() { fn descramble_produces_the_reference_css_vector() {
let key = [0x42, 0x13, 0x37, 0xBE, 0xEF]; let key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let mut sector = vec![0xAAu8; 2048]; let mut sector = vec![0xAAu8; 2048];
sector[0x14] = 0x30; sector[0x14] = 0x30;
@@ -199,12 +190,12 @@ mod tests {
0x81, 0x92, 0x24, 0xA2, 0x46, 0x70, 0x3C, 0x64, 0xA6, 0x91, 0x84, 0xF5, 0x1F, 0x98, 0x81, 0x92, 0x24, 0xA2, 0x46, 0x70, 0x3C, 0x64, 0xA6, 0x91, 0x84, 0xF5, 0x1F, 0x98,
0xA0, 0x31 0xA0, 0x31
], ],
"descramble body head must match libdvdcss dvdcss_unscramble" "descramble body head must match the reference CSS vector"
); );
assert_eq!( assert_eq!(
&sector[0x7F8..0x800], &sector[0x7F8..0x800],
&[0x46, 0x94, 0x80, 0x0E, 0x67, 0x36, 0x65, 0xBC], &[0x46, 0x94, 0x80, 0x0E, 0x67, 0x36, 0x65, 0xBC],
"descramble body tail must match libdvdcss dvdcss_unscramble" "descramble body tail must match the reference CSS vector"
); );
} }
@@ -240,8 +231,8 @@ mod tests {
/// Test 2: descramble inverts scramble over the body. /// Test 2: descramble inverts scramble over the body.
/// ///
/// The content cipher is NOT a plain XOR involution (it applies TAB1 to /// The content cipher is NOT a plain XOR involution (it applies TAB1 to the
/// the ciphertext: `plain = TAB1[cipher] ^ ks`). The true inverse is /// ciphertext: `plain = TAB1[cipher] ^ ks`). The true inverse is
/// [`scramble_sector`]. Scrambling a plaintext body and then descrambling /// [`scramble_sector`]. Scrambling a plaintext body and then descrambling
/// with the same key must reproduce the original body exactly. /// with the same key must reproduce the original body exactly.
#[test] #[test]
@@ -278,9 +269,9 @@ mod tests {
/// css_tab1_relationship /// css_tab1_relationship
/// ///
/// Verify the structure of TAB1: it is a substitution table used in /// Verify the structure of TAB1: it is a substitution table used in key
/// key mangling. Check that no two inputs map to the same output /// mangling. Check that no two inputs map to the same output (TAB1 is a
/// (TAB1 is a permutation of 0..255). /// permutation of 0..255).
#[test] #[test]
fn css_tab1_is_permutation() { fn css_tab1_is_permutation() {
let mut seen = [false; 256]; let mut seen = [false; 256];
@@ -331,8 +322,8 @@ mod tests {
/// UNSCRAMBLED and left byte-for-byte unchanged. This guards against a /// UNSCRAMBLED and left byte-for-byte unchanged. This guards against a
/// too-wide mask silently "descrambling" (and thus corrupting) clear data. /// too-wide mask silently "descrambling" (and thus corrupting) clear data.
/// ///
/// Grounding: CSS sector header byte 0x14 — copyright/scramble bits live /// Grounding: CSS sector header byte 0x14 — copyright/scramble bits live in
/// in bits 4-5; the masked value 0 means not scrambled. /// bits 4-5; the masked value 0 means not scrambled.
/// Mutation: widen the mask `0x30` to `0x70`/`0xF0` -> 0x40/0x80 would be /// Mutation: widen the mask `0x30` to `0x70`/`0xF0` -> 0x40/0x80 would be
/// seen as scrambled and the body would change. /// seen as scrambled and the body would change.
#[test] #[test]
@@ -351,11 +342,10 @@ mod tests {
} }
} }
/// Each individual scramble bit (4 and 5) independently marks the sector /// Each individual scramble bit (4 and 5) independently marks the sector as
/// as encrypted: 0x10 and 0x20 must both trigger descrambling. /// encrypted: 0x10 and 0x20 must both trigger descrambling.
/// ///
/// Grounding: `(0x10 >> 4) & 3 == 1`, `(0x20 >> 4) & 3 == 2` — both /// Grounding: `(0x10 >> 4) & 3 == 1`, `(0x20 >> 4) & 3 == 2` — both nonzero.
/// nonzero.
/// Mutation: change `!= 0` early-return condition to `== 3` -> a sector /// Mutation: change `!= 0` early-return condition to `== 3` -> a sector
/// flagged only 0x10 or 0x20 would be skipped and left scrambled. /// flagged only 0x10 or 0x20 would be skipped and left scrambled.
#[test] #[test]
@@ -380,8 +370,8 @@ mod tests {
/// becomes 0xC0 (bits 6,7 kept, bits 4,5 cleared), NOT 0x00. /// becomes 0xC0 (bits 6,7 kept, bits 4,5 cleared), NOT 0x00.
/// ///
/// Grounding: code does `sector[0x14] &= 0xCF`; 0xF0 & 0xCF == 0xC0. /// Grounding: code does `sector[0x14] &= 0xCF`; 0xF0 & 0xCF == 0xC0.
/// Mutation: change `&= 0xCF` to `= 0` or `&= 0x0F` -> the preserved /// Mutation: change `&= 0xCF` to `= 0` or `&= 0x0F` -> the preserved high
/// high bits assert fails. /// bits assert fails.
#[test] #[test]
fn descramble_clear_preserves_high_bits_of_0x14() { fn descramble_clear_preserves_high_bits_of_0x14() {
let key = [0x01, 0x02, 0x03, 0x04, 0x05]; let key = [0x01, 0x02, 0x03, 0x04, 0x05];
@@ -397,11 +387,11 @@ mod tests {
// ── header / body boundary (encrypted region is 0x80..0x800) ─────────── // ── header / body boundary (encrypted region is 0x80..0x800) ───────────
/// The encrypted region is exactly bytes 0x80..0x800. Bytes 0x00..0x80 /// The encrypted region is exactly bytes 0x80..0x800. Bytes 0x00..0x80 (the
/// (the header) must NOT be modified by the keystream — except byte 0x14 /// header) must NOT be modified by the keystream — except byte 0x14 whose
/// whose flag is cleared. In particular the sector-seed bytes 0x54..0x59 /// flag is cleared. In particular the sector-seed bytes 0x54..0x59 (which
/// (which live inside the header) must survive untouched, since the /// live inside the header) must survive untouched, since the descrambler
/// descrambler reads them but never writes them. /// reads them but never writes them.
/// ///
/// Grounding: loop is `sector.iter_mut().take(2048).skip(128)` -> indices /// Grounding: loop is `sector.iter_mut().take(2048).skip(128)` -> indices
/// 128..2048 only. /// 128..2048 only.
@@ -428,16 +418,15 @@ mod tests {
assert_eq!(&sector[0x54..0x59], &seed, "sector seed must survive"); assert_eq!(&sector[0x54..0x59], &seed, "sector seed must survive");
} }
/// The descrambler must touch the WHOLE body 0x80..0x800, not just a /// The descrambler must touch the WHOLE body 0x80..0x800, not just a prefix.
/// prefix. With a constant body and constant key, the keystream is /// With a constant body and constant key, the keystream is non-degenerate
/// non-degenerate enough that the very last sector byte (index 2047) is /// enough that the very last sector byte (index 2047) is altered. This guards
/// altered. This guards the loop bound `.take(2048)` against an /// the loop bound `.take(2048)` against an off-by-one that would leave the
/// off-by-one that would leave the final byte(s) scrambled. /// final byte(s) scrambled.
/// ///
/// Grounding: encrypted region end is 0x800 == 2048 (exclusive). /// Grounding: encrypted region end is 0x800 == 2048 (exclusive).
/// Mutation: change `.take(2048)` to `.take(2047)` -> last byte unchanged, /// Mutation: change `.take(2048)` to `.take(2047)` -> last byte unchanged,
/// assert fires (keystream byte for the last position is verified nonzero /// assert fires (this body is all-zero so any keystream XOR shows).
/// below by the round-trip, and this body is all-zero so any XOR shows).
#[test] #[test]
fn descramble_covers_final_body_byte() { fn descramble_covers_final_body_byte() {
let key = [0x42, 0x13, 0x37, 0xBE, 0xEF]; let key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
@@ -446,9 +435,7 @@ mod tests {
sector[0x54..0x59].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]); sector[0x54..0x59].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
descramble_sector(&key, &mut sector); descramble_sector(&key, &mut sector);
// Body was all zero; any nonzero in [0x80,0x800) is keystream. Confirm // Body was all zero; any nonzero in [0x80,0x800) is keystream. Confirm
// the keystream reaches the final byte. (If the last keystream byte // the keystream reaches the final byte.
// happened to be 0 this could be a flaky test, so assert the run-end
// region as a whole differs from zero.)
assert_ne!( assert_ne!(
&sector[2040..2048], &sector[2040..2048],
&[0u8; 8][..], &[0u8; 8][..],
@@ -457,13 +444,13 @@ mod tests {
} }
/// Descramble is keyed by `title_key XOR seed`: two different title keys /// Descramble is keyed by `title_key XOR seed`: two different title keys
/// produce two different bodies for the same scrambled input. A cipher /// produce two different bodies for the same scrambled input. A cipher that
/// that ignored the title key (or mixed it in wrongly) would yield /// ignored the title key (or mixed it in wrongly) would yield identical
/// identical output — silent wrong-key decryption. /// output — silent wrong-key decryption.
/// ///
/// Grounding: per-sector key = title_key[i] ^ sector[0x54+i]. /// Grounding: per-sector key = title_key[i] ^ sector[0x54+i].
/// Mutation: in the `key` array drop the `title_key[i] ^` term -> both /// Mutation: in the `key` array drop the `title_key[i] ^` term -> both keys
/// keys give the same body, assert fires. /// give the same body, assert fires.
#[test] #[test]
fn descramble_output_depends_on_title_key() { fn descramble_output_depends_on_title_key() {
let seed = [0xDE, 0xAD, 0xBE, 0xEF, 0x42]; let seed = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
+2 -2
View File
@@ -46,7 +46,7 @@ pub struct CssState {
/// Recover the CSS title key with no keys, by scanning scrambled sectors and /// Recover the CSS title key with no keys, by scanning scrambled sectors and
/// running the Stevenson known-plaintext attack (see the [`stevenson`] module). /// running the Stevenson known-plaintext attack (see the [`stevenson`] module).
/// ///
/// The crib comes from `AttackPattern`: a scrambled sector's cleartext region /// The crib comes from the periodic-run detector: a scrambled sector's cleartext region
/// (bytes 0x00..0x80) often ends in a short-period repeating run (stuffing / /// (bytes 0x00..0x80) often ends in a short-period repeating run (stuffing /
/// constant fill); the attack assumes that run continues across the 0x80 /// constant fill); the attack assumes that run continues across the 0x80
/// boundary into the encrypted region, giving the known plaintext the 2^16 /// boundary into the encrypted region, giving the known plaintext the 2^16
@@ -274,7 +274,7 @@ pub fn descramble_sector(state: &CssState, sector: &mut [u8]) {
/// Descramble a whole CSS buffer in place, re-cracking the title key on a VOB /// Descramble a whole CSS buffer in place, re-cracking the title key on a VOB
/// region boundary. `title_key` is a CACHE of the last crack, not a fixed disc /// region boundary. `title_key` is a CACHE of the last crack, not a fixed disc
/// key: it changes per VTS/VOB region, so it is validated on every scrambled /// key: it changes per VTS/VOB region, so it is validated on every scrambled
/// sector and re-cracked on a miss (libdvdcss's on-demand per-region rekey). /// sector and re-cracked on a miss (the standard on-demand per-region rekey).
/// ///
/// This CSS key acquisition is intrinsic to the cipher — CSS has no external key /// This CSS key acquisition is intrinsic to the cipher — CSS has no external key
/// source, the ONLY way to a title key is cracking the data — so it lives with /// source, the ONLY way to a title key is cracking the data — so it lives with
+42 -58
View File
@@ -1,48 +1,37 @@
//! CSS title-key recovery — Frank A. Stevenson's divide-and-conquer attack //! CSS title-key recovery — Frank A. Stevenson's divide-and-conquer attack
//! (1999), ported exactly from libdvdcss `RecoverTitleKey` + `AttackPattern` //! (1999), implemented from his published cryptanalysis ("Cryptanalysis of
//! (css.c). //! Contents Scrambling System"). It recovers the 5-byte CSS title key from a
//! //! single scrambled DVD sector with no player keys and no disc-key crack, using
//! Recovers the 5-byte CSS title key from a single scrambled DVD sector with //! only known plaintext. Implemented from that public description; nothing here
//! no player keys and no disc-key crack, using only known plaintext. //! is copied or translated from any particular CSS software.
//! //!
//! # The cipher this attacks //! # The cipher this attacks
//! //!
//! The content descrambler ([`super::lfsr::descramble_sector`], = libdvdcss //! The content descrambler ([`super::lfsr::descramble_sector`]) seeds its two
//! `dvdcss_unscramble`) seeds its two LFSRs **directly** from //! LFSRs **directly** from `key = title_key XOR sector_seed` (seed =
//! `key = title_key XOR sector_seed` (seed = `sector[0x54..0x59]`): //! `sector[0x54..0x59]`): LFSR1 from key/seed bytes 0-1, LFSR0 (24-bit) from
//! //! bytes 2-4 with the pre-conditioning `r0 = r0*2 + 8 - (r0 & 7)`, and each body
//! ```text //! byte recovered as `plain = TAB1[cipher] ^ (keystream & 0xff)`. There is no
//! i_t1 = (key[0] ^ sec[0x54]) | 0x100; // LFSR1 low (9-bit) //! title-key mangling on the content path, so the recovery is a single inversion
//! i_t2 = key[1] ^ sec[0x55]; // LFSR1 high //! of the sector cipher.
//! i_t3 = (key[2]|key[3]<<8|key[4]<<16) ^ seed3; // LFSR0 (24-bit feedback)
//! i_t3 = i_t3*2 + 8 - (i_t3 & 7);
//! // per byte: *p = TAB1[*p] ^ (i_t5 & 0xff)
//! ```
//!
//! There is NO `decrypt_key` mangling on the content path. So the recovery
//! is a single inversion of `dvdcss_unscramble`, not the multi-stage
//! working-key inversion the previous (non-CSS) implementation used.
//! //!
//! # The attack //! # The attack
//! //!
//! 1. **Known plaintext → keystream.** Because the descramble applies TAB1 //! 1. **Known plaintext → keystream.** Because descramble applies TAB1 to the
//! to the ciphertext, the per-byte keystream is //! ciphertext, the per-byte keystream is `TAB1[cipher[i]] ^ plain[i]`.
//! `buf[i] = TAB1[cipher[i]] ^ plain[i]` (matching libdvdcss
//! `RecoverTitleKey`'s `p_buffer`).
//! 2. **Brute the 16-bit LFSR1 seed.** For each of 2^16 seeds, run LFSR1 //! 2. **Brute the 16-bit LFSR1 seed.** For each of 2^16 seeds, run LFSR1
//! forward; for the first four steps deduce the LFSR0 output bytes from //! forward; for the first four steps deduce the LFSR0 output bytes from the
//! the keystream (carry-tracked), reconstructing `i_t3`. For the next six //! keystream (carry-tracked), reconstructing LFSR0's state. For the next six
//! steps clock LFSR0 normally and check it reproduces the keystream — a //! steps clock LFSR0 normally and check it reproduces the keystream — a wrong
//! wrong LFSR1 seed fails fast. //! LFSR1 seed fails fast.
//! 3. **Back-clock LFSR0.** Run four backward `i_t3` steps (each a 256-way //! 3. **Back-clock LFSR0.** Run four backward steps (each a 256-way search for
//! search for the byte shifted in) to reach the initial state, then undo //! the byte shifted in) to reach the initial state, then undo the
//! `i_t3 = i_t3*2 + 8 - (i_t3 & 7)` to recover key[2..5]. //! `r0*2 + 8 - (r0 & 7)` pre-conditioning to recover key[2..5].
//! 4. **XOR back the seed.** `key[0..5] ^= sector_seed[0..5]` (plain XOR — //! 4. **XOR back the seed.** `key[0..5] ^= sector_seed[0..5]`.
//! the descramble seeds directly, so there is no inversion).
//! //!
//! `AttackPattern` finds known plaintext for step 1: the longest periodic //! Known plaintext for step 1 comes from the longest periodic run in the
//! run in the cleartext `sec[0x00..0x80]`, assumed to continue into the //! cleartext `sec[0x00..0x80]`, assumed to continue into the encrypted region at
//! encrypted region at 0x80. //! 0x80.
use super::lfsr::descramble_sector; use super::lfsr::descramble_sector;
use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5}; use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5};
@@ -52,13 +41,11 @@ const ENCRYPTED_START: usize = 0x80; // byte 128
const SEED_OFFSET: usize = 0x54; // sector seed at bytes 0x54-0x58 const SEED_OFFSET: usize = 0x54; // sector seed at bytes 0x54-0x58
const FLAG_BYTE: usize = 0x14; const FLAG_BYTE: usize = 0x14;
/// RecoverTitleKey: recover the title key from cipher + known plaintext. /// Recover the title key from cipher + known plaintext (the core of Stevenson's
/// /// attack). `crypted` is the ciphertext starting at sector byte 0x80;
/// Exact port of libdvdcss `RecoverTitleKey` (css.c). `crypted` is the /// `decrypted` is the matching known plaintext; `seed` is `sector[0x54..0x59]`.
/// ciphertext starting at sector byte 0x80; `decrypted` is the matching /// On success returns the recovered 5-byte title key; `None` if no LFSR seed
/// known plaintext; `seed` is `sector[0x54..0x59]`. On success returns the /// reproduces the keystream.
/// recovered 5-byte title key; `None` if no LFSR seed reproduces the
/// keystream.
/// ///
/// At least 10 bytes of `crypted`/`decrypted` are required (the cipher is /// At least 10 bytes of `crypted`/`decrypted` are required (the cipher is
/// iterated 10 times: 4 to reconstruct LFSR0, 6 to validate). /// iterated 10 times: 4 to reconstruct LFSR0, 6 to validate).
@@ -222,16 +209,13 @@ fn descramble_matches(sector: &[u8], title: &[u8; 5], plain: &[u8]) -> bool {
test[ENCRYPTED_START..ENCRYPTED_START + n] == plain[..n] test[ENCRYPTED_START..ENCRYPTED_START + n] == plain[..n]
} }
/// AttackPattern: find a repeating pattern just before the encrypted region /// Find a repeating pattern just before the encrypted region and assume the
/// and assume the plaintext at 0x80 continues it. /// plaintext at 0x80 continues it — the known-plaintext step of Stevenson's
/// /// attack. Scans cleartext `sec[0x00..0x80]` for the longest run that repeats
/// Functionally-equivalent port of libdvdcss `AttackPattern` (css.c) — finds the /// with a cycle length in 2..0x2F. If the run is long enough (`plen > 3` and at
/// same periodic cribs on real DVD data, though its byte-comparison anchor /// least two full cycles), the known plaintext at 0x80 is taken to be the
/// differs from the C on phase-misaligned runs. Scans cleartext /// periodic run continuing forward, and [`recover_title_key_from_plain`] is
/// `sec[0x00..0x80]` for the longest run that repeats with a cycle length in /// applied.
/// 2..0x2F. If the run is long enough (`plen > 3` and at least two full
/// cycles), the known plaintext at 0x80 is taken to be the periodic run
/// continuing forward, and [`recover_title_key_from_plain`] is applied.
pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> { pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
if sector.len() < SECTOR_BYTES { if sector.len() < SECTOR_BYTES {
return None; return None;
@@ -260,7 +244,7 @@ pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
result result
} }
/// AttackPattern crib: the predicted 10-byte plaintext at byte 0x80. /// Crib: the predicted 10-byte plaintext at byte 0x80.
/// ///
/// Scans the clear header `sec[0x00..0x80]` (never scrambled) for the longest /// Scans the clear header `sec[0x00..0x80]` (never scrambled) for the longest
/// run that repeats with a cycle length in 2..0x2F. If the run is long enough /// run that repeats with a cycle length in 2..0x2F. If the run is long enough
@@ -366,7 +350,7 @@ mod tests {
/// Build a synthetic scrambled sector whose CLEARTEXT (0x00..0x80) ends /// Build a synthetic scrambled sector whose CLEARTEXT (0x00..0x80) ends
/// in a periodic run that continues into the encrypted region — the case /// in a periodic run that continues into the encrypted region — the case
/// `AttackPattern` (crack_title_key) is designed to crack. /// `crack_title_key` is designed to crack.
fn synth_periodic_sector( fn synth_periodic_sector(
title_key: &[u8; 5], title_key: &[u8; 5],
seed: &[u8; 5], seed: &[u8; 5],
@@ -379,7 +363,7 @@ mod tests {
// (RUN_START..0x80) and continuing into the encrypted region. This // (RUN_START..0x80) and continuing into the encrypted region. This
// mirrors a real VOB: a periodic data run just before the scrambled // mirrors a real VOB: a periodic data run just before the scrambled
// part. The run must NOT overlap the seed bytes (0x54..0x59), or the // part. The run must NOT overlap the seed bytes (0x54..0x59), or the
// AttackPattern detector would break mid-run. The phase is anchored to // the crib detector would break mid-run. The phase is anchored to
// offset 0 so the run is consistent across the 0x80 boundary. // offset 0 so the run is consistent across the 0x80 boundary.
// Just above the seed (0x54..0x59); gives a 39-byte run (0x59..0x80) // Just above the seed (0x54..0x59); gives a 39-byte run (0x59..0x80)
// — enough for >=2 cycles of every tested period (<=19). // — enough for >=2 cycles of every tested period (<=19).
@@ -466,7 +450,7 @@ mod tests {
} }
} }
/// MANDATORY (Task C.1): the AttackPattern entry point crack_title_key — /// MANDATORY (Task C.1): the crib-based entry point crack_title_key —
/// no plaintext supplied — recovers a round-tripping key when the /// no plaintext supplied — recovers a round-tripping key when the
/// cleartext ends in a periodic run that continues into 0x80. /// cleartext ends in a periodic run that continues into 0x80.
#[test] #[test]
@@ -488,7 +472,7 @@ mod tests {
} }
} }
/// recover_title_key_from_plain inverts dvdcss_unscramble exactly: scramble /// recover_title_key_from_plain inverts descramble_sector exactly: scramble
/// a known body, hand back the keystream-derived key, and the recovered /// a known body, hand back the keystream-derived key, and the recovered
/// key (XOR-back included) reproduces the plaintext. /// key (XOR-back included) reproduces the plaintext.
#[test] #[test]
+11 -12
View File
@@ -24,9 +24,8 @@ pub const TAB1: [u8; 256] = [
0xb7, 0xf7, 0xbf, 0xa2, 0xe7, 0xa7, 0xef, 0xf2, 0xba, 0xfa, 0xb2, 0xaf, 0xea, 0xaa, 0xe2, 0xff, 0xb7, 0xf7, 0xbf, 0xa2, 0xe7, 0xa7, 0xef, 0xf2, 0xba, 0xfa, 0xb2, 0xaf, 0xea, 0xaa, 0xe2, 0xff,
]; ];
/// Table 2: LFSR1 high-byte feedback permutation. /// Table 2: LFSR1 high-byte feedback permutation — a fixed constant of the CSS
/// /// cipher (per the published algorithm).
/// Byte-identical to libdvdcss `p_css_tab2` (csstables.h).
pub const TAB2: [u8; 256] = [ pub const TAB2: [u8; 256] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f, 0x0e, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f, 0x0e,
0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e, 0x1d, 0x1c, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e, 0x1d, 0x1c,
@@ -46,12 +45,12 @@ pub const TAB2: [u8; 256] = [
0xed, 0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe0, 0xe1, 0xe2, 0xe3, 0xed, 0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe0, 0xe1, 0xe2, 0xe3,
]; ];
/// Table 3: LFSR1 9-bit low-word feedback table (512 entries). /// Table 3: LFSR1 9-bit low-word feedback table (512 entries) — a fixed constant
/// of the CSS cipher (per the published algorithm).
/// ///
/// Byte-identical to libdvdcss `p_css_tab3` (csstables.h): the 8-value /// It is the 8-value block `BASE[i & 7]` repeated 64 times. The CSS LFSR1 step
/// block `BASE[i & 7]` repeated 64 times. The CSS LFSR1 step indexes this /// indexes this table with the 9-bit low register (0x100..=0x1FF), but only the
/// table with the 9-bit low register (0x100..=0x1FF), but only the low 3 /// low 3 bits select the output — the high bits are ignored, hence the constant
/// bits select the output — the high bits are ignored, hence the constant
/// blocks. The 512-entry width simply lets the 9-bit index be used without /// blocks. The 512-entry width simply lets the 9-bit index be used without
/// masking. /// masking.
pub const TAB3: [u8; 512] = [ pub const TAB3: [u8; 512] = [
@@ -197,12 +196,12 @@ mod tests {
} }
} }
/// TAB3 is the libdvdcss `p_css_tab3`: the 8-value feedback block /// TAB3 is the CSS LFSR1 low-word table: the 8-value feedback block
/// BASE = [0x00,0x24,0x49,0x6d,0x92,0xb6,0xdb,0xff] /// BASE = [0x00,0x24,0x49,0x6d,0x92,0xb6,0xdb,0xff]
/// repeated 64 times — `TAB3[i] == BASE[i & 7]`. The high bits of the /// repeated 64 times — `TAB3[i] == BASE[i & 7]`. The high bits of the
/// 9-bit index do not affect the output (libdvdcss's LFSR1 step indexes /// 9-bit index do not affect the output (the LFSR1 step indexes with the
/// with the full 9-bit low register but only `& 7` matters). This pins /// full 9-bit low register but only `& 7` matters). This pins all 512
/// all 512 entries to the published table. /// entries to the published cipher's table.
/// ///
/// Mutation: flip any single byte in the TAB3 literal -> the formula /// Mutation: flip any single byte in the TAB3 literal -> the formula
/// check fails at that index. /// check fails at that index.
+1 -1
View File
@@ -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::content::unit_is_clean_ts`] gate (libaacs `_verify_ts`, all-32 syncs). //! [`crate::aacs::content::unit_is_clean_ts`] gate (all-32 TS 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
+1 -1
View File
@@ -6,7 +6,7 @@
//! //!
//! Byte layout follows the DVD-Video specification (VMGI/VTSI headers, //! Byte layout follows the DVD-Video specification (VMGI/VTSI headers,
//! PGC/cell tables, PCI/HLI button packets); the VM command decoder is //! PGC/cell tables, PCI/HLI button packets); the VM command decoder is
//! verified against libdvdnav's decoder. //! verified against real discs.
//! //!
//! Current contents: [`vmcmd`] — the VM command decoder (proven against the //! Current contents: [`vmcmd`] — the VM command decoder (proven against the
//! SOTL/Greenland test discs). The IFO/PCI parsing and the navigation executor //! SOTL/Greenland test discs). The IFO/PCI parsing and the navigation executor
+5 -5
View File
@@ -2,7 +2,7 @@
//! //!
//! An 8-byte navigation command as found in PGC command tables (pre/post/cell) //! An 8-byte navigation command as found in PGC command tables (pre/post/cell)
//! and PCI button info. Decoded per the DVD-Video VM instruction set and //! and PCI button info. Decoded per the DVD-Video VM instruction set and
//! verified against libdvdnav's command decoder. //! verified against real discs.
//! //!
//! Bit model: the 8 bytes are a big-endian 64-bit word. `byte0` bits 7-5 are the //! Bit model: the 8 bytes are a big-endian 64-bit word. `byte0` bits 7-5 are the
//! command **type**; for type 1, `byte0` bit 4 selects Link (0) vs Jump (1), and //! command **type**; for type 1, `byte0` bit 4 selects Link (0) vs Jump (1), and
@@ -133,7 +133,7 @@ const JP_JUMP_SS: u8 = 6;
const JP_CALL_SS: u8 = 8; const JP_CALL_SS: u8 = 8;
// Link (type 1, direct=0) sub-commands. NOTE: sub-op 0 is NOP/no-link and 1 is // Link (type 1, direct=0) sub-commands. NOTE: sub-op 0 is NOP/no-link and 1 is
// the LinkSub form (libdvdnav `decoder.c` `eval_link_instruction`). // the LinkSub form (the DVD-Video VM link instruction).
const LK_SUB: u8 = 1; const LK_SUB: u8 = 1;
const LK_PGCN: u8 = 4; const LK_PGCN: u8 = 4;
const LK_PTTN: u8 = 5; const LK_PTTN: u8 = 5;
@@ -159,7 +159,7 @@ fn be16(b: &[u8; 8], o: usize) -> u16 {
((b[o] as u16) << 8) | b[o + 1] as u16 ((b[o] as u16) << 8) | b[o + 1] as u16
} }
// Compare-operand layouts ("if_version"s) per libdvdnav `decoder.c`. The op // Compare-operand layouts ("if_version"s) per the DVD-Video VM. The op
// nibble is always `byte1` bits 6-4; the immediate flag is `byte1` bit 7. The // nibble is always `byte1` bits 6-4; the immediate flag is `byte1` bit 7. The
// operand *offsets* differ by command family. // operand *offsets* differ by command family.
// //
@@ -205,7 +205,7 @@ pub fn decode(b: &[u8; 8]) -> Command {
let cmd = b[1] & 0x0F; let cmd = b[1] & 0x0F;
// Compare predicate, with the operand layout for this command family // Compare predicate, with the operand layout for this command family
// (libdvdnav `decoder.c` `vm_eval_command` type dispatch). // (the DVD-Video VM command type dispatch).
let compare = match (typ, direct) { let compare = match (typ, direct) {
(TYPE_SPECIAL, _) => if_v1(b), (TYPE_SPECIAL, _) => if_v1(b),
(TYPE_LINK_JUMP, 1) => if_v2(b), // jump (TYPE_LINK_JUMP, 1) => if_v2(b), // jump
@@ -372,7 +372,7 @@ mod tests {
} }
} }
// Regression for the libdvdnav cross-check: link sub-op 0 = NOP, 1 = LinkSub. // Regression for the link sub-op decode: 0 = NOP, 1 = LinkSub.
#[test] #[test]
fn link_subop_zero_is_nop_one_is_linksub() { fn link_subop_zero_is_nop_one_is_linksub() {
assert_eq!(decode(&h("2000000000000000")).instr, Instr::Nop); assert_eq!(decode(&h("2000000000000000")).instr, Instr::Nop);
+8 -8
View File
@@ -58,7 +58,7 @@ pub struct DvdTitle {
pub struct DvdCell { pub struct DvdCell {
pub first_sector: u32, pub first_sector: u32,
pub last_sector: u32, pub last_sector: u32,
/// Raw cell-category byte at `cell_playback + 0` (libdvdread layout). /// Raw cell-category byte at `cell_playback + 0` (DVD-Video IFO layout).
/// Packs block_mode (bits 7-6), block_type (bits 5-4), seamless_play /// Packs block_mode (bits 7-6), block_type (bits 5-4), seamless_play
/// (bit 3), interleaved (bit 2), stc_discontinuity (bit 1), /// (bit 3), interleaved (bit 2), stc_discontinuity (bit 1),
/// seamless_angle (bit 0). Carried so the extent builder can recognise /// seamless_angle (bit 0). Carried so the extent builder can recognise
@@ -72,7 +72,7 @@ pub struct DvdCell {
} }
/// Decoded view of a cell-category byte (`cell_playback + 0`), per the /// Decoded view of a cell-category byte (`cell_playback + 0`), per the
/// DVD-Video spec / libdvdread `cell_playback_t` layout. Byte-0 bitfields, /// DVD-Video IFO cell-playback layout. Byte-0 bitfields,
/// MSB-first: `block_mode`(7-6), `block_type`(5-4), `seamless_play`(3), /// MSB-first: `block_mode`(7-6), `block_type`(5-4), `seamless_play`(3),
/// `interleaved`(2), `stc_discontinuity`(1), `seamless_angle`(0). (The real /// `interleaved`(2), `stc_discontinuity`(1), `seamless_angle`(0). (The real
/// `cell_type` is a karaoke-only field in byte 1, not used here.) /// `cell_type` is a karaoke-only field in byte 1, not used here.)
@@ -93,7 +93,7 @@ pub struct CellCategory {
} }
impl CellCategory { impl CellCategory {
/// Decode the raw `cell_playback + 0` byte (libdvdread `read_cell_playback`). /// Decode the raw `cell_playback + 0` byte (DVD-Video IFO cell playback).
pub fn decode(raw: u8) -> Self { pub fn decode(raw: u8) -> Self {
CellCategory { CellCategory {
block_mode: (raw >> 6) & 0x03, block_mode: (raw >> 6) & 0x03,
@@ -423,7 +423,7 @@ fn parse_vts(
} }
// VTSI_MAT (VTS_xx_0.IFO header) field offsets — fixed by the DVD-Video // VTSI_MAT (VTS_xx_0.IFO header) field offsets — fixed by the DVD-Video
// spec (libdvdread `vtsi_mat_t`). The offsets are constant; the sector // spec (the VTSI management table). The offsets are constant; the sector
// values they point to are per-disc. // values they point to are per-disc.
const VTSTT_VOBS_OFFSET: usize = 0xC4; // VTS title VOBS start sector (feature) const VTSTT_VOBS_OFFSET: usize = 0xC4; // VTS title VOBS start sector (feature)
const VTS_PGCIT_OFFSET: usize = 0xCC; // VTS_PGCIT sector pointer const VTS_PGCIT_OFFSET: usize = 0xCC; // VTS_PGCIT sector pointer
@@ -580,7 +580,7 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
_ => Codec::Unknown(coding_mode), _ => Codec::Unknown(coding_mode),
}; };
let sample_rate_flag = (b1 >> 4) & 0x03; // sample_frequency: byte 1 bits 5-4 (libdvdread audio_attr_t) let sample_rate_flag = (b1 >> 4) & 0x03; // sample_frequency: byte 1 bits 5-4 (DVD-Video audio attributes)
let sample_rate = match sample_rate_flag { let sample_rate = match sample_rate_flag {
0 => 48000, 0 => 48000,
1 => 96000, 1 => 96000,
@@ -1071,7 +1071,7 @@ mod tests {
/// reads with — a co-edit of constant + helper would silently re-introduce /// reads with — a co-edit of constant + helper would silently re-introduce
/// the PAL-as-NTSC bug and every test would still pass. This test feeds /// the PAL-as-NTSC bug and every test would still pass. This test feeds
/// `parse_video_attr` HARDCODED bytes captured from real DVD-Video layouts /// `parse_video_attr` HARDCODED bytes captured from real DVD-Video layouts
/// (DVD spec / libdvdread `video_attr_t`: mpeg_version[7-6] video_format[5-4] /// (DVD-Video video attributes: mpeg_version[7-6] video_format[5-4]
/// display_aspect[3-2] permitted_df[1-0]) — no `v_atr_byte`. If the parser's /// display_aspect[3-2] permitted_df[1-0]) — no `v_atr_byte`. If the parser's
/// bit positions drift, these fail. /// bit positions drift, these fail.
#[test] #[test]
@@ -1238,7 +1238,7 @@ mod tests {
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
// Added hardening tests. Grounded in the DVD-Video IFO spec // Added hardening tests. Grounded in the DVD-Video IFO spec
// (dvd_udf / libdvdread ifo_types.h; http://dvd.sourceforge.net). // (DVD-Video IFO format; http://dvd.sourceforge.net).
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────
/// BCD frame-rate flag: bits 7-6 of byte[3]. 0b01 = 25fps (PAL), /// BCD frame-rate flag: bits 7-6 of byte[3]. 0b01 = 25fps (PAL),
@@ -1593,7 +1593,7 @@ mod tests {
} }
} }
/// CellCategory decodes the libdvdread byte-0 bitfields: block_mode (7-6), /// CellCategory decodes the DVD-Video cell-category byte-0 bitfields: block_mode (7-6),
/// block_type (5-4), seamless_play (3), interleaved (2), /// block_type (5-4), seamless_play (3), interleaved (2),
/// stc_discontinuity (1), seamless_angle (0). /// stc_discontinuity (1), seamless_angle (0).
#[test] #[test]
+1 -1
View File
@@ -346,7 +346,7 @@ pub fn key_fetch(
/// ///
/// "Encrypted" is decided by [`crate::aacs::content::aacs_unit_encrypted`] — the /// "Encrypted" is decided by [`crate::aacs::content::aacs_unit_encrypted`] — the
/// AACS Copy Permission Indicator (CPI) in the top 2 bits of byte 0, the /// AACS Copy Permission Indicator (CPI) in the top 2 bits of byte 0, the
/// spec-correct signal (libaacs' `buf[0] & 0xc0`). NOT the `ts_sync_destroyed` /// spec-correct signal (`buf[0] & 0xc0`). NOT the `ts_sync_destroyed`
/// sync heuristic: destroyed TS syncs do not imply encryption (an FMTS variant /// sync heuristic: destroyed TS syncs do not imply encryption (an FMTS variant
/// frame or an odd clear unit can lack syncs yet be unencrypted), and a clear /// frame or an odd clear unit can lack syncs yet be unencrypted), and a clear
/// unit sent to a key server yields nothing to validate against — the "0 /// unit sent to a key server yields nothing to validate against — the "0