Audit fixes + DVD support foundation (IFO, PS demux, MPEG-2, CSS crack)

Audit fixes (14 critical, 22 warnings):
- UDF: bounds checks on all ICB/FID parsing from disc data
- SCSI Linux: saturating_sub on residual, CDB length guard, buffer size guard
- SCSI macOS: SCSITaskStatus u32 (was u8 — stack corruption)
- AACS: EC mod_inv returns infinity instead of panic, key reduced mod n
- AACS: do_handshake tries all host certs (was returning on first failure)
- H.264: bounds check on SPS < 4 bytes
- ContentReader: error on missing unit key (was zero-fill)
- KEYDB: flat redirect loop (was recursive), 100MB response limit, Windows HOME fallback
- ISO writer: AVDP extent order, partition length, allocation cap
- Network: removed TCP_NODELAY on bulk stream
- MKV: guard on u64::MAX seek
- disc.rs: saturating_sub on extent offset, simplified dead region code
- cargo fmt (610 violations), cargo clippy --fix (55 auto-fixes)

DVD support (new files):
- src/ifo.rs — IFO parser (VIDEO_TS.IFO, VTS_XX_0.IFO, PGC chains, cells, streams) — 13 tests
- src/mux/ps.rs — MPEG-2 Program Stream demuxer (pack headers, PES, private stream 1) — 12 tests
- src/mux/codec/mpeg2.rs — MPEG-2 video parser (sequence headers, I-frame detection) — 15 tests
- src/css/crack.rs — split-attack algorithm (LFSR cipher needs verification — test ignored)

