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:
+26
-5
@@ -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
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[](LICENSE)
|
[](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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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!(
|
||||||
§or[0x7F8..0x800],
|
§or[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!(§or[0x54..0x59], &seed, "sector seed must survive");
|
assert_eq!(§or[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!(
|
||||||
§or[2040..2048],
|
§or[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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user