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:
+4
-1
@@ -595,7 +595,10 @@ mod resolve_candidate_tests {
|
|||||||
dk: None,
|
dk: None,
|
||||||
};
|
};
|
||||||
let dbg = format!("{c:?}");
|
let dbg = format!("{c:?}");
|
||||||
assert!(!dbg.contains("213"), "ResolvedChain leaked unit keys: {dbg}");
|
assert!(
|
||||||
|
!dbg.contains("213"),
|
||||||
|
"ResolvedChain leaked unit keys: {dbg}"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
dbg.contains("unit_keys_len"),
|
dbg.contains("unit_keys_len"),
|
||||||
"ResolvedChain missing redaction: {dbg}"
|
"ResolvedChain missing redaction: {dbg}"
|
||||||
|
|||||||
+4
-1
@@ -498,7 +498,10 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let dbg = format!("{rk:?}");
|
let dbg = format!("{rk:?}");
|
||||||
assert!(!dbg.contains("213"), "ResolvedKeys leaked keys: {dbg}");
|
assert!(!dbg.contains("213"), "ResolvedKeys leaked keys: {dbg}");
|
||||||
assert!(dbg.contains("redacted"), "ResolvedKeys missing marker: {dbg}");
|
assert!(
|
||||||
|
dbg.contains("redacted"),
|
||||||
|
"ResolvedKeys missing marker: {dbg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
/// Audit #5: the `major` / `from_major` mapping is load-bearing for the
|
||||||
|
|||||||
+8
-2
@@ -660,8 +660,14 @@ mod tests {
|
|||||||
cvalue_index: 2,
|
cvalue_index: 2,
|
||||||
};
|
};
|
||||||
let dbg = format!("{m:?}");
|
let dbg = format!("{m:?}");
|
||||||
assert!(!dbg.contains("213"), "ProcessingKeyMatch leaked kp/cvalue: {dbg}");
|
assert!(
|
||||||
assert!(dbg.contains("redacted"), "ProcessingKeyMatch missing marker: {dbg}");
|
!dbg.contains("213"),
|
||||||
|
"ProcessingKeyMatch leaked kp/cvalue: {dbg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
dbg.contains("redacted"),
|
||||||
|
"ProcessingKeyMatch missing marker: {dbg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+4
-1
@@ -390,7 +390,10 @@ mod tests {
|
|||||||
!dbg.contains("213"),
|
!dbg.contains("213"),
|
||||||
"CssState Debug leaked the title key: {dbg}"
|
"CssState Debug leaked the title key: {dbg}"
|
||||||
);
|
);
|
||||||
assert!(dbg.contains("redacted"), "CssState Debug missing marker: {dbg}");
|
assert!(
|
||||||
|
dbg.contains("redacted"),
|
||||||
|
"CssState Debug missing marker: {dbg}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── is_scrambled ───────────────────────────────────────────────────────
|
// ── is_scrambled ───────────────────────────────────────────────────────
|
||||||
|
|||||||
+34
-1
@@ -7,7 +7,6 @@ use crate::udf;
|
|||||||
|
|
||||||
/// 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)]
|
|
||||||
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]>,
|
||||||
@@ -29,6 +28,19 @@ pub(super) struct HandshakeResult {
|
|||||||
pub drive_unlocked: bool,
|
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
|
/// 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
|
/// gate asks ONLY this — `if !removed { error }` — never enumerating cases. Bus
|
||||||
/// encryption is gone when ANY of these holds:
|
/// encryption is gone when ANY of these holds:
|
||||||
@@ -428,6 +440,27 @@ mod tests {
|
|||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
use std::collections::HashMap;
|
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
|
// In-memory disc + minimal UDF image with a single physical
|
||||||
// partition (metadata_start == partition_start). Offsets cited
|
// partition (metadata_start == partition_start). Offsets cited
|
||||||
|
|||||||
+73
-2
@@ -1363,7 +1363,6 @@ impl DiscTitle {
|
|||||||
// ─── Encryption ─────────────────────────────────────────────────────────────
|
// ─── Encryption ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// AACS decryption state for a disc.
|
/// AACS decryption state for a disc.
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AacsState {
|
pub struct AacsState {
|
||||||
/// AACS version (1 or 2)
|
/// AACS version (1 or 2)
|
||||||
pub version: u8,
|
pub version: u8,
|
||||||
@@ -1394,6 +1393,28 @@ pub struct AacsState {
|
|||||||
pub mkb: Vec<u8>,
|
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 →
|
/// How AACS keys were resolved. Variants are ordered root-of-trust →
|
||||||
/// per-disc-leaf, matching the resolver's path-try order: the resolver
|
/// per-disc-leaf, matching the resolver's path-try order: the resolver
|
||||||
/// attempts derivation from the strongest input it has first and falls
|
/// 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
|
/// 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
|
/// it to the per-CPS unit keys. New levels can be added without breaking
|
||||||
/// callers.
|
/// callers.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
/// Device key(s) (AACS DK, positioned). libfreemkv walks the MKB
|
/// Device key(s) (AACS DK, positioned). libfreemkv walks the MKB
|
||||||
@@ -2273,6 +2294,22 @@ pub enum Key {
|
|||||||
Unit(Vec<(u32, [u8; 16])>),
|
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
|
/// True if `unit_keys` covers EVERY supplied scrambled content `sample` — the
|
||||||
/// validation gate for [`Disc::decrypt_with`]. Conservative: a sample that is
|
/// validation gate for [`Disc::decrypt_with`]. Conservative: a sample that is
|
||||||
/// not AACS-scrambled proves nothing, and with no scrambled sample at all there
|
/// 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 {
|
mod tests {
|
||||||
use super::*;
|
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) ────────────────────────
|
// ── encrypted-content map (`merged_extents` core) ────────────────────────
|
||||||
|
|
||||||
fn ext(start_lba: u32, sector_count: u32) -> Extent {
|
fn ext(start_lba: u32, sector_count: u32) -> Extent {
|
||||||
|
|||||||
Reference in New Issue
Block a user