diff --git a/src/aacs/derive.rs b/src/aacs/derive.rs index b0f9c37..ac436a7 100644 --- a/src/aacs/derive.rs +++ b/src/aacs/derive.rs @@ -595,7 +595,10 @@ mod resolve_candidate_tests { dk: None, }; let dbg = format!("{c:?}"); - assert!(!dbg.contains("213"), "ResolvedChain leaked unit keys: {dbg}"); + assert!( + !dbg.contains("213"), + "ResolvedChain leaked unit keys: {dbg}" + ); assert!( dbg.contains("unit_keys_len"), "ResolvedChain missing redaction: {dbg}" diff --git a/src/aacs/resolve.rs b/src/aacs/resolve.rs index a02ccf2..1009d42 100644 --- a/src/aacs/resolve.rs +++ b/src/aacs/resolve.rs @@ -498,7 +498,10 @@ mod tests { }; let dbg = format!("{rk:?}"); 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 diff --git a/src/aacs/variant.rs b/src/aacs/variant.rs index a42eff1..b5b8eaf 100644 --- a/src/aacs/variant.rs +++ b/src/aacs/variant.rs @@ -660,8 +660,14 @@ mod tests { cvalue_index: 2, }; let dbg = format!("{m:?}"); - assert!(!dbg.contains("213"), "ProcessingKeyMatch leaked kp/cvalue: {dbg}"); - assert!(dbg.contains("redacted"), "ProcessingKeyMatch missing marker: {dbg}"); + assert!( + !dbg.contains("213"), + "ProcessingKeyMatch leaked kp/cvalue: {dbg}" + ); + assert!( + dbg.contains("redacted"), + "ProcessingKeyMatch missing marker: {dbg}" + ); } #[test] diff --git a/src/css/mod.rs b/src/css/mod.rs index 44ab12d..49448d0 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -390,7 +390,10 @@ mod tests { !dbg.contains("213"), "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 ─────────────────────────────────────────────────────── diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index 633b53c..4fa1b30 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -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", &"") + .field("read_data_key", &self.read_data_key.map(|_| "")) + .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 diff --git a/src/disc/mod.rs b/src/disc/mod.rs index d208623..bca3d8f 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -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, } +// 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(|_| "")) + .field("unit_keys_len", &self.unit_keys.len()) + .field("read_data_key", &self.read_data_key.map(|_| "")) + .field("volume_id", &"") + .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()"), + 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 {