WIP: OEM disc-keys CDB path — parked pending CDB template discovery
Adds a 5th key-resolution path (KeySource::OemDiscKeys) that uses a per-drive OEM CDB to retrieve disc-key candidates from drive firmware, then validates each as a Media Key against the MKB's verify record before deriving VUK and unit keys. End-to-end plumbing is in place: handshake-side capture, best-effort propagation through HandshakeResult, short-circuit in resolve_encryption ahead of the classical Path 1-4 chain, validate_media_key_against_mkb helper with unit tests, two new error variants. Not usable yet: - No drive profile populates the read_disc_keys_cdb template. - Semantic of returned bytes is unconfirmed. Current code assumes MK candidates; PK candidates is a more architecturally sensible alternative (PKs are per-MKBv finite-set, MKs are per-disc). Either interpretation needs empirical confirmation against a drive whose firmware actually exposes a disc-keys CDB. - src/aacs/verify_magics.rs has speculative non-spec constants and RE-provenance prose; must not ship to public repos in current form. Parked here so the plumbing survives if/when the CDB bytes turn up.
This commit is contained in:
@@ -276,6 +276,25 @@ fn validate_processing_key(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate a candidate Media Key directly against an MKB's Verify
|
||||||
|
/// Media Key record.
|
||||||
|
///
|
||||||
|
/// AES-128D(mk, mk_dv) must decrypt to the magic prefix
|
||||||
|
/// `01 23 45 67 89 AB CD EF`. Used to check an MK that came from
|
||||||
|
/// outside the standard MKB walk (e.g. a drive-provided OEM disc-keys
|
||||||
|
/// response) before bothering to derive a VUK with it.
|
||||||
|
///
|
||||||
|
/// Returns `false` if the MKB has no usable Verify Media Key record
|
||||||
|
/// (so no in-band validation is possible).
|
||||||
|
pub fn validate_media_key_against_mkb(mkb: &[u8], mk: &[u8; 16]) -> bool {
|
||||||
|
let Some(mk_dv) = mkb_find_mk_dv(mkb) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let dec_vd = aes_ecb_decrypt(mk, &mk_dv);
|
||||||
|
const VERIFY_MAGIC: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
||||||
|
dec_vd[..8] == VERIFY_MAGIC
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
fn mkb_find_mk_dv(mkb: &[u8]) -> Option<[u8; 16]> {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
@@ -1253,6 +1272,79 @@ mod tests {
|
|||||||
assert_eq!(body, vec![0xCC, 0xCC, 0xCC, 0xCC]);
|
assert_eq!(body, vec![0xCC, 0xCC, 0xCC, 0xCC]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_media_key_against_mkb_accepts_correct_mk() {
|
||||||
|
// Synthesise an MKB whose 0x86 record's mk_dv was encrypted
|
||||||
|
// with a known MK. The validator must accept that MK and
|
||||||
|
// reject any other.
|
||||||
|
use super::super::decrypt::aes_ecb_encrypt as enc;
|
||||||
|
|
||||||
|
let mk: [u8; 16] = [
|
||||||
|
0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD,
|
||||||
|
0xAE, 0xAF,
|
||||||
|
];
|
||||||
|
let mut plaintext_vd = [0u8; 16];
|
||||||
|
plaintext_vd[..8].copy_from_slice(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
|
||||||
|
plaintext_vd[8..].copy_from_slice(&[0x55; 8]);
|
||||||
|
let mk_dv = enc(&mk, &plaintext_vd);
|
||||||
|
|
||||||
|
let mut mkb = vec![
|
||||||
|
0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D,
|
||||||
|
];
|
||||||
|
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]);
|
||||||
|
mkb.extend_from_slice(&mk_dv);
|
||||||
|
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
validate_media_key_against_mkb(&mkb, &mk),
|
||||||
|
"correct MK must validate"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A bit-flipped MK must be rejected.
|
||||||
|
let mut wrong = mk;
|
||||||
|
wrong[0] ^= 0x01;
|
||||||
|
assert!(
|
||||||
|
!validate_media_key_against_mkb(&mkb, &wrong),
|
||||||
|
"wrong MK must be rejected"
|
||||||
|
);
|
||||||
|
|
||||||
|
// An MKB without any verify-record returns false (no validation possible).
|
||||||
|
let mkb_no_verify = vec![
|
||||||
|
0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D,
|
||||||
|
];
|
||||||
|
assert!(
|
||||||
|
!validate_media_key_against_mkb(&mkb_no_verify, &mk),
|
||||||
|
"MKB without 0x81/0x86 record cannot validate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_media_key_recovers_known_vuk_from_mk_plus_vid() {
|
||||||
|
// Sanity: derive_vuk(mk, vid) is the same operation regardless
|
||||||
|
// of whether MK came from a Device Key walk or a drive-side
|
||||||
|
// disc-keys CDB. With a planted MK + VID we recover the VUK
|
||||||
|
// exactly. This is the math the OEM-disc-keys short-circuit
|
||||||
|
// relies on.
|
||||||
|
use super::super::decrypt::aes_ecb_decrypt as dec;
|
||||||
|
|
||||||
|
let mk: [u8; 16] = [
|
||||||
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||||
|
0xFF, 0x00,
|
||||||
|
];
|
||||||
|
let vid: [u8; 16] = [
|
||||||
|
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC,
|
||||||
|
0xDE, 0xF0,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut expected = dec(&mk, &vid);
|
||||||
|
for i in 0..16 {
|
||||||
|
expected[i] ^= vid[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
let derived = derive_vuk(&mk, &vid);
|
||||||
|
assert_eq!(derived, expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mkb_find_mk_dv_recognizes_type_0x86() {
|
fn mkb_find_mk_dv_recognizes_type_0x86() {
|
||||||
// AACS 2.0 form uses type 0x86 for the verify record.
|
// AACS 2.0 form uses type 0x86 for the verify record.
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ pub mod handshake;
|
|||||||
pub mod keydb;
|
pub mod keydb;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod variants;
|
pub mod variants;
|
||||||
|
pub mod verify_magics;
|
||||||
|
|
||||||
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
// Explicit re-exports — only items needed by external consumers and sibling crate modules.
|
||||||
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
// AES primitives (aes_ecb_encrypt, aes_ecb_decrypt, aes_cbc_decrypt) are pub(crate) in decrypt.rs.
|
||||||
@@ -30,7 +31,7 @@ pub use keys::{
|
|||||||
AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key,
|
AacsVersion, ContentCert, ResolveContext, ResolvedKeys, UnitKeyFile, decrypt_unit_key,
|
||||||
derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex,
|
derive_media_key_from_dk, derive_media_key_from_pk, derive_vuk, disc_hash, disc_hash_hex,
|
||||||
mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1,
|
mkb_version, parse_content_cert, parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1,
|
||||||
resolve_keys_v2, resolve_keys_v21,
|
resolve_keys_v2, resolve_keys_v21, validate_media_key_against_mkb,
|
||||||
};
|
};
|
||||||
pub use variants::{
|
pub use variants::{
|
||||||
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, MkbRecord, ProcessingKeyMatch,
|
KEY_CORRECTION_DATA_PLACEHOLDER, MediaKeyVariantError, MkbRecord, ProcessingKeyMatch,
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//! AACS Verify-Media-Key magic constants used to confirm Media Key
|
||||||
|
//! candidates produced during MKB walking.
|
||||||
|
//!
|
||||||
|
//! AACS MKBs contain "Verify Media Key Records" whose decrypted output
|
||||||
|
//! is a known-plaintext constant. Walking code decrypts the verify
|
||||||
|
//! record with each MK candidate and compares the result against the
|
||||||
|
//! magic; on match, the MK is correct.
|
||||||
|
//!
|
||||||
|
//! Five distinct magics are observed in the canonical reference AACS
|
||||||
|
//! engine (MakeMKV v1.18.3, file offsets in parens):
|
||||||
|
//!
|
||||||
|
//! 1. **MK\_V10** at `.rodata:0x2909c0`. The original AACS-1.0 spec
|
||||||
|
//! constant. Single 16-byte AES-128-ECB compare. Used at 3 sites in
|
||||||
|
//! that engine. We already use it in `keys.rs::validate_media_key_against_mkb`.
|
||||||
|
//!
|
||||||
|
//! 2. **MK\_AUX\_16** at `.rodata:0x290890`. A second single-block
|
||||||
|
//! 16-byte verification magic. Reverse-engineering of the call site
|
||||||
|
//! at `0x580f73` shows it after a call to the single-block AES-ECB
|
||||||
|
//! helper. Likely a per-vendor or per-record-type extended verify.
|
||||||
|
//! Use it when an MKB carries an extended verify record alongside
|
||||||
|
//! the standard one.
|
||||||
|
//!
|
||||||
|
//! 3. **MK\_SK\_32a** = `MK_SK32A_BLK0` || `MK_SK32A_BLK1`. A 32-byte
|
||||||
|
//! (2-block) verify magic at `.rodata:0x290910 / 0x290620`. Used at
|
||||||
|
//! `0x580ff0`: both blocks must match after AES-128 decrypt of a
|
||||||
|
//! 32-byte verify record. Almost certainly the AACS-2 / Sequence
|
||||||
|
//! Key Block "Verify Media Key Record for Sequence Keys" expanded
|
||||||
|
//! form — i.e. AACS-2 SKB verification.
|
||||||
|
//!
|
||||||
|
//! 4. **MK\_SK\_32b** = `MK_SK32B_BLK0` || `MK_SK32B_BLK1`. A second
|
||||||
|
//! 32-byte verify magic at `.rodata:0x290980 / 0x290a60`. Used at
|
||||||
|
//! `0x581063`. Different record type within the SKB family — likely
|
||||||
|
//! the AACS-2 SD-tree variant verification.
|
||||||
|
//!
|
||||||
|
//! All five are KNOWN PLAINTEXT compared bit-for-bit against the
|
||||||
|
//! AES-128 decrypt output. They are NOT keys. They are oracle values
|
||||||
|
//! that say "yes, the MK candidate you tried is the right one."
|
||||||
|
//!
|
||||||
|
//! Provenance: identified via static RE of MakeMKV v1.18.3 amd64
|
||||||
|
//! (binary sha256 `9970a50a97231b2d09d73f521ff1daf0609ea201040a68ecaa9f31af957d6401`)
|
||||||
|
//! on 2026-05-22 via objdump of the `pcmpeqb` callsite cluster around
|
||||||
|
//! file offset `0x580f70..0x581080`.
|
||||||
|
|
||||||
|
/// AACS-1.0 / pre-existing canonical Verify Media Key magic.
|
||||||
|
///
|
||||||
|
/// `AES-128-ECB-DECRYPT(MK, verify_record) == [VERIFY_MK_V10 || pad]`
|
||||||
|
pub const VERIFY_MK_V10: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
|
||||||
|
|
||||||
|
/// Single-block 16-byte verify magic (auxiliary). Compared full-16
|
||||||
|
/// after AES-128-ECB(MK, in) at `pcmpeqb` site `0x580f73`.
|
||||||
|
pub const VERIFY_MK_AUX_16: [u8; 16] = [
|
||||||
|
0xf9, 0x91, 0xa3, 0x60, 0x68, 0x15, 0xa6, 0xb9, 0x55, 0xbb, 0xce, 0xa3, 0xb1, 0x4b, 0xf8, 0xd8,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 32-byte SKB-style verify magic, block 0 of 2. Compared full-16
|
||||||
|
/// after AES-128 decrypt of the first 16 bytes of a 32-byte verify
|
||||||
|
/// record. `pcmpeqb` site `0x580ff0`.
|
||||||
|
pub const VERIFY_MK_SK_32A_BLK0: [u8; 16] = [
|
||||||
|
0x19, 0x0f, 0xe9, 0x7f, 0xad, 0x11, 0xa4, 0x10, 0xc6, 0x56, 0x9d, 0x1c, 0x84, 0x21, 0x1d, 0x18,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 32-byte SKB-style verify magic, block 1 of 2. Compared full-16
|
||||||
|
/// after AES-128 decrypt of bytes 16..32 of the same record.
|
||||||
|
/// `pcmpeqb` site `0x580fe8`.
|
||||||
|
pub const VERIFY_MK_SK_32A_BLK1: [u8; 16] = [
|
||||||
|
0x9b, 0x54, 0x9a, 0x25, 0x69, 0x8a, 0xa2, 0x3f, 0x9d, 0xfd, 0x2c, 0x95, 0xe2, 0x4a, 0x97, 0x02,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 32-byte SKB-style verify magic (variant B), block 0 of 2.
|
||||||
|
/// `pcmpeqb` site `0x581063`.
|
||||||
|
pub const VERIFY_MK_SK_32B_BLK0: [u8; 16] = [
|
||||||
|
0x8d, 0xee, 0xe0, 0x1e, 0xc7, 0x0c, 0xea, 0xb3, 0xdb, 0xd2, 0xfb, 0x82, 0x16, 0x3c, 0x26, 0x80,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 32-byte SKB-style verify magic (variant B), block 1 of 2.
|
||||||
|
/// `pcmpeqb` site `0x58105b`.
|
||||||
|
pub const VERIFY_MK_SK_32B_BLK1: [u8; 16] = [
|
||||||
|
0xaf, 0x93, 0x7a, 0x74, 0x8a, 0xce, 0xd3, 0x69, 0x36, 0x84, 0xe6, 0xea, 0xf8, 0x54, 0xe8, 0xa2,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Tag for a candidate-Media-Key check. Tells the verifier which
|
||||||
|
/// known-plaintext to compare against; the verifier chooses the
|
||||||
|
/// magic that matches the MKB record type at hand.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum VerifyMagic {
|
||||||
|
/// AACS-1.0 / canonical.
|
||||||
|
V10,
|
||||||
|
/// Auxiliary single-block (16-byte) verification.
|
||||||
|
Aux16,
|
||||||
|
/// SKB-style 32-byte verification, variant A.
|
||||||
|
Sk32A,
|
||||||
|
/// SKB-style 32-byte verification, variant B.
|
||||||
|
Sk32B,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a candidate Media Key against a `dec_vd` (AES-128 decrypt
|
||||||
|
/// of the MKB Verify Media Key Record under the candidate MK).
|
||||||
|
///
|
||||||
|
/// Returns `true` if `dec_vd` matches the magic identified by `tag`.
|
||||||
|
///
|
||||||
|
/// - `V10`: compares the first 8 bytes against `VERIFY_MK_V10`.
|
||||||
|
/// - `Aux16`: compares the full 16 bytes against `VERIFY_MK_AUX_16`.
|
||||||
|
/// - `Sk32A` / `Sk32B`: `dec_vd` must be exactly 32 bytes (`block0 ||
|
||||||
|
/// block1`); compares each block against the corresponding constant.
|
||||||
|
pub fn check_verify(tag: VerifyMagic, dec_vd: &[u8]) -> bool {
|
||||||
|
match tag {
|
||||||
|
VerifyMagic::V10 => dec_vd.len() >= 8 && dec_vd[..8] == VERIFY_MK_V10,
|
||||||
|
VerifyMagic::Aux16 => dec_vd.len() >= 16 && dec_vd[..16] == VERIFY_MK_AUX_16,
|
||||||
|
VerifyMagic::Sk32A => {
|
||||||
|
dec_vd.len() >= 32
|
||||||
|
&& dec_vd[..16] == VERIFY_MK_SK_32A_BLK0
|
||||||
|
&& dec_vd[16..32] == VERIFY_MK_SK_32A_BLK1
|
||||||
|
}
|
||||||
|
VerifyMagic::Sk32B => {
|
||||||
|
dec_vd.len() >= 32
|
||||||
|
&& dec_vd[..16] == VERIFY_MK_SK_32B_BLK0
|
||||||
|
&& dec_vd[16..32] == VERIFY_MK_SK_32B_BLK1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v10_matches_canonical_prefix() {
|
||||||
|
let mut dec = [0u8; 16];
|
||||||
|
dec[..8].copy_from_slice(&VERIFY_MK_V10);
|
||||||
|
assert!(check_verify(VerifyMagic::V10, &dec));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aux16_matches_full_block() {
|
||||||
|
assert!(check_verify(VerifyMagic::Aux16, &VERIFY_MK_AUX_16));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sk32a_requires_both_blocks() {
|
||||||
|
let mut dec = [0u8; 32];
|
||||||
|
dec[..16].copy_from_slice(&VERIFY_MK_SK_32A_BLK0);
|
||||||
|
dec[16..].copy_from_slice(&VERIFY_MK_SK_32A_BLK1);
|
||||||
|
assert!(check_verify(VerifyMagic::Sk32A, &dec));
|
||||||
|
|
||||||
|
// Mutate block 1, must fail.
|
||||||
|
dec[20] ^= 0x80;
|
||||||
|
assert!(!check_verify(VerifyMagic::Sk32A, &dec));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sk32b_distinct_from_sk32a() {
|
||||||
|
let mut dec = [0u8; 32];
|
||||||
|
dec[..16].copy_from_slice(&VERIFY_MK_SK_32B_BLK0);
|
||||||
|
dec[16..].copy_from_slice(&VERIFY_MK_SK_32B_BLK1);
|
||||||
|
assert!(check_verify(VerifyMagic::Sk32B, &dec));
|
||||||
|
// Same plaintext must NOT validate as Sk32A.
|
||||||
|
assert!(!check_verify(VerifyMagic::Sk32A, &dec));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn short_input_never_matches() {
|
||||||
|
let dec = [0u8; 4];
|
||||||
|
for tag in [
|
||||||
|
VerifyMagic::V10,
|
||||||
|
VerifyMagic::Aux16,
|
||||||
|
VerifyMagic::Sk32A,
|
||||||
|
VerifyMagic::Sk32B,
|
||||||
|
] {
|
||||||
|
assert!(!check_verify(tag, &dec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,47 @@ use crate::error::{Error, Result};
|
|||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
use crate::udf;
|
use crate::udf;
|
||||||
|
|
||||||
|
/// Format a 16-byte key as lowercase hex (no separators) for logging.
|
||||||
|
fn hex16(k: &[u8; 16]) -> String {
|
||||||
|
let mut s = String::with_capacity(32);
|
||||||
|
for b in k {
|
||||||
|
s.push_str(&format!("{b:02x}"));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of SCSI AACS handshake (ECDH authentication).
|
/// Result of SCSI AACS handshake (ECDH authentication).
|
||||||
/// Only available when scanning from a real drive, not ISO images.
|
/// Only available when scanning from a real drive, not ISO images.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) struct HandshakeResult {
|
pub(super) struct HandshakeResult {
|
||||||
pub volume_id: [u8; 16],
|
pub volume_id: [u8; 16],
|
||||||
pub read_data_key: Option<[u8; 16]>,
|
pub read_data_key: Option<[u8; 16]>,
|
||||||
|
/// OEM disc-keys (drive-side) — populated only when the drive is in
|
||||||
|
/// the extended-access state and its profile carries the
|
||||||
|
/// `read_disc_keys_cdb` template. Two 16-byte candidate keys per
|
||||||
|
/// the OEM response layout. Interpretation is empirical: candidates
|
||||||
|
/// are tried as Media Key against the MKB's mk_dv magic in
|
||||||
|
/// [`crate::disc::Disc::resolve_encryption`].
|
||||||
|
pub oem_disc_keys: Option<DiscKeys>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive-provided disc-keys pair retrieved via the OEM
|
||||||
|
/// `read_disc_keys_cdb` template.
|
||||||
|
///
|
||||||
|
/// Response layout (36 bytes):
|
||||||
|
/// * `[0..3]` 3-byte signature (validated against `00 22 00`)
|
||||||
|
/// * `[3]` reserved
|
||||||
|
/// * `[4..20]` first 16-byte key candidate (`key_a`)
|
||||||
|
/// * `[20..36]` second 16-byte key candidate (`key_b`)
|
||||||
|
///
|
||||||
|
/// Semantic of `key_a` / `key_b` is empirical — both may carry a Media
|
||||||
|
/// Key, a derived VUK, or telemetry padding depending on firmware. The
|
||||||
|
/// resolver tries them as MK candidates against the MKB's mk_dv first
|
||||||
|
/// (cheap and in-spec) before falling back to other paths.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct DiscKeys {
|
||||||
|
pub key_a: [u8; 16],
|
||||||
|
pub key_b: [u8; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
@@ -96,6 +131,61 @@ impl Disc {
|
|||||||
Ok(vid)
|
Ok(vid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve the drive-side disc-keys pair.
|
||||||
|
///
|
||||||
|
/// Mirrors [`Disc::read_vid`]: when the drive reports
|
||||||
|
/// `is_unlocked()` and its profile carries a `read_disc_keys_cdb`
|
||||||
|
/// template, issue a single SCSI command and parse the 36-byte
|
||||||
|
/// response into two 16-byte key candidates. No AGID setup, no
|
||||||
|
/// bus-key — the drive's runtime firmware serves the keys directly
|
||||||
|
/// when in extended-access state.
|
||||||
|
///
|
||||||
|
/// Returns `Err(DriveProfileMissing)` / `Err(DiscKeysCdbUnavailable)`
|
||||||
|
/// when the path simply isn't available for this drive (no profile
|
||||||
|
/// match, or profile predates the disc-keys CDB capture).
|
||||||
|
/// Returns `Err(DiscKeysSignatureInvalid)` when the response header
|
||||||
|
/// is not `00 22 00` — typically the drive isn't in the expected
|
||||||
|
/// state. Returns the OEM `ScsiError` on transport failure.
|
||||||
|
pub fn read_disc_keys(session: &mut crate::drive::Drive) -> Result<DiscKeys> {
|
||||||
|
if !session.is_unlocked() {
|
||||||
|
return Err(Error::DiscKeysCdbUnavailable);
|
||||||
|
}
|
||||||
|
let profile = session
|
||||||
|
.drive_profile()
|
||||||
|
.ok_or(Error::DriveProfileMissing)?
|
||||||
|
.clone();
|
||||||
|
Self::read_disc_keys_oem(session, &profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OEM disc-keys retrieval — issues the per-drive CDB and parses
|
||||||
|
/// the 36-byte response.
|
||||||
|
fn read_disc_keys_oem(
|
||||||
|
session: &mut crate::drive::Drive,
|
||||||
|
profile: &crate::profile::DriveProfile,
|
||||||
|
) -> Result<DiscKeys> {
|
||||||
|
const RESPONSE_LEN: usize = 36;
|
||||||
|
let cdb = profile
|
||||||
|
.read_disc_keys_cdb
|
||||||
|
.ok_or(Error::DiscKeysCdbUnavailable)?;
|
||||||
|
let mut buf = vec![0u8; RESPONSE_LEN];
|
||||||
|
let result = session.scsi_execute(
|
||||||
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
|
)?;
|
||||||
|
if result.bytes_transferred < RESPONSE_LEN {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "oem_disc_keys_short_response",
|
||||||
|
bytes_transferred = result.bytes_transferred,
|
||||||
|
"OEM disc-keys CDB returned short response"
|
||||||
|
);
|
||||||
|
return Err(Error::DiscKeysSignatureInvalid);
|
||||||
|
}
|
||||||
|
parse_disc_keys_response(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
/// Cert-based VID retrieval — runs the full AACS mutual-auth
|
/// Cert-based VID retrieval — runs the full AACS mutual-auth
|
||||||
/// handshake and extracts VID from the bus-key-MAC'd
|
/// handshake and extracts VID from the bus-key-MAC'd
|
||||||
/// `REPORT_DISC_STRUCTURE` response.
|
/// `REPORT_DISC_STRUCTURE` response.
|
||||||
@@ -144,10 +234,29 @@ impl Disc {
|
|||||||
// malformed, fall through to cert-based auth.
|
// malformed, fall through to cert-based auth.
|
||||||
match Self::read_vid(session, opts) {
|
match Self::read_vid(session, opts) {
|
||||||
Ok(volume_id) => {
|
Ok(volume_id) => {
|
||||||
|
// Best-effort: also pull the OEM disc-keys pair so
|
||||||
|
// the resolver can try them as MK candidates
|
||||||
|
// against the MKB later. Missing CDB template /
|
||||||
|
// bad signature / transport hiccup all degrade
|
||||||
|
// silently — we still have a working VID and the
|
||||||
|
// standard KEYDB paths remain available.
|
||||||
|
let oem_disc_keys = match Self::read_disc_keys(session) {
|
||||||
|
Ok(dk) => Some(dk),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "handshake_oem_disc_keys_unavailable",
|
||||||
|
error_code = e.code(),
|
||||||
|
"OEM disc-keys retrieval failed; continuing without"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
Some(HandshakeResult {
|
Some(HandshakeResult {
|
||||||
volume_id,
|
volume_id,
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
|
oem_disc_keys,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@@ -278,6 +387,7 @@ impl Disc {
|
|||||||
Some(HandshakeResult {
|
Some(HandshakeResult {
|
||||||
volume_id,
|
volume_id,
|
||||||
read_data_key,
|
read_data_key,
|
||||||
|
oem_disc_keys: None,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@@ -396,6 +506,102 @@ impl Disc {
|
|||||||
let vid_available = volume_id != [0u8; 16];
|
let vid_available = volume_id != [0u8; 16];
|
||||||
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
||||||
|
|
||||||
|
// OEM disc-keys short-circuit. When the drive provided a
|
||||||
|
// disc-keys pair via the unlocked-firmware CDB, try each as a
|
||||||
|
// Media Key candidate against the MKB's mk_dv magic. If one
|
||||||
|
// validates the resulting VUK works exactly as if we'd walked
|
||||||
|
// the MKB with a device key — bypasses the v77+ DK requirement
|
||||||
|
// entirely.
|
||||||
|
//
|
||||||
|
// Both candidates are also dumped to the log unconditionally
|
||||||
|
// so an operator can inspect them when validation misses
|
||||||
|
// (e.g. firmware returns a pre-derived VUK rather than MK,
|
||||||
|
// which has no in-band validator).
|
||||||
|
if vid_available {
|
||||||
|
if let Some(disc_keys) = handshake.and_then(|h| h.oem_disc_keys) {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "resolve_encryption_oem_disc_keys_candidates",
|
||||||
|
key_a = %hex16(&disc_keys.key_a),
|
||||||
|
key_b = %hex16(&disc_keys.key_b),
|
||||||
|
mkb_present = mkb_data.is_some(),
|
||||||
|
"OEM disc-keys retrieved; testing as MK candidates"
|
||||||
|
);
|
||||||
|
if let Some(mkb) = mkb_data.as_deref() {
|
||||||
|
for (label, candidate) in
|
||||||
|
[("key_a", &disc_keys.key_a), ("key_b", &disc_keys.key_b)]
|
||||||
|
{
|
||||||
|
if aacs::validate_media_key_against_mkb(mkb, candidate) {
|
||||||
|
let vuk = aacs::derive_vuk(candidate, &volume_id);
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "resolve_encryption_oem_disc_keys_mk_validated",
|
||||||
|
slot = label,
|
||||||
|
vuk = %hex16(&vuk),
|
||||||
|
"OEM disc-keys MK candidate validated against MKB"
|
||||||
|
);
|
||||||
|
// Parse Unit_Key_RO at the correct stride
|
||||||
|
// for this disc (V20/V21 stride for UHD,
|
||||||
|
// V10 for legacy BD). We pick V20 as a
|
||||||
|
// safe default for AACS 2.x; the
|
||||||
|
// dispatcher would have done the same.
|
||||||
|
let cc_version = cc_data
|
||||||
|
.as_deref()
|
||||||
|
.and_then(aacs::parse_content_cert)
|
||||||
|
.map(|c| c.version)
|
||||||
|
.unwrap_or(aacs::AacsVersion::V20);
|
||||||
|
let stride_version = if matches!(cc_version, aacs::AacsVersion::V10) {
|
||||||
|
aacs::AacsVersion::V10
|
||||||
|
} else {
|
||||||
|
aacs::AacsVersion::V20
|
||||||
|
};
|
||||||
|
if let Some(uk_file) =
|
||||||
|
aacs::parse_unit_key_ro(&uk_ro_data, stride_version)
|
||||||
|
{
|
||||||
|
let unit_keys: Vec<(u32, [u8; 16])> = uk_file
|
||||||
|
.encrypted_keys
|
||||||
|
.iter()
|
||||||
|
.map(|(num, enc_key)| {
|
||||||
|
(*num, aacs::decrypt_unit_key(&vuk, enc_key))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let bus_encryption = cc_data
|
||||||
|
.as_deref()
|
||||||
|
.and_then(aacs::parse_content_cert)
|
||||||
|
.map(|c| c.bus_encryption)
|
||||||
|
.unwrap_or(false);
|
||||||
|
return Ok(AacsState {
|
||||||
|
version: match cc_version {
|
||||||
|
aacs::AacsVersion::V10 => 1,
|
||||||
|
aacs::AacsVersion::V20 | aacs::AacsVersion::V21 => 2,
|
||||||
|
},
|
||||||
|
bus_encryption,
|
||||||
|
mkb_version: mkb_ver,
|
||||||
|
disc_hash: aacs::disc_hash_hex(&uk_file.disc_hash),
|
||||||
|
key_source: KeySource::OemDiscKeys,
|
||||||
|
vuk,
|
||||||
|
unit_keys,
|
||||||
|
read_data_key,
|
||||||
|
volume_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "resolve_encryption_oem_disc_keys_mk_miss",
|
||||||
|
"neither OEM disc-keys candidate validated as MK against MKB"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "resolve_encryption_oem_disc_keys_no_mkb",
|
||||||
|
"OEM disc-keys present but no MKB on disc; cannot validate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key.
|
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key.
|
||||||
//
|
//
|
||||||
// Distinguish "we had every input and still missed" from "we
|
// Distinguish "we had every input and still missed" from "we
|
||||||
@@ -466,3 +672,96 @@ impl Disc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pure-data parser for the 36-byte OEM disc-keys response. Split out
|
||||||
|
/// for unit tests so the layout invariant (`00 22 00` header,
|
||||||
|
/// `[4..20]`, `[20..36]`) is covered without a live SCSI session.
|
||||||
|
pub(crate) fn parse_disc_keys_response(buf: &[u8]) -> Result<DiscKeys> {
|
||||||
|
const RESPONSE_LEN: usize = 36;
|
||||||
|
const EXPECTED_HEADER: [u8; 3] = [0x00, 0x22, 0x00];
|
||||||
|
|
||||||
|
if buf.len() < RESPONSE_LEN {
|
||||||
|
return Err(Error::DiscKeysSignatureInvalid);
|
||||||
|
}
|
||||||
|
if buf[0..3] != EXPECTED_HEADER {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "oem_disc_keys_bad_header",
|
||||||
|
header_0 = buf[0],
|
||||||
|
header_1 = buf[1],
|
||||||
|
header_2 = buf[2],
|
||||||
|
"OEM disc-keys response header mismatch"
|
||||||
|
);
|
||||||
|
return Err(Error::DiscKeysSignatureInvalid);
|
||||||
|
}
|
||||||
|
let mut key_a = [0u8; 16];
|
||||||
|
let mut key_b = [0u8; 16];
|
||||||
|
key_a.copy_from_slice(&buf[4..20]);
|
||||||
|
key_b.copy_from_slice(&buf[20..36]);
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "oem_disc_keys_ok",
|
||||||
|
"OEM disc-keys retrieved"
|
||||||
|
);
|
||||||
|
Ok(DiscKeys { key_a, key_b })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn synth_response(header: [u8; 3], a: [u8; 16], b: [u8; 16]) -> Vec<u8> {
|
||||||
|
let mut v = Vec::with_capacity(36);
|
||||||
|
v.extend_from_slice(&header);
|
||||||
|
v.push(0x00); // reserved byte at offset 3
|
||||||
|
v.extend_from_slice(&a);
|
||||||
|
v.extend_from_slice(&b);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_disc_keys_response_accepts_signature_and_splits_keys() {
|
||||||
|
let key_a: [u8; 16] = [
|
||||||
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||||||
|
0xFF, 0x00,
|
||||||
|
];
|
||||||
|
let key_b: [u8; 16] = [
|
||||||
|
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB,
|
||||||
|
0xCD, 0xEF,
|
||||||
|
];
|
||||||
|
let resp = synth_response([0x00, 0x22, 0x00], key_a, key_b);
|
||||||
|
let parsed = parse_disc_keys_response(&resp).expect("valid response must parse");
|
||||||
|
assert_eq!(parsed.key_a, key_a);
|
||||||
|
assert_eq!(parsed.key_b, key_b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_disc_keys_response_rejects_bad_signature() {
|
||||||
|
let resp = synth_response([0xFF, 0xFF, 0xFF], [0u8; 16], [0u8; 16]);
|
||||||
|
let err = parse_disc_keys_response(&resp).unwrap_err();
|
||||||
|
assert!(matches!(err, Error::DiscKeysSignatureInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_disc_keys_response_rejects_short_buffer() {
|
||||||
|
let short = [0u8; 16];
|
||||||
|
let err = parse_disc_keys_response(&short).unwrap_err();
|
||||||
|
assert!(matches!(err, Error::DiscKeysSignatureInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_disc_keys_response_garbage_is_signature_invalid() {
|
||||||
|
let resp = vec![0u8; 36];
|
||||||
|
let err = parse_disc_keys_response(&resp).unwrap_err();
|
||||||
|
assert!(matches!(err, Error::DiscKeysSignatureInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hex16_formats_lowercase_no_separator() {
|
||||||
|
let k: [u8; 16] = [
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
|
||||||
|
0xEE, 0xFF,
|
||||||
|
];
|
||||||
|
assert_eq!(hex16(&k), "00112233445566778899aabbccddeeff");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-1
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
mod bluray;
|
mod bluray;
|
||||||
mod dvd;
|
mod dvd;
|
||||||
mod encrypt;
|
pub mod encrypt;
|
||||||
pub mod mapfile;
|
pub mod mapfile;
|
||||||
mod patch;
|
mod patch;
|
||||||
pub mod read_error;
|
pub mod read_error;
|
||||||
@@ -21,8 +21,19 @@ use crate::error::{Error, Result};
|
|||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
use crate::udf;
|
use crate::udf;
|
||||||
|
|
||||||
|
pub use encrypt::DiscKeys;
|
||||||
use encrypt::HandshakeResult;
|
use encrypt::HandshakeResult;
|
||||||
|
|
||||||
|
/// Retrieve the drive-side OEM disc-keys pair.
|
||||||
|
///
|
||||||
|
/// Thin free-function wrapper around [`Disc::read_disc_keys`] so
|
||||||
|
/// `Drive` can expose a top-level accessor without depending on
|
||||||
|
/// `Disc`'s internal layout. See [`Disc::read_disc_keys`] for the
|
||||||
|
/// contract.
|
||||||
|
pub fn read_disc_keys(session: &mut crate::drive::Drive) -> crate::error::Result<DiscKeys> {
|
||||||
|
Disc::read_disc_keys(session)
|
||||||
|
}
|
||||||
|
|
||||||
// Re-export label classification enums alongside AudioStream / SubtitleStream
|
// Re-export label classification enums alongside AudioStream / SubtitleStream
|
||||||
// so the public surface keeps the structured metadata together. Callers map
|
// so the public surface keeps the structured metadata together. Callers map
|
||||||
// these to display text in their own locale.
|
// these to display text in their own locale.
|
||||||
@@ -904,6 +915,12 @@ pub enum KeySource {
|
|||||||
ProcessingKey,
|
ProcessingKey,
|
||||||
/// MKB + device keys → subset-difference tree → VUK
|
/// MKB + device keys → subset-difference tree → VUK
|
||||||
DeviceKey,
|
DeviceKey,
|
||||||
|
/// Drive-side OEM disc-keys CDB produced a Media Key candidate
|
||||||
|
/// that validated against the MKB's mk_dv; VUK derived from it
|
||||||
|
/// and the handshake VID. Available only on drives whose
|
||||||
|
/// extended-access firmware exposes the disc-keys path (no
|
||||||
|
/// host-side device keys required).
|
||||||
|
OemDiscKeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeySource {
|
impl KeySource {
|
||||||
@@ -913,6 +930,7 @@ impl KeySource {
|
|||||||
KeySource::KeyDbDerived => "KEYDB (derived)",
|
KeySource::KeyDbDerived => "KEYDB (derived)",
|
||||||
KeySource::ProcessingKey => "MKB + processing key",
|
KeySource::ProcessingKey => "MKB + processing key",
|
||||||
KeySource::DeviceKey => "MKB + device key",
|
KeySource::DeviceKey => "MKB + device key",
|
||||||
|
KeySource::OemDiscKeys => "OEM disc-keys",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -671,6 +671,23 @@ impl Drive {
|
|||||||
) -> Result<crate::scsi::ScsiResult> {
|
) -> Result<crate::scsi::ScsiResult> {
|
||||||
self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
|
self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve the drive-side OEM disc-keys pair.
|
||||||
|
///
|
||||||
|
/// Issues the per-drive `read_disc_keys_cdb` template (a single
|
||||||
|
/// SCSI command) and parses the 36-byte response into two 16-byte
|
||||||
|
/// key candidates. Requires the drive to report `is_unlocked()`
|
||||||
|
/// and its profile to carry the disc-keys CDB template; otherwise
|
||||||
|
/// returns `Err(DiscKeysCdbUnavailable)` /
|
||||||
|
/// `Err(DriveProfileMissing)`.
|
||||||
|
///
|
||||||
|
/// Empirical instrumentation: the contents are not interpreted by
|
||||||
|
/// the drive layer. Higher layers (AACS resolver) try them as MK
|
||||||
|
/// candidates against the MKB's mk_dv to test whether the drive's
|
||||||
|
/// extended-access firmware exposes a working MK.
|
||||||
|
pub fn read_disc_keys(&mut self) -> Result<crate::disc::DiscKeys> {
|
||||||
|
crate::disc::read_disc_keys(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Drive {
|
impl Drop for Drive {
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ pub const E_AACS_MK_UNAVAILABLE: u16 = 7018;
|
|||||||
pub const E_AACS_VUK_NOT_IN_KEYDB: u16 = 7019;
|
pub const E_AACS_VUK_NOT_IN_KEYDB: u16 = 7019;
|
||||||
pub const E_DRIVE_PROFILE_MISSING: u16 = 7020;
|
pub const E_DRIVE_PROFILE_MISSING: u16 = 7020;
|
||||||
pub const E_VID_CDB_UNAVAILABLE: u16 = 7021;
|
pub const E_VID_CDB_UNAVAILABLE: u16 = 7021;
|
||||||
|
pub const E_DISC_KEYS_CDB_UNAVAILABLE: u16 = 7022;
|
||||||
|
pub const E_DISC_KEYS_SIGNATURE_INVALID: u16 = 7023;
|
||||||
|
|
||||||
// Keydb (8xxx)
|
// Keydb (8xxx)
|
||||||
pub const E_KEYDB_CONNECT: u16 = 8000;
|
pub const E_KEYDB_CONNECT: u16 = 8000;
|
||||||
@@ -253,6 +255,14 @@ pub enum Error {
|
|||||||
/// template (older profile blob, or a drive class without an OEM
|
/// template (older profile blob, or a drive class without an OEM
|
||||||
/// VID path).
|
/// VID path).
|
||||||
VidCdbUnavailable,
|
VidCdbUnavailable,
|
||||||
|
/// Drive's profile is present but doesn't carry a Disc-Keys CDB
|
||||||
|
/// template. The OEM disc-keys retrieval path can't run.
|
||||||
|
DiscKeysCdbUnavailable,
|
||||||
|
/// OEM `read_disc_keys` response header did not match the expected
|
||||||
|
/// signature `00 22 00`. Either the drive is not actually in the
|
||||||
|
/// extended-access state, the CDB template is wrong, or the
|
||||||
|
/// firmware on this drive does not expose disc-keys.
|
||||||
|
DiscKeysSignatureInvalid,
|
||||||
|
|
||||||
// Keydb (8xxx)
|
// Keydb (8xxx)
|
||||||
KeydbConnect {
|
KeydbConnect {
|
||||||
@@ -345,6 +355,8 @@ impl Error {
|
|||||||
Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB,
|
Error::AacsVukNotInKeydb => E_AACS_VUK_NOT_IN_KEYDB,
|
||||||
Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING,
|
Error::DriveProfileMissing => E_DRIVE_PROFILE_MISSING,
|
||||||
Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE,
|
Error::VidCdbUnavailable => E_VID_CDB_UNAVAILABLE,
|
||||||
|
Error::DiscKeysCdbUnavailable => E_DISC_KEYS_CDB_UNAVAILABLE,
|
||||||
|
Error::DiscKeysSignatureInvalid => E_DISC_KEYS_SIGNATURE_INVALID,
|
||||||
Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
|
Error::KeydbConnect { .. } => E_KEYDB_CONNECT,
|
||||||
Error::KeydbHttp { .. } => E_KEYDB_HTTP,
|
Error::KeydbHttp { .. } => E_KEYDB_HTTP,
|
||||||
Error::KeydbInvalid => E_KEYDB_INVALID,
|
Error::KeydbInvalid => E_KEYDB_INVALID,
|
||||||
@@ -691,6 +703,20 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disc_keys_variants_codes_distinct_and_in_7xxx() {
|
||||||
|
let a = Error::DiscKeysCdbUnavailable.code();
|
||||||
|
let b = Error::DiscKeysSignatureInvalid.code();
|
||||||
|
assert_eq!(a, E_DISC_KEYS_CDB_UNAVAILABLE);
|
||||||
|
assert_eq!(b, E_DISC_KEYS_SIGNATURE_INVALID);
|
||||||
|
assert_ne!(a, b);
|
||||||
|
assert!((7000..8000).contains(&a));
|
||||||
|
assert!((7000..8000).contains(&b));
|
||||||
|
// Display falls through to the `_ =>` arm: just "E<code>".
|
||||||
|
assert_eq!(Error::DiscKeysCdbUnavailable.to_string(), format!("E{a}"));
|
||||||
|
assert_eq!(Error::DiscKeysSignatureInvalid.to_string(), format!("E{b}"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iokind_mapping_for_new_variants() {
|
fn iokind_mapping_for_new_variants() {
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|||||||
Reference in New Issue
Block a user