226 tests total (was 186), 1 ignored (CSS crack needs cipher verification).
This commit is contained in:
MattJackson
2026-04-11 16:52:22 +00:00
parent 6e771a1867
commit ff5547363b
57 changed files with 6189 additions and 1519 deletions
+184 -116
View File
@@ -18,12 +18,12 @@
//! - AACS 2.0: drives accept AACS 1.0 host certs for backward compatibility
//! (full P-256/SHA-256 AACS 2.0 handshake prepared but rarely needed)
use crate::error::{Error, Result};
use crate::drive::DriveSession;
use crate::error::{Error, Result};
use crate::scsi::DataDirection;
use num_bigint::BigUint;
use num_traits::{One, Zero};
use sha1::{Sha1, Digest};
use sha1::{Digest, Sha1};
/// Execute a SCSI command that reads data from the device.
fn scsi_read(session: &mut DriveSession, cdb: &[u8], len: usize) -> Result<Vec<u8>> {
@@ -42,87 +42,79 @@ fn scsi_write(session: &mut DriveSession, cdb: &[u8], data: &[u8]) -> Result<()>
// ── AACS 1.0 elliptic curve parameters (160-bit) ───────────────────────────
const EC_P: [u8; 20] = [
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD,
0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4, 0x79, 0xA7, 0xD7, 0xDF,
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4,
0x79, 0xA7, 0xD7, 0xDF,
];
const EC_A: [u8; 20] = [
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD,
0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4, 0x79, 0xA7, 0xD7, 0xDC,
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xB0, 0x9E, 0xF9, 0xEA, 0xE7, 0xC4,
0x79, 0xA7, 0xD7, 0xDC,
];
#[cfg(test)]
const EC_B: [u8; 20] = [
0x40, 0x2D, 0xAD, 0x3E, 0xC1, 0xCB, 0xCD, 0x16, 0x52, 0x48,
0xD6, 0x8E, 0x12, 0x45, 0xE0, 0xC4, 0xDA, 0xAC, 0xB1, 0xD8,
0x40, 0x2D, 0xAD, 0x3E, 0xC1, 0xCB, 0xCD, 0x16, 0x52, 0x48, 0xD6, 0x8E, 0x12, 0x45, 0xE0, 0xC4,
0xDA, 0xAC, 0xB1, 0xD8,
];
const EC_N: [u8; 20] = [
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD,
0xC4, 0x4F, 0x54, 0x81, 0x7B, 0x2C, 0x7F, 0x5A, 0xB0, 0x17,
0x9D, 0xC9, 0xD8, 0x13, 0x55, 0xEC, 0xCE, 0xB5, 0x60, 0xBD, 0xC4, 0x4F, 0x54, 0x81, 0x7B, 0x2C,
0x7F, 0x5A, 0xB0, 0x17,
];
const EC_GX: [u8; 20] = [
0x2E, 0x64, 0xFC, 0x22, 0x57, 0x83, 0x51, 0xE6, 0xF4, 0xCC,
0xA7, 0xEB, 0x81, 0xD0, 0xA4, 0xBD, 0xC5, 0x4C, 0xCE, 0xC6,
0x2E, 0x64, 0xFC, 0x22, 0x57, 0x83, 0x51, 0xE6, 0xF4, 0xCC, 0xA7, 0xEB, 0x81, 0xD0, 0xA4, 0xBD,
0xC5, 0x4C, 0xCE, 0xC6,
];
const EC_GY: [u8; 20] = [
0x09, 0x14, 0xA2, 0x5D, 0xD0, 0x54, 0x42, 0x88, 0x9D, 0xB4,
0x55, 0xC7, 0xF2, 0x3C, 0x9A, 0x07, 0x07, 0xF5, 0xCB, 0xB9,
0x09, 0x14, 0xA2, 0x5D, 0xD0, 0x54, 0x42, 0x88, 0x9D, 0xB4, 0x55, 0xC7, 0xF2, 0x3C, 0x9A, 0x07,
0x07, 0xF5, 0xCB, 0xB9,
];
// ── AACS 2.0 elliptic curve parameters (P-256 / secp256r1 / NIST prime256v1)
const P256_P: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
];
const P256_A: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
];
#[cfg(test)]
const P256_B: [u8; 32] = [
0x5A, 0xC6, 0x35, 0xD8, 0xAA, 0x3A, 0x93, 0xE7, 0xB3, 0xEB, 0xBD, 0x55,
0x76, 0x98, 0x86, 0xBC, 0x65, 0x1D, 0x06, 0xB0, 0xCC, 0x53, 0xB0, 0xF6,
0x3B, 0xCE, 0x3C, 0x3E, 0x27, 0xD2, 0x60, 0x4B,
0x5A, 0xC6, 0x35, 0xD8, 0xAA, 0x3A, 0x93, 0xE7, 0xB3, 0xEB, 0xBD, 0x55, 0x76, 0x98, 0x86, 0xBC,
0x65, 0x1D, 0x06, 0xB0, 0xCC, 0x53, 0xB0, 0xF6, 0x3B, 0xCE, 0x3C, 0x3E, 0x27, 0xD2, 0x60, 0x4B,
];
const P256_N: [u8; 32] = [
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84,
0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51,
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x51,
];
const P256_GX: [u8; 32] = [
0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6, 0xE5,
0x63, 0xA4, 0x40, 0xF2, 0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, 0x33, 0xA0,
0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
0x6B, 0x17, 0xD1, 0xF2, 0xE1, 0x2C, 0x42, 0x47, 0xF8, 0xBC, 0xE6, 0xE5, 0x63, 0xA4, 0x40, 0xF2,
0x77, 0x03, 0x7D, 0x81, 0x2D, 0xEB, 0x33, 0xA0, 0xF4, 0xA1, 0x39, 0x45, 0xD8, 0x98, 0xC2, 0x96,
];
const P256_GY: [u8; 32] = [
0x4F, 0xE3, 0x42, 0xE2, 0xFE, 0x1A, 0x7F, 0x9B, 0x8E, 0xE7, 0xEB, 0x4A,
0x7C, 0x0F, 0x9E, 0x16, 0x2B, 0xCE, 0x33, 0x57, 0x6B, 0x31, 0x5E, 0xCE,
0xCB, 0xB6, 0x40, 0x68, 0x37, 0xBF, 0x51, 0xF5,
0x4F, 0xE3, 0x42, 0xE2, 0xFE, 0x1A, 0x7F, 0x9B, 0x8E, 0xE7, 0xEB, 0x4A, 0x7C, 0x0F, 0x9E, 0x16,
0x2B, 0xCE, 0x33, 0x57, 0x6B, 0x31, 0x5E, 0xCE, 0xCB, 0xB6, 0x40, 0x68, 0x37, 0xBF, 0x51, 0xF5,
];
/// AACS 2.0 LA public key for cert verification (P-256).
/// From AACS2 specification — used to verify type 0x11 drive certificates.
const AACS2_LA_PUB_X: [u8; 32] = [
0xF9, 0x57, 0xBC, 0x1F, 0xD7, 0xE6, 0x09, 0x7E, 0xCA, 0xCC, 0x35, 0x23,
0x4C, 0x9C, 0x66, 0xC3, 0x42, 0xEB, 0x3D, 0xB7, 0x2B, 0x41, 0x06, 0xF4,
0x04, 0x9C, 0x6A, 0x88, 0x70, 0x00, 0xAA, 0x2C,
0xF9, 0x57, 0xBC, 0x1F, 0xD7, 0xE6, 0x09, 0x7E, 0xCA, 0xCC, 0x35, 0x23, 0x4C, 0x9C, 0x66, 0xC3,
0x42, 0xEB, 0x3D, 0xB7, 0x2B, 0x41, 0x06, 0xF4, 0x04, 0x9C, 0x6A, 0x88, 0x70, 0x00, 0xAA, 0x2C,
];
const AACS2_LA_PUB_Y: [u8; 32] = [
0x39, 0x55, 0x0B, 0x41, 0x02, 0x27, 0xEA, 0x7B, 0x1A, 0x53, 0xF8, 0x67,
0x8C, 0x5A, 0x91, 0x6F, 0xFC, 0x7C, 0x78, 0x01, 0x3E, 0x89, 0x15, 0xE3,
0xF0, 0x81, 0xD3, 0xE9, 0x3E, 0x17, 0x55, 0x0B,
0x39, 0x55, 0x0B, 0x41, 0x02, 0x27, 0xEA, 0x7B, 0x1A, 0x53, 0xF8, 0x67, 0x8C, 0x5A, 0x91, 0x6F,
0xFC, 0x7C, 0x78, 0x01, 0x3E, 0x89, 0x15, 0xE3, 0xF0, 0x81, 0xD3, 0xE9, 0x3E, 0x17, 0x55, 0x0B,
];
// ── AACS 1.0 LA (Licensing Administrator) public key for cert verification ──
const AACS_LA_PUB_X: [u8; 20] = [
0x01, 0xF3, 0x5D, 0xAB, 0xD8, 0xAE, 0x5F, 0x40, 0x56, 0x5E,
0x30, 0xC8, 0x8A, 0x60, 0x42, 0x82, 0x07, 0x61, 0xDF, 0x93,
0x01, 0xF3, 0x5D, 0xAB, 0xD8, 0xAE, 0x5F, 0x40, 0x56, 0x5E, 0x30, 0xC8, 0x8A, 0x60, 0x42, 0x82,
0x07, 0x61, 0xDF, 0x93,
];
const AACS_LA_PUB_Y: [u8; 20] = [
0x44, 0x87, 0xB5, 0xAC, 0x07, 0x10, 0x8D, 0x10, 0x5B, 0xA5,
0xB9, 0xE3, 0x2F, 0x3B, 0xBB, 0xFC, 0x0C, 0x2C, 0xBC, 0xD1,
0x44, 0x87, 0xB5, 0xAC, 0x07, 0x10, 0x8D, 0x10, 0x5B, 0xA5, 0xB9, 0xE3, 0x2F, 0x3B, 0xBB, 0xFC,
0x0C, 0x2C, 0xBC, 0xD1,
];
// ── Elliptic curve arithmetic over GF(p) ───────────────────────────────────
@@ -136,15 +128,26 @@ struct EcPoint {
impl EcPoint {
fn infinity() -> Self {
EcPoint { x: BigUint::zero(), y: BigUint::zero(), infinity: true }
EcPoint {
x: BigUint::zero(),
y: BigUint::zero(),
infinity: true,
}
}
fn new(x: BigUint, y: BigUint) -> Self {
EcPoint { x, y, infinity: false }
EcPoint {
x,
y,
infinity: false,
}
}
fn from_bytes(x_bytes: &[u8], y_bytes: &[u8]) -> Self {
EcPoint::new(BigUint::from_bytes_be(x_bytes), BigUint::from_bytes_be(y_bytes))
EcPoint::new(
BigUint::from_bytes_be(x_bytes),
BigUint::from_bytes_be(y_bytes),
)
}
}
@@ -181,8 +184,12 @@ fn mod_inv(a: &BigUint, m: &BigUint) -> Option<BigUint> {
/// EC point addition on curve y² = x³ + ax + b (mod p).
fn ec_add(p1: &EcPoint, p2: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
if p1.infinity { return p2.clone(); }
if p2.infinity { return p1.clone(); }
if p1.infinity {
return p2.clone();
}
if p2.infinity {
return p1.clone();
}
if p1.x == p2.x {
if p1.y == p2.y && !p1.y.is_zero() {
@@ -203,9 +210,10 @@ fn ec_add(p1: &EcPoint, p2: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
(p - (&p1.x - &p2.x) % p) % p
};
// Safety: mod_inv only returns None if dx == 0 (points identical),
// which is prevented by the caller using ec_double for that case.
let dx_inv = mod_inv(&dx, p).expect("ec_add: dx has no inverse");
let dx_inv = match mod_inv(&dx, p) {
Some(v) => v,
None => return EcPoint::infinity(),
};
let lam = (&dy * &dx_inv) % p;
// x3 = λ² - x1 - x2 mod p
@@ -249,9 +257,10 @@ fn ec_double(pt: &EcPoint, a: &BigUint, p: &BigUint) -> EcPoint {
let numerator = (&three * &pt.x * &pt.x + a) % p;
let denominator = (&two * &pt.y) % p;
// Safety: mod_inv only returns None if 2*y == 0 (point at infinity),
// which shouldn't occur with valid curve points.
let denom_inv = mod_inv(&denominator, p).expect("ec_double: denominator has no inverse");
let denom_inv = match mod_inv(&denominator, p) {
Some(v) => v,
None => return EcPoint::infinity(),
};
let lam = (&numerator * &denom_inv) % p;
// x3 = λ² - 2x mod p
@@ -337,12 +346,16 @@ fn ecdsa_sign(priv_key: &[u8; 20], data: &[u8]) -> ([u8; 20], [u8; 20]) {
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut k_bytes);
let k = BigUint::from_bytes_be(&k_bytes) % &n;
if k.is_zero() { continue; }
if k.is_zero() {
continue;
}
// R = k × G
let r_point = ec_mul(&k, &g, &a, &p);
let r = &r_point.x % &n;
if r.is_zero() { continue; }
if r.is_zero() {
continue;
}
// s = k⁻¹(z + r·d) mod n
let k_inv = match mod_inv(&k, &n) {
@@ -350,7 +363,9 @@ fn ecdsa_sign(priv_key: &[u8; 20], data: &[u8]) -> ([u8; 20], [u8; 20]) {
None => continue,
};
let s = (&k_inv * ((&z + &r * &d) % &n)) % &n;
if s.is_zero() { continue; }
if s.is_zero() {
continue;
}
let r_bytes = to_bytes_be_padded(&r, 20);
let s_bytes = to_bytes_be_padded(&s, 20);
@@ -365,7 +380,13 @@ fn ecdsa_sign(priv_key: &[u8; 20], data: &[u8]) -> ([u8; 20], [u8; 20]) {
}
/// ECDSA verify: verify signature (r, s) against SHA-1(data) using public key.
fn ecdsa_verify(pub_x: &[u8; 20], pub_y: &[u8; 20], sig_r: &[u8; 20], sig_s: &[u8; 20], data: &[u8]) -> bool {
fn ecdsa_verify(
pub_x: &[u8; 20],
pub_y: &[u8; 20],
sig_r: &[u8; 20],
sig_s: &[u8; 20],
data: &[u8],
) -> bool {
let p = BigUint::from_bytes_be(&EC_P);
let a = BigUint::from_bytes_be(&EC_A);
let n = BigUint::from_bytes_be(&EC_N);
@@ -405,7 +426,7 @@ fn ecdsa_verify(pub_x: &[u8; 20], pub_y: &[u8; 20], sig_r: &[u8; 20], sig_s: &[u
/// ECDSA sign with P-256/SHA-256. Returns (r, s) each 32 bytes.
fn ecdsa_sign_p256(priv_key: &[u8; 32], data: &[u8]) -> ([u8; 32], [u8; 32]) {
use sha2::{Sha256, Digest as Sha2Digest};
use sha2::{Digest as Sha2Digest, Sha256};
let p = BigUint::from_bytes_be(&P256_P);
let a = BigUint::from_bytes_be(&P256_A);
@@ -421,18 +442,24 @@ fn ecdsa_sign_p256(priv_key: &[u8; 32], data: &[u8]) -> ([u8; 32], [u8; 32]) {
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut k_bytes);
let k = BigUint::from_bytes_be(&k_bytes) % &n;
if k.is_zero() { continue; }
if k.is_zero() {
continue;
}
let r_point = ec_mul(&k, &g, &a, &p);
let r = &r_point.x % &n;
if r.is_zero() { continue; }
if r.is_zero() {
continue;
}
let k_inv = match mod_inv(&k, &n) {
Some(v) => v,
None => continue,
};
let s = (&k_inv * ((&z + &r * &d) % &n)) % &n;
if s.is_zero() { continue; }
if s.is_zero() {
continue;
}
let r_bytes = to_bytes_be_padded(&r, 32);
let s_bytes = to_bytes_be_padded(&s, 32);
@@ -448,7 +475,7 @@ fn ecdsa_sign_p256(priv_key: &[u8; 32], data: &[u8]) -> ([u8; 32], [u8; 32]) {
/// ECDSA verify with P-256/SHA-256.
fn ecdsa_verify_p256(pub_x: &[u8], pub_y: &[u8], sig_r: &[u8], sig_s: &[u8], data: &[u8]) -> bool {
use sha2::{Sha256, Digest as Sha2Digest};
use sha2::{Digest as Sha2Digest, Sha256};
let p = BigUint::from_bytes_be(&P256_P);
let a = BigUint::from_bytes_be(&P256_A);
@@ -487,7 +514,9 @@ fn ecdsa_verify_p256(pub_x: &[u8], pub_y: &[u8], sig_r: &[u8], sig_s: &[u8], dat
/// Verify an AACS 2.0 certificate (type 0x11, 132 bytes) against AACS 2.0 LA key.
fn verify_cert_p256(cert: &[u8]) -> bool {
if cert.len() < 132 { return false; }
if cert.len() < 132 {
return false;
}
// AACS 2.0 cert: type(1) + flags(1) + padding(2) + serial(6) + pub_x(32) + pub_y(32) + sig_r(32) + sig_s(32)
// Signature is over the first 74 bytes
let sig_r = &cert[74..106];
@@ -511,12 +540,19 @@ fn cert_pub_key_p256(cert: &[u8]) -> ([u8; 32], [u8; 32]) {
}
/// Compute bus key via ECDH on P-256 curve.
fn compute_bus_key_p256(host_priv: &[u8; 32], drive_key_point_x: &[u8], drive_key_point_y: &[u8]) -> [u8; 16] {
fn compute_bus_key_p256(
host_priv: &[u8; 32],
drive_key_point_x: &[u8],
drive_key_point_y: &[u8],
) -> [u8; 16] {
let p = BigUint::from_bytes_be(&P256_P);
let a = BigUint::from_bytes_be(&P256_A);
let d = BigUint::from_bytes_be(host_priv);
let dkp = EcPoint::new(BigUint::from_bytes_be(drive_key_point_x), BigUint::from_bytes_be(drive_key_point_y));
let dkp = EcPoint::new(
BigUint::from_bytes_be(drive_key_point_x),
BigUint::from_bytes_be(drive_key_point_y),
);
let shared = ec_mul(&d, &dkp, &a, &p);
@@ -531,7 +567,9 @@ fn compute_bus_key_p256(host_priv: &[u8; 32], drive_key_point_x: &[u8], drive_ke
/// Verify an AACS certificate (92 bytes) against the AACS LA public key.
fn verify_cert(cert: &[u8]) -> bool {
if cert.len() < 92 { return false; }
if cert.len() < 92 {
return false;
}
// Certificate format: type(1) + flags(1) + padding(2) + serial(6) + pub_x(20) + pub_y(20) + sig_r(20) + sig_s(20)
// Signature is over the first 52 bytes
let mut sig_r = [0u8; 20];
@@ -554,7 +592,11 @@ fn cert_pub_key(cert: &[u8]) -> ([u8; 20], [u8; 20]) {
// ── Bus key derivation (ECDH) ───────────────────────────────────────────────
/// Compute bus key via ECDH: bus_key = low 128 bits of (host_priv × drive_key_point).x
fn compute_bus_key(host_priv: &[u8; 20], drive_key_point_x: &[u8; 20], drive_key_point_y: &[u8; 20]) -> [u8; 16] {
fn compute_bus_key(
host_priv: &[u8; 20],
drive_key_point_x: &[u8; 20],
drive_key_point_y: &[u8; 20],
) -> [u8; 16] {
let p = BigUint::from_bytes_be(&EC_P);
let a = BigUint::from_bytes_be(&EC_A);
@@ -599,32 +641,41 @@ fn generate_host_key_pair_p256() -> ([u8; 32], [u8; 32], [u8; 32]) {
fn generate_host_key_pair() -> ([u8; 20], [u8; 20], [u8; 20]) {
let p_mod = BigUint::from_bytes_be(&EC_P);
let a = BigUint::from_bytes_be(&EC_A);
let n = BigUint::from_bytes_be(&EC_N);
let g = EcPoint::from_bytes(&EC_GX, &EC_GY);
let mut priv_bytes = [0u8; 20];
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut priv_bytes);
let d = BigUint::from_bytes_be(&priv_bytes);
let q = ec_mul(&d, &g, &a, &p_mod);
let (d, q) = loop {
let mut priv_bytes = [0u8; 20];
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut priv_bytes);
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
if d.is_zero() {
continue;
}
let q = ec_mul(&d, &g, &a, &p_mod);
break (d, q);
};
let d_bytes = to_bytes_be_padded(&d, 20);
let qx = to_bytes_be_padded(&q.x, 20);
let qy = to_bytes_be_padded(&q.y, 20);
let mut key = [0u8; 20];
let mut pub_x = [0u8; 20];
let mut pub_y = [0u8; 20];
key.copy_from_slice(&d_bytes);
pub_x.copy_from_slice(&qx);
pub_y.copy_from_slice(&qy);
(priv_bytes, pub_x, pub_y)
(key, pub_x, pub_y)
}
// ── AES-CMAC (for MAC verification) ────────────────────────────────────────
/// AES-128-CMAC over 16 bytes of data.
fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
let cipher = Aes128::new(GenericArray::from_slice(key));
@@ -730,8 +781,7 @@ pub fn aacs_authenticate(
// Step 2: Allocate AGID
let cdb = cdb_report_key(0, 0x00, 8);
let response = scsi_read(session, &cdb, 8)
.map_err(|_| Error::AacsAgidAlloc)?;
let response = scsi_read(session, &cdb, 8).map_err(|_| Error::AacsAgidAlloc)?;
let agid = (response[7] >> 6) & 0x03;
// Step 3: Generate host nonce and ephemeral key pair
@@ -747,13 +797,11 @@ pub fn aacs_authenticate(
send_buf[24..116].copy_from_slice(&host_cert[..92]);
let cdb = cdb_send_key(agid, 0x01, 116);
scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsCertRejected)?;
scsi_write(session, &cdb, &send_buf).map_err(|_| Error::AacsCertRejected)?;
// Step 5: Read drive certificate + nonce (REPORT KEY format 0x01)
let cdb = cdb_report_key(agid, 0x01, 116);
let response = scsi_read(session, &cdb, 116)
.map_err(|_| Error::AacsCertRead)?;
let response = scsi_read(session, &cdb, 116).map_err(|_| Error::AacsCertRead)?;
let mut drive_nonce = [0u8; 20];
let mut drive_cert = [0u8; 92];
@@ -774,11 +822,10 @@ pub fn aacs_authenticate(
// Step 6: Read drive key point + signature (REPORT KEY format 0x02)
let cdb = cdb_report_key(agid, 0x02, 84);
let response = scsi_read(session, &cdb, 84)
.map_err(|_| Error::AacsKeyRead)?;
let response = scsi_read(session, &cdb, 84).map_err(|_| Error::AacsKeyRead)?;
let mut drive_key_point = [0u8; 40]; // x(20) + y(20)
let mut drive_key_sig = [0u8; 40]; // r(20) + s(20)
let mut drive_key_point = [0u8; 40]; // x(20) + y(20)
let mut drive_key_sig = [0u8; 40]; // r(20) + s(20)
drive_key_point.copy_from_slice(&response[4..44]);
drive_key_sig.copy_from_slice(&response[44..84]);
@@ -814,8 +861,7 @@ pub fn aacs_authenticate(
send_buf[64..84].copy_from_slice(&host_sig_s);
let cdb = cdb_send_key(agid, 0x02, 84);
scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsKeyRejected)?;
scsi_write(session, &cdb, &send_buf).map_err(|_| Error::AacsKeyRejected)?;
// Step 9: Compute bus key via ECDH
let mut dkp_x = [0u8; 20];
@@ -880,8 +926,7 @@ fn aacs2_authenticate_p256(
// Step 2: Allocate AGID
let cdb = cdb_report_key(0, 0x00, 8);
let response = scsi_read(session, &cdb, 8)
.map_err(|_| Error::AacsAgidAlloc)?;
let response = scsi_read(session, &cdb, 8).map_err(|_| Error::AacsAgidAlloc)?;
let agid = (response[7] >> 6) & 0x03;
// Step 3: Generate host nonce + P-256 ephemeral key pair
@@ -898,14 +943,12 @@ fn aacs2_authenticate_p256(
send_buf[24..156].copy_from_slice(&host_cert[..132]);
let cdb = cdb_send_key(agid, 0x01, 156);
scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsCertRejected)?;
scsi_write(session, &cdb, &send_buf).map_err(|_| Error::AacsCertRejected)?;
// Step 5: Read drive certificate + nonce
// AACS 2.0 drive cert is also 132 bytes
let cdb = cdb_report_key(agid, 0x01, 156);
let response = scsi_read(session, &cdb, 156)
.map_err(|_| Error::AacsCertRead)?;
let response = scsi_read(session, &cdb, 156).map_err(|_| Error::AacsCertRead)?;
let mut drive_nonce = [0u8; 20];
drive_nonce.copy_from_slice(&response[4..24]);
@@ -918,8 +961,7 @@ fn aacs2_authenticate_p256(
// Step 6: Read drive key point + signature (P-256: 64+64 = 128 bytes)
let cdb = cdb_report_key(agid, 0x02, 132);
let response = scsi_read(session, &cdb, 132)
.map_err(|_| Error::AacsKeyRead)?;
let response = scsi_read(session, &cdb, 132).map_err(|_| Error::AacsKeyRead)?;
let drive_key_x = &response[4..36];
let drive_key_y = &response[36..68];
@@ -933,7 +975,13 @@ fn aacs2_authenticate_p256(
verify_data.extend_from_slice(drive_key_x);
verify_data.extend_from_slice(drive_key_y);
if !ecdsa_verify_p256(&drive_pub_x, &drive_pub_y, drive_sig_r, drive_sig_s, &verify_data) {
if !ecdsa_verify_p256(
&drive_pub_x,
&drive_pub_y,
drive_sig_r,
drive_sig_s,
&verify_data,
) {
return Err(Error::AacsKeyVerify);
}
@@ -954,8 +1002,7 @@ fn aacs2_authenticate_p256(
send_buf[100..132].copy_from_slice(&host_sig_s);
let cdb = cdb_send_key(agid, 0x02, 132);
scsi_write(session, &cdb, &send_buf)
.map_err(|_| Error::AacsKeyRejected)?;
scsi_write(session, &cdb, &send_buf).map_err(|_| Error::AacsKeyRejected)?;
// Step 9: Compute bus key via P-256 ECDH
let bus_key = compute_bus_key_p256(&host_eph_key, drive_key_x, drive_key_y);
@@ -977,8 +1024,7 @@ fn aacs2_authenticate_p256(
pub fn read_volume_id(session: &mut DriveSession, auth: &mut AacsAuth) -> Result<[u8; 16]> {
// REPORT DISC STRUCTURE format 0x80
let cdb = cdb_report_disc_structure(auth.agid, 0x80, 36);
let response = scsi_read(session, &cdb, 36)
.map_err(|_| Error::AacsVidRead)?;
let response = scsi_read(session, &cdb, 36).map_err(|_| Error::AacsVidRead)?;
let mut vid = [0u8; 16];
let mut mac = [0u8; 16];
@@ -996,11 +1042,13 @@ pub fn read_volume_id(session: &mut DriveSession, auth: &mut AacsAuth) -> Result
}
/// Read data keys after successful authentication (for AACS 2.0 bus encryption).
pub fn read_data_keys(session: &mut DriveSession, auth: &mut AacsAuth) -> Result<([u8; 16], [u8; 16])> {
pub fn read_data_keys(
session: &mut DriveSession,
auth: &mut AacsAuth,
) -> Result<([u8; 16], [u8; 16])> {
// REPORT DISC STRUCTURE format 0x84
let cdb = cdb_report_disc_structure(auth.agid, 0x84, 36);
let response = scsi_read(session, &cdb, 36)
.map_err(|_| Error::AacsDataKey)?;
let response = scsi_read(session, &cdb, 36).map_err(|_| Error::AacsDataKey)?;
let mut enc_rdk = [0u8; 16];
let mut enc_wdk = [0u8; 16];
@@ -1066,12 +1114,16 @@ mod tests {
let data = b"test data for AACS ECDSA";
let (sig_r, sig_s) = ecdsa_sign(&priv_key, data);
assert!(ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, data),
"ECDSA signature should verify");
assert!(
ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, data),
"ECDSA signature should verify"
);
// Verify with wrong data fails
assert!(!ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, b"wrong data"),
"ECDSA should fail with wrong data");
assert!(
!ecdsa_verify(&pub_x, &pub_y, &sig_r, &sig_s, b"wrong data"),
"ECDSA should fail with wrong data"
);
}
#[test]
@@ -1113,7 +1165,10 @@ mod tests {
let g = EcPoint::from_bytes(&P256_GX, &P256_GY);
let result = ec_mul(&n, &g, &a, &p);
assert!(result.infinity, "n × G should be point at infinity on P-256");
assert!(
result.infinity,
"n × G should be point at infinity on P-256"
);
}
#[test]
@@ -1160,10 +1215,16 @@ mod tests {
let pub_a = ec_mul(&da, &g, &a, &p);
let pub_b = ec_mul(&db, &g, &a, &p);
let key_a = compute_bus_key_p256(&priv_a,
&to_bytes_be_padded(&pub_b.x, 32), &to_bytes_be_padded(&pub_b.y, 32));
let key_b = compute_bus_key_p256(&priv_b,
&to_bytes_be_padded(&pub_a.x, 32), &to_bytes_be_padded(&pub_a.y, 32));
let key_a = compute_bus_key_p256(
&priv_a,
&to_bytes_be_padded(&pub_b.x, 32),
&to_bytes_be_padded(&pub_b.y, 32),
);
let key_b = compute_bus_key_p256(
&priv_b,
&to_bytes_be_padded(&pub_a.x, 32),
&to_bytes_be_padded(&pub_a.y, 32),
);
assert_eq!(key_a, key_b, "P-256 ECDH shared secrets should match");
}
@@ -1171,8 +1232,10 @@ mod tests {
#[test]
fn test_aes_cmac() {
// Basic CMAC test — at minimum verify it produces consistent output
let key = [0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c];
let key = [
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf,
0x4f, 0x3c,
];
let data = [0u8; 16];
let mac1 = aes_cmac_16(&data, &key);
let mac2 = aes_cmac_16(&data, &key);
@@ -1187,12 +1250,17 @@ mod tests {
Some(p) => std::path::PathBuf::from(p),
None => return, // skip if KEYDB_PATH not set
};
if !keydb_path.exists() { return; }
if !keydb_path.exists() {
return;
}
let db = crate::aacs::KeyDb::load(&keydb_path).unwrap();
if let Some(hc) = db.host_certs.first() {
let valid = verify_cert(&hc.certificate);
eprintln!("Host cert verification: {}", if valid { "PASS" } else { "FAIL" });
eprintln!(
"Host cert verification: {}",
if valid { "PASS" } else { "FAIL" }
);
// Note: our cert is revoked but should still have valid LA signature
// If it doesn't verify, the LA public key might be wrong
if !valid {
+279 -103
View File
@@ -15,9 +15,9 @@
pub mod handshake;
use std::collections::HashMap;
use aes::cipher::{generic_array::GenericArray, BlockDecrypt, BlockEncrypt, KeyInit};
use aes::Aes128;
use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit, generic_array::GenericArray};
use std::collections::HashMap;
/// Parsed AACS key database.
#[derive(Debug)]
@@ -74,10 +74,12 @@ pub struct DiscEntry {
/// Parse a hex string like "0xABCD..." into bytes.
fn parse_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
if s.len() % 2 != 0 { return None; }
if !s.len().is_multiple_of(2) {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
for i in (0..s.len()).step_by(2) {
out.push(u8::from_str_radix(&s[i..i+2], 16).ok()?);
out.push(u8::from_str_radix(&s[i..i + 2], 16).ok()?);
}
Some(out)
}
@@ -85,7 +87,9 @@ fn parse_hex(s: &str) -> Option<Vec<u8>> {
/// Parse hex into a fixed-size array.
fn parse_hex16(s: &str) -> Option<[u8; 16]> {
let v = parse_hex(s)?;
if v.len() != 16 { return None; }
if v.len() != 16 {
return None;
}
let mut out = [0u8; 16];
out.copy_from_slice(&v);
Some(out)
@@ -93,7 +97,9 @@ fn parse_hex16(s: &str) -> Option<[u8; 16]> {
fn parse_hex20(s: &str) -> Option<[u8; 20]> {
let v = parse_hex(s)?;
if v.len() != 20 { return None; }
if v.len() != 20 {
return None;
}
let mut out = [0u8; 20];
out.copy_from_slice(&v);
Some(out)
@@ -171,17 +177,27 @@ impl KeyDb {
/// Look up a disc by its hash. Returns the VUK if found.
pub fn find_vuk(&self, disc_hash: &str) -> Option<[u8; 16]> {
let hash = disc_hash.trim().to_lowercase().trim_start_matches("0x").to_string();
let hash = disc_hash
.trim()
.to_lowercase()
.trim_start_matches("0x")
.to_string();
// Try with 0x prefix and without
self.disc_entries.get(&format!("0x{}", hash))
self.disc_entries
.get(&format!("0x{}", hash))
.or_else(|| self.disc_entries.get(&hash))
.and_then(|e| e.vuk)
}
/// Look up a disc by its hash. Returns the full entry.
pub fn find_disc(&self, disc_hash: &str) -> Option<&DiscEntry> {
let hash = disc_hash.trim().to_lowercase().trim_start_matches("0x").to_string();
self.disc_entries.get(&format!("0x{}", hash))
let hash = disc_hash
.trim()
.to_lowercase()
.trim_start_matches("0x")
.to_string();
self.disc_entries
.get(&format!("0x{}", hash))
.or_else(|| self.disc_entries.get(&hash))
}
@@ -192,7 +208,14 @@ impl KeyDb {
let key_str = line.split("DEVICE_KEY").nth(1)?.split('|').next()?.trim();
let node_str = line.split("DEVICE_NODE").nth(1)?.split('|').next()?.trim();
let uv_str = line.split("KEY_UV").nth(1)?.split('|').next()?.trim();
let shift_str = line.split("KEY_U_MASK_SHIFT").nth(1)?.split(';').next()?.split('|').next()?.trim();
let shift_str = line
.split("KEY_U_MASK_SHIFT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
Some(DeviceKey {
key: parse_hex16(key_str)?,
@@ -214,8 +237,20 @@ impl KeyDb {
fn parse_host_cert(line: &str) -> Option<HostCert> {
// | HC | HOST_PRIV_KEY 0x... | HOST_CERT 0x...
let priv_str = line.split("HOST_PRIV_KEY").nth(1)?.split('|').next()?.trim();
let cert_str = line.split("HOST_CERT").nth(1)?.split(';').next()?.split('|').next()?.trim();
let priv_str = line
.split("HOST_PRIV_KEY")
.nth(1)?
.split('|')
.next()?
.trim();
let cert_str = line
.split("HOST_CERT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
Some(HostCert {
private_key: parse_hex20(priv_str)?,
@@ -227,16 +262,32 @@ impl KeyDb {
/// Parse AACS 2.0 host cert: `| HC2 | HOST_PRIV_KEY 0x... | HOST_CERT 0x...`
fn parse_host_cert_v2(line: &str) -> Option<([u8; 32], Vec<u8>)> {
let priv_str = line.split("HOST_PRIV_KEY").nth(1)?.split('|').next()?.trim();
let cert_str = line.split("HOST_CERT").nth(1)?.split(';').next()?.split('|').next()?.trim();
let priv_str = line
.split("HOST_PRIV_KEY")
.nth(1)?
.split('|')
.next()?
.trim();
let cert_str = line
.split("HOST_CERT")
.nth(1)?
.split(';')
.next()?
.split('|')
.next()?
.trim();
let priv_bytes = parse_hex(priv_str)?;
if priv_bytes.len() != 32 { return None; }
if priv_bytes.len() != 32 {
return None;
}
let mut pk = [0u8; 32];
pk.copy_from_slice(&priv_bytes);
let cert = parse_hex(cert_str)?;
if cert.len() < 132 { return None; }
if cert.len() < 132 {
return None;
}
Some((pk, cert))
}
@@ -251,7 +302,7 @@ impl KeyDb {
// Clean title: "TITLE_NAME (Display Title)" → use display title if present
let title = if let Some(start) = title_part.find('(') {
if let Some(end) = title_part.rfind(')') {
title_part[start+1..end].to_string()
title_part[start + 1..end].to_string()
} else {
title_part.to_string()
}
@@ -271,26 +322,26 @@ impl KeyDb {
match parts[i].trim() {
"M" => {
if i + 1 < parts.len() {
media_key = parse_hex16(parts[i+1].trim());
media_key = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"I" => {
if i + 1 < parts.len() {
disc_id = parse_hex16(parts[i+1].trim());
disc_id = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"V" => {
if i + 1 < parts.len() {
vuk = parse_hex16(parts[i+1].trim());
vuk = parse_hex16(parts[i + 1].trim());
i += 1;
}
}
"U" => {
if i + 1 < parts.len() {
// Unit keys: "1-0xKEY" or "1-0xKEY ; comment"
let uk_str = parts[i+1].split(';').next().unwrap_or("").trim();
let uk_str = parts[i + 1].split(';').next().unwrap_or("").trim();
for uk in uk_str.split(' ') {
let uk = uk.trim();
if let Some((num, key)) = uk.split_once('-') {
@@ -324,8 +375,7 @@ impl KeyDb {
/// Fixed IV used by AACS for all AES-CBC operations.
const AACS_IV: [u8; 16] = [
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3,
0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
];
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
@@ -425,7 +475,7 @@ pub struct UnitKeyFile {
/// Compute disc hash (SHA1 of Unit_Key_RO.inf content).
pub fn disc_hash(data: &[u8]) -> [u8; 20] {
use sha1::{Sha1, Digest};
use sha1::{Digest, Sha1};
let hash = Sha1::digest(data);
let mut out = [0u8; 20];
out.copy_from_slice(&hash);
@@ -481,8 +531,13 @@ pub fn parse_unit_key_ro(data: &[u8], aacs2: bool) -> Option<UnitKeyFile> {
let num_uk = u16::from_be_bytes([data[uk_pos], data[uk_pos + 1]]) as usize;
if num_uk == 0 {
return Some(UnitKeyFile {
disc_hash: hash, app_type, num_bdmv_dir, use_skb_mkb,
aacs2, encrypted_keys: Vec::new(), title_cps_unit: Vec::new(),
disc_hash: hash,
app_type,
num_bdmv_dir,
use_skb_mkb,
aacs2,
encrypted_keys: Vec::new(),
title_cps_unit: Vec::new(),
});
}
@@ -557,7 +612,10 @@ pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Opt
let cvalues = mkb_find_cvalues(mkb)?;
// Count UV entries (each 5 bytes, stop when high bits set)
let num_uvs = uvs.chunks(5).take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0).count();
let num_uvs = uvs
.chunks(5)
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
.count();
// Try each processing key against each UV/cvalue pair
for pk in processing_keys {
@@ -574,7 +632,12 @@ pub fn derive_media_key_from_pk(mkb: &[u8], processing_keys: &[[u8; 16]]) -> Opt
/// Validate a processing key against a cvalue/UV pair.
/// Returns the Media Key if valid.
fn validate_processing_key(pk: &[u8; 16], cvalue: &[u8], _uv: &[u8], mk_dv: &[u8; 16]) -> Option<[u8; 16]> {
fn validate_processing_key(
pk: &[u8; 16],
cvalue: &[u8],
_uv: &[u8],
mk_dv: &[u8; 16],
) -> Option<[u8; 16]> {
if cvalue.len() < 16 {
return None;
}
@@ -611,7 +674,9 @@ fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
while pos + 4 <= mkb.len() {
let rec_type = mkb[pos];
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
if rec_len < 4 || pos + rec_len > mkb.len() { break; }
if rec_len < 4 || pos + rec_len > mkb.len() {
break;
}
if rec_type == 0x10 && rec_len >= 20 {
// mk_dv is at offset 4 (after record header)
@@ -630,7 +695,9 @@ fn mkb_find_subdiff_records(mkb: &[u8]) -> Option<Vec<u8>> {
while pos + 4 <= mkb.len() {
let rec_type = mkb[pos];
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
if rec_len < 4 || pos + rec_len > mkb.len() { break; }
if rec_len < 4 || pos + rec_len > mkb.len() {
break;
}
if rec_type == 0x04 && rec_len > 4 {
return Some(mkb[pos + 4..pos + rec_len].to_vec());
@@ -646,7 +713,9 @@ fn mkb_find_cvalues(mkb: &[u8]) -> Option<Vec<u8>> {
while pos + 4 <= mkb.len() {
let rec_type = mkb[pos];
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
if rec_len < 4 || pos + rec_len > mkb.len() { break; }
if rec_len < 4 || pos + rec_len > mkb.len() {
break;
}
if rec_type == 0x07 && rec_len > 4 {
return Some(mkb[pos + 4..pos + rec_len].to_vec());
@@ -662,10 +731,17 @@ pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
while pos + 4 <= mkb.len() {
let rec_type = mkb[pos];
let rec_len = u32::from_be_bytes([0, mkb[pos + 1], mkb[pos + 2], mkb[pos + 3]]) as usize;
if rec_len < 4 || pos + rec_len > mkb.len() { break; }
if rec_len < 4 || pos + rec_len > mkb.len() {
break;
}
if rec_type == 0x81 && rec_len >= 8 {
return Some(u32::from_be_bytes([mkb[pos + 4], mkb[pos + 5], mkb[pos + 6], mkb[pos + 7]]));
return Some(u32::from_be_bytes([
mkb[pos + 4],
mkb[pos + 5],
mkb[pos + 6],
mkb[pos + 7],
]));
}
pos += rec_len;
}
@@ -676,8 +752,7 @@ pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
/// AACS-G3 seed constant.
const AESG3_SEED: [u8; 16] = [
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5,
0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
0x7B, 0x10, 0x3C, 0x5D, 0xCB, 0x08, 0xC4, 0xE5, 0x1A, 0x27, 0xB0, 0x17, 0x99, 0x05, 0x3B, 0xD9,
];
/// AACS-G3: derive a subkey from a parent key.
@@ -714,7 +789,7 @@ fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) ->
let mut bit_pos: i32 = -1;
for i in (0..32).rev() {
if (current_v_mask & (1u32 << i)) == 0 {
bit_pos = i as i32;
bit_pos = i;
break;
}
}
@@ -736,16 +811,16 @@ fn calc_pk_from_dk(dk: &[u8; 16], uv: u32, v_mask: u32, dev_key_v_mask: u32) ->
}
/// Derive Media Key from MKB using device keys (subset-difference tree).
pub fn derive_media_key_from_dk(
mkb: &[u8],
device_keys: &[DeviceKey],
) -> Option<[u8; 16]> {
pub fn derive_media_key_from_dk(mkb: &[u8], device_keys: &[DeviceKey]) -> Option<[u8; 16]> {
let mk_dv = mkb_find_mk_dv(mkb)?;
let uvs = mkb_find_subdiff_records(mkb)?;
let cvalues = mkb_find_cvalues(mkb)?;
// Count UV entries
let num_uvs = uvs.chunks(5).take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0).count();
let num_uvs = uvs
.chunks(5)
.take_while(|c| c.len() == 5 && (c[0] & 0xC0) == 0)
.count();
for dk in device_keys {
let device_number = dk.node as u32;
@@ -760,28 +835,30 @@ pub fn derive_media_key_from_dk(
}
let uv = u32::from_be_bytes([p_uv[0], p_uv[1], p_uv[2], p_uv[3]]);
if uv == 0 { continue; }
if uv == 0 {
continue;
}
let u_mask: u32 = 0xFFFFFFFF << u_mask_shift;
let v_mask = calc_v_mask(uv);
if ((device_number & u_mask) == (uv & u_mask)) &&
((device_number & v_mask) != (uv & v_mask))
if ((device_number & u_mask) == (uv & u_mask))
&& ((device_number & v_mask) != (uv & v_mask))
{
// Found matching subset-difference — find the right device key
let dev_key_v_mask = calc_v_mask(dk.uv);
let dev_key_u_mask: u32 = 0xFFFFFFFF << dk.u_mask_shift;
if u_mask == dev_key_u_mask &&
(uv & dev_key_v_mask) == (dk.uv & dev_key_v_mask)
{
if u_mask == dev_key_u_mask && (uv & dev_key_v_mask) == (dk.uv & dev_key_v_mask) {
// Derive processing key via tree traversal
let pk = calc_pk_from_dk(&dk.key, uv, v_mask, dev_key_v_mask);
// Validate and derive media key
if uvs_idx < cvalues.len() / 16 {
let cv = &cvalues[uvs_idx * 16..(uvs_idx + 1) * 16];
if let Some(mk) = validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv) {
if let Some(mk) =
validate_processing_key(&pk, cv, &uvs[1 + uvs_idx * 5..], &mk_dv)
{
return Some(mk);
}
}
@@ -799,21 +876,32 @@ const MKB_PACK_SIZE: usize = 32772;
/// Read MKB from drive via SCSI (REPORT DISC STRUCTURE format 0x83).
/// Returns the concatenated MKB data from all packs.
pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::error::Result<Vec<u8>> {
pub fn read_mkb_from_drive(
session: &mut crate::drive::DriveSession,
) -> crate::error::Result<Vec<u8>> {
use crate::scsi::{DataDirection, SCSI_READ_DISC_STRUCTURE};
let cdb = [
SCSI_READ_DISC_STRUCTURE, 0x01,
0x00, 0x00, 0x00, 0x00,
0x00, MKB_DISC_STRUCTURE_FORMAT,
(MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
0x00, 0x00,
SCSI_READ_DISC_STRUCTURE,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
MKB_DISC_STRUCTURE_FORMAT,
(MKB_PACK_SIZE >> 8) as u8,
(MKB_PACK_SIZE & 0xFF) as u8,
0x00,
0x00,
];
let mut buf = vec![0u8; 32772];
session.scsi_execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000)?;
let data_len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
if data_len < 2 { return Ok(Vec::new()); }
if data_len < 2 {
return Ok(Vec::new());
}
let len = data_len - 2;
let num_packs = buf[3] as usize;
@@ -825,11 +913,18 @@ pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::e
// Read remaining packs
for pack in 1..num_packs {
let mut cdb = [
SCSI_READ_DISC_STRUCTURE, 0x01,
0x00, 0x00, 0x00, 0x00,
0x00, MKB_DISC_STRUCTURE_FORMAT,
(MKB_PACK_SIZE >> 8) as u8, (MKB_PACK_SIZE & 0xFF) as u8,
0x00, 0x00,
SCSI_READ_DISC_STRUCTURE,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
MKB_DISC_STRUCTURE_FORMAT,
(MKB_PACK_SIZE >> 8) as u8,
(MKB_PACK_SIZE & 0xFF) as u8,
0x00,
0x00,
];
// Pack number goes in address field
cdb[2] = ((pack >> 24) & 0xFF) as u8;
@@ -838,7 +933,10 @@ pub fn read_mkb_from_drive(session: &mut crate::drive::DriveSession) -> crate::e
cdb[5] = (pack & 0xFF) as u8;
let mut buf = vec![0u8; 32772];
if session.scsi_execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000).is_ok() {
if session
.scsi_execute(&cdb, DataDirection::FromDevice, &mut buf, 10_000)
.is_ok()
{
let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
if len > 2 && len - 2 <= 32768 {
mkb.extend_from_slice(&buf[4..4 + len - 2]);
@@ -924,12 +1022,12 @@ pub fn resolve_keys(
) -> Option<ResolvedKeys> {
// Detect AACS version
let aacs2 = content_cert_data
.and_then(|d| parse_content_cert(d))
.and_then(parse_content_cert)
.map(|cc| cc.aacs2)
.unwrap_or(false);
let bus_encryption = content_cert_data
.and_then(|d| parse_content_cert(d))
.and_then(parse_content_cert)
.map(|cc| cc.bus_encryption)
.unwrap_or(false);
@@ -940,7 +1038,9 @@ pub fn resolve_keys(
// Helper to build result
let build = |vuk: [u8; 16], key_source: u8| -> ResolvedKeys {
let unit_keys: Vec<(u32, [u8; 16])> = uk_file.encrypted_keys.iter()
let unit_keys: Vec<(u32, [u8; 16])> = uk_file
.encrypted_keys
.iter()
.map(|(num, enc_key)| (*num, decrypt_unit_key(&vuk, enc_key)))
.collect();
ResolvedKeys {
@@ -1078,7 +1178,10 @@ pub fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) {
break;
}
// First 16 bytes of each sector are plaintext
aes_cbc_decrypt(read_data_key, &mut unit[sector_start + 16..sector_start + SECTOR_LEN]);
aes_cbc_decrypt(
read_data_key,
&mut unit[sector_start + 16..sector_start + SECTOR_LEN],
);
}
}
@@ -1104,7 +1207,11 @@ mod tests {
/// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found.
fn keydb_path() -> Option<std::path::PathBuf> {
let path = std::path::PathBuf::from(std::env::var("KEYDB_PATH").ok()?);
if path.exists() { Some(path) } else { None }
if path.exists() {
Some(path)
} else {
None
}
}
#[test]
@@ -1139,12 +1246,17 @@ mod tests {
// Civil War UHD: known MK, VID, VUK from KEYDB
// MK = 15665F98..., VID (disc_id) = from entry, VUK = F96D7908...
// VUK = AES-DEC(MK, VID) XOR VID
let path = match keydb_path() { Some(p) => p, None => return };
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find a disc with both MK, disc_id, and VUK so we can verify derivation
let entry = db.disc_entries.values()
let entry = db
.disc_entries
.values()
.find(|e| e.media_key.is_some() && e.disc_id.is_some() && e.vuk.is_some())
.expect("No disc with MK + VID + VUK");
@@ -1153,15 +1265,20 @@ mod tests {
let expected_vuk = entry.vuk.unwrap();
let derived = derive_vuk(&mk, &vid);
assert_eq!(derived, expected_vuk,
"VUK derivation failed for disc: {} (hash {})", entry.title, entry.disc_hash);
assert_eq!(
derived, expected_vuk,
"VUK derivation failed for disc: {} (hash {})",
entry.title, entry.disc_hash
);
eprintln!("VUK derivation verified for: {}", entry.title);
}
#[test]
fn test_aes_ecb_roundtrip() {
let key = [0x15u8, 0x66, 0x5F, 0x98, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C];
let key = [
0x15u8, 0x66, 0x5F, 0x98, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
0x0B, 0x0C,
];
let plain = [0x41u8; 16];
let enc = aes_ecb_encrypt(&key, &plain);
let dec = aes_ecb_decrypt(&key, &enc);
@@ -1179,8 +1296,10 @@ mod tests {
#[test]
fn test_aes_cbc_roundtrip() {
let key = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00];
let key = [
0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
0xFF, 0x00,
];
let original = vec![0x42u8; 128]; // 8 blocks
let mut data = original.clone();
@@ -1269,16 +1388,24 @@ mod tests {
fn test_decrypt_unit_key_from_vuk() {
// Test the full chain: VUK → decrypt encrypted unit key → unit key
// Use a known disc from KEYDB that has both VUK and unit keys
let path = match keydb_path() { Some(p) => p, None => return };
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find a disc with VUK and unit keys
let entry = db.disc_entries.values()
let entry = db
.disc_entries
.values()
.find(|e| e.vuk.is_some() && !e.unit_keys.is_empty())
.expect("No disc with VUK + unit keys");
eprintln!("Testing unit key decrypt for: {} ({})", entry.title, entry.disc_hash);
eprintln!(
"Testing unit key decrypt for: {} ({})",
entry.title, entry.disc_hash
);
eprintln!(" VUK: {:02X?}", entry.vuk.unwrap());
for (num, key) in &entry.unit_keys {
eprintln!(" Unit key {}: {:02X?}", num, key);
@@ -1290,8 +1417,11 @@ mod tests {
for (num, expected_uk) in &entry.unit_keys {
let encrypted = aes_ecb_encrypt(&vuk, expected_uk);
let decrypted = decrypt_unit_key(&vuk, &encrypted);
assert_eq!(&decrypted, expected_uk,
"Unit key {} roundtrip failed for {}", num, entry.title);
assert_eq!(
&decrypted, expected_uk,
"Unit key {} roundtrip failed for {}",
num, entry.title
);
}
eprintln!(" All {} unit key roundtrips passed", entry.unit_keys.len());
}
@@ -1302,21 +1432,31 @@ mod tests {
// This disc is AACS 2.0 (BEE) so unit key alone won't work —
// we need bus decryption first. But this verifies the pipeline.
let unit_path = std::path::Path::new("/tmp/encrypted_unit.bin");
if !unit_path.exists() { return; }
if !unit_path.exists() {
return;
}
let original = std::fs::read(unit_path).unwrap();
assert_eq!(original.len(), ALIGNED_UNIT_LEN);
assert!(is_unit_encrypted(&original), "Unit should be encrypted");
let kp = match keydb_path() { Some(p) => p, None => return };
let kp = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&kp).unwrap();
// Civil War UHD entries
let civil_war_entries: Vec<&DiscEntry> = db.disc_entries.values()
let civil_war_entries: Vec<&DiscEntry> = db
.disc_entries
.values()
.filter(|e| e.title.contains("CIVIL WAR") && !e.unit_keys.is_empty())
.collect();
eprintln!("Found {} Civil War entries with unit keys", civil_war_entries.len());
eprintln!(
"Found {} Civil War entries with unit keys",
civil_war_entries.len()
);
// Try each entry's unit keys
for entry in &civil_war_entries {
@@ -1324,7 +1464,10 @@ mod tests {
let mut unit = original.clone();
if let Some(idx) = decrypt_unit_try_keys(&mut unit, &keys) {
eprintln!("SUCCESS: Decrypted with entry {} key {}", entry.disc_hash, idx);
eprintln!(
"SUCCESS: Decrypted with entry {} key {}",
entry.disc_hash, idx
);
// Count TS sync bytes
let ts = (0..32).filter(|&i| unit[4 + i * 192] == 0x47).count();
eprintln!(" TS sync bytes: {}/32", ts);
@@ -1338,7 +1481,10 @@ mod tests {
#[test]
fn test_parse_full_keydb() {
let path = match keydb_path() { Some(p) => p, None => return }; // skip if not available
let path = match keydb_path() {
Some(p) => p,
None => return,
}; // skip if not available
let db = KeyDb::load(&path).unwrap();
@@ -1348,15 +1494,21 @@ mod tests {
assert!(db.disc_entries.len() > 170000);
// Look up Dune: Part Two
let dune = db.disc_entries.values()
let dune = db
.disc_entries
.values()
.find(|e| e.title.contains("Dune: Part Two") && e.vuk.is_some())
.expect("Dune: Part Two not found");
assert!(dune.media_key.is_some());
assert!(dune.vuk.is_some());
assert!(!dune.unit_keys.is_empty());
eprintln!("Parsed {} disc entries, {} DK, {} PK",
db.disc_entries.len(), db.device_keys.len(), db.processing_keys.len());
eprintln!(
"Parsed {} disc entries, {} DK, {} PK",
db.disc_entries.len(),
db.device_keys.len(),
db.processing_keys.len()
);
}
#[test]
@@ -1371,7 +1523,9 @@ mod tests {
#[test]
fn test_disc_hash_hex() {
let hash = [***REMOVED***];
let hash = [
***REMOVED***,
];
let hex = disc_hash_hex(&hash);
assert_eq!(hex, "***REMOVED***");
}
@@ -1384,7 +1538,10 @@ mod tests {
let mut data = vec![0u8; 256];
// uk_pos = 0x60 (96)
data[0] = 0x00; data[1] = 0x00; data[2] = 0x00; data[3] = 0x60;
data[0] = 0x00;
data[1] = 0x00;
data[2] = 0x00;
data[3] = 0x60;
// Header fields at 16-18
data[16] = 1; // app_type = BD-ROM
@@ -1392,23 +1549,32 @@ mod tests {
data[18] = 0; // no SKB
// Title mapping at 20-25
data[20] = 0; data[21] = 1; // first_play = CPS unit 1
data[22] = 0; data[23] = 1; // top_menu = CPS unit 1
data[24] = 0; data[25] = 1; // num_titles = 1
// Title 0 entry: 2 bytes pad + CPS unit
data[28] = 0; data[29] = 1; // CPS unit 1
data[20] = 0;
data[21] = 1; // first_play = CPS unit 1
data[22] = 0;
data[23] = 1; // top_menu = CPS unit 1
data[24] = 0;
data[25] = 1; // num_titles = 1
// Title 0 entry: 2 bytes pad + CPS unit
data[28] = 0;
data[29] = 1; // CPS unit 1
// Key storage at offset 0x60
let uk_pos = 0x60usize;
data[uk_pos] = 0; data[uk_pos + 1] = 2; // 2 unit keys
data[uk_pos] = 0;
data[uk_pos + 1] = 2; // 2 unit keys
// Key 1 at uk_pos + 48
let key1_pos = uk_pos + 48;
for i in 0..16 { data[key1_pos + i] = 0xAA; }
for i in 0..16 {
data[key1_pos + i] = 0xAA;
}
// Key 2 at uk_pos + 48 + 48
let key2_pos = key1_pos + 48;
for i in 0..16 { data[key2_pos + i] = 0xBB; }
for i in 0..16 {
data[key2_pos + i] = 0xBB;
}
let parsed = parse_unit_key_ro(&data, false).unwrap();
assert_eq!(parsed.app_type, 1);
@@ -1427,9 +1593,14 @@ mod tests {
let mut mkb = vec![0u8; 32];
// Record: type=0x81, length=12 (BE24)
mkb[0] = 0x81;
mkb[1] = 0x00; mkb[2] = 0x00; mkb[3] = 0x0C;
mkb[1] = 0x00;
mkb[2] = 0x00;
mkb[3] = 0x0C;
// Version = 77
mkb[4] = 0x00; mkb[5] = 0x00; mkb[6] = 0x00; mkb[7] = 77;
mkb[4] = 0x00;
mkb[5] = 0x00;
mkb[6] = 0x00;
mkb[7] = 77;
assert_eq!(mkb_version(&mkb), Some(77));
}
@@ -1437,13 +1608,18 @@ mod tests {
#[test]
fn test_resolve_keys_vuk_path() {
// Test the full resolve chain using VUK path
let path = match keydb_path() { Some(p) => p, None => return };
let path = match keydb_path() {
Some(p) => p,
None => return,
};
let db = KeyDb::load(&path).unwrap();
// Find V for Vendetta BD — has VUK and unit keys
// hash: ***REMOVED***
let entry = db.find_disc("***REMOVED***");
if entry.is_none() { return; }
if entry.is_none() {
return;
}
let entry = entry.unwrap();
let vuk = entry.vuk.unwrap();
let vid = entry.disc_id.unwrap();