disc: redact Debug for AacsState/Key/HandshakeResult (test-guarded)
AacsState (public via Disc.aacs) and Key (the decrypt_with key-transport enum) are crate-root re-exported and carried VUK/unit/read-data keys + volume id on #[derive(Debug)]; HandshakeResult carried the VID + AACS 2.0 bus key. Manual Debug impls print shape only, guarded by red->green tests.
This commit is contained in:
+34
-1
@@ -7,7 +7,6 @@ use crate::udf;
|
||||
|
||||
/// Result of SCSI AACS handshake (ECDH authentication).
|
||||
/// Only available when scanning from a real drive, not ISO images.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct HandshakeResult {
|
||||
pub volume_id: [u8; 16],
|
||||
pub read_data_key: Option<[u8; 16]>,
|
||||
@@ -29,6 +28,19 @@ pub(super) struct HandshakeResult {
|
||||
pub drive_unlocked: bool,
|
||||
}
|
||||
|
||||
// Redacting `Debug`: `volume_id` and `read_data_key` (the AACS 2.0 bus key) are
|
||||
// secret; print only shape. Guarded by `handshake_result_debug_is_redacted`.
|
||||
impl std::fmt::Debug for HandshakeResult {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HandshakeResult")
|
||||
.field("volume_id", &"<redacted>")
|
||||
.field("read_data_key", &self.read_data_key.map(|_| "<redacted>"))
|
||||
.field("read_data_key_err", &self.read_data_key_err)
|
||||
.field("drive_unlocked", &self.drive_unlocked)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Single source of truth for "is AACS bus encryption gone for this scan?". The
|
||||
/// gate asks ONLY this — `if !removed { error }` — never enumerating cases. Bus
|
||||
/// encryption is gone when ANY of these holds:
|
||||
@@ -428,6 +440,27 @@ mod tests {
|
||||
use crate::sector::SectorSource;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// `HandshakeResult` carries the Volume ID and the AACS 2.0 bus (read-data)
|
||||
/// key; `Debug` must redact both. Sentinel 213 (0xD5).
|
||||
#[test]
|
||||
fn handshake_result_debug_is_redacted() {
|
||||
let hs = HandshakeResult {
|
||||
volume_id: [0xD5; 16],
|
||||
read_data_key: Some([0xD5; 16]),
|
||||
read_data_key_err: None,
|
||||
drive_unlocked: false,
|
||||
};
|
||||
let d = format!("{hs:?}");
|
||||
assert!(
|
||||
!d.contains("213"),
|
||||
"HandshakeResult leaked VID/bus key: {d}"
|
||||
);
|
||||
assert!(
|
||||
d.contains("redacted"),
|
||||
"HandshakeResult missing marker: {d}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// In-memory disc + minimal UDF image with a single physical
|
||||
// partition (metadata_start == partition_start). Offsets cited
|
||||
|
||||
+73
-2
@@ -1363,7 +1363,6 @@ impl DiscTitle {
|
||||
// ─── Encryption ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// AACS decryption state for a disc.
|
||||
#[derive(Debug)]
|
||||
pub struct AacsState {
|
||||
/// AACS version (1 or 2)
|
||||
pub version: u8,
|
||||
@@ -1394,6 +1393,28 @@ pub struct AacsState {
|
||||
pub mkb: Vec<u8>,
|
||||
}
|
||||
|
||||
// Redacting `Debug`: `AacsState` is crate-root re-exported and reachable via the
|
||||
// public `Disc.aacs` field; it carries VUK / unit keys / read-data (bus) key /
|
||||
// volume id / raw .inf + MKB. Print only non-secret shape; redact every
|
||||
// key/secret field. Guarded by `aacs_state_and_key_debug_are_redacted`.
|
||||
impl std::fmt::Debug for AacsState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AacsState")
|
||||
.field("version", &self.version)
|
||||
.field("bus_encryption", &self.bus_encryption)
|
||||
.field("mkb_version", &self.mkb_version)
|
||||
.field("disc_hash", &self.disc_hash)
|
||||
.field("key_source", &self.key_source)
|
||||
.field("vuk", &self.vuk.map(|_| "<redacted>"))
|
||||
.field("unit_keys_len", &self.unit_keys.len())
|
||||
.field("read_data_key", &self.read_data_key.map(|_| "<redacted>"))
|
||||
.field("volume_id", &"<redacted>")
|
||||
.field("uk_ro_len", &self.uk_ro.len())
|
||||
.field("mkb_len", &self.mkb.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// How AACS keys were resolved. Variants are ordered root-of-trust →
|
||||
/// per-disc-leaf, matching the resolver's path-try order: the resolver
|
||||
/// attempts derivation from the strongest input it has first and falls
|
||||
@@ -2244,7 +2265,7 @@ impl Disc {
|
||||
/// point at one level of that chain; [`Disc::decrypt_with`] derives down from
|
||||
/// it to the per-CPS unit keys. New levels can be added without breaking
|
||||
/// callers.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Key {
|
||||
/// Device key(s) (AACS DK, positioned). libfreemkv walks the MKB
|
||||
@@ -2273,6 +2294,22 @@ pub enum Key {
|
||||
Unit(Vec<(u32, [u8; 16])>),
|
||||
}
|
||||
|
||||
// Redacting `Debug`: `Key` is crate-root re-exported and is the key-transport
|
||||
// type crossing `Disc::decrypt_with`; every variant carries raw key material.
|
||||
// Print only the variant name and count — never bytes. Guarded by
|
||||
// `aacs_state_and_key_debug_are_redacted`.
|
||||
impl std::fmt::Debug for Key {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Key::Device(v) => write!(f, "Key::Device(<{} redacted>)", v.len()),
|
||||
Key::Processing(v) => write!(f, "Key::Processing(<{} redacted>)", v.len()),
|
||||
Key::Media(v) => write!(f, "Key::Media(<{} redacted>)", v.len()),
|
||||
Key::Volume(_) => f.write_str("Key::Volume(<redacted>)"),
|
||||
Key::Unit(v) => write!(f, "Key::Unit(<{} redacted>)", v.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True if `unit_keys` covers EVERY supplied scrambled content `sample` — the
|
||||
/// validation gate for [`Disc::decrypt_with`]. Conservative: a sample that is
|
||||
/// not AACS-scrambled proves nothing, and with no scrambled sample at all there
|
||||
@@ -4188,6 +4225,40 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `AacsState` (public via `Disc.aacs`) and `Key` (the key-transport enum)
|
||||
/// must never print raw key bytes on `{:?}`. Sentinel 213 (0xD5); non-secret
|
||||
/// fields below are not 213.
|
||||
#[test]
|
||||
fn aacs_state_and_key_debug_are_redacted() {
|
||||
let st = AacsState {
|
||||
version: 2,
|
||||
bus_encryption: true,
|
||||
mkb_version: Some(77),
|
||||
disc_hash: "0xAA".into(),
|
||||
key_source: KeyOrigin::ExternalUk,
|
||||
vuk: Some([0xD5; 16]),
|
||||
unit_keys: vec![(1, [0xD5; 16])],
|
||||
read_data_key: Some([0xD5; 16]),
|
||||
volume_id: [0xD5; 16],
|
||||
uk_ro: vec![1, 2, 3],
|
||||
mkb: vec![4, 5, 6],
|
||||
};
|
||||
let d = format!("{st:?}");
|
||||
assert!(!d.contains("213"), "AacsState leaked key bytes: {d}");
|
||||
assert!(d.contains("redacted"), "AacsState missing marker: {d}");
|
||||
|
||||
for k in [
|
||||
Key::Unit(vec![(1, [0xD5; 16])]),
|
||||
Key::Volume([0xD5; 16]),
|
||||
Key::Processing(vec![[0xD5; 16]]),
|
||||
Key::Media(vec![[0xD5; 16]]),
|
||||
] {
|
||||
let d = format!("{k:?}");
|
||||
assert!(!d.contains("213"), "Key leaked bytes: {d}");
|
||||
assert!(d.contains("redacted"), "Key missing marker: {d}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── encrypted-content map (`merged_extents` core) ────────────────────────
|
||||
|
||||
fn ext(start_lba: u32, sector_count: u32) -> Extent {
|
||||
|
||||
Reference in New Issue
Block a user