From e308c5b825a7420fdb656794ad25f8517d13b3ac Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:56:38 -0700 Subject: [PATCH] aacs/types: redact Debug for all key-bearing types (test-guarded) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviceKey/HostCert/Vid/MediaKey/Vuk/ProcessingKey/UnitKey/DiscEntry carried key material on #[derive(Debug)] — a stray {:?} would leak device/host-private/media/ volume/unit keys. Manual Debug impls print shape only. redaction_tests asserts no key bytes appear and a redaction marker is present (fails if a derive returns). --- src/aacs/types.rs | 171 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 8 deletions(-) diff --git a/src/aacs/types.rs b/src/aacs/types.rs index 19400a2..2195b69 100644 --- a/src/aacs/types.rs +++ b/src/aacs/types.rs @@ -6,7 +6,7 @@ //! owns only the crypto and these value types that flow through it. /// A device key for MKB subset-difference tree processing. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct DeviceKey { pub key: [u8; 16], pub node: u16, @@ -15,7 +15,7 @@ pub struct DeviceKey { } /// Host certificate + private key for AACS SCSI authentication. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct HostCert { /// AACS 1.0: 20 bytes. AACS 2.0: 32 bytes. pub private_key: [u8; 20], @@ -28,22 +28,22 @@ pub struct HostCert { } /// Volume ID (16 bytes) — read from the disc via the SCSI handshake / OEM path. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct Vid(pub [u8; 16]); /// Media Key (Km, 16 bytes) — the MKB-scoped key derived from device keys. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct MediaKey(pub [u8; 16]); /// Volume Unique Key (VUK / Kvu, 16 bytes) — derived from `MediaKey` + `Vid`, /// decrypts the per-disc encrypted title keys in `Unit_Key_RO.inf`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct Vuk(pub [u8; 16]); /// Processing Key (Kp, 16 bytes) — an MKB Subset-Difference key that yields the /// Media Key. A leaked/precomputed PK in the keydb, or the intermediate PK a /// device-key walk derives at its matching SD node. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct ProcessingKey(pub [u8; 16]); /// One decrypted per-CPS-unit AACS title key. @@ -53,7 +53,7 @@ pub struct ProcessingKey(pub [u8; 16]); /// area). The CPS-unit *number* association is a higher-level concern owned by /// [`super::inf::parse_unit_key_ro`], which pairs each positional key with its /// declared CPS unit; this primitive only does the AES, so it surfaces position. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct UnitKey { pub idx: u32, pub key: [u8; 16], @@ -95,7 +95,7 @@ impl UnitKey { } /// A per-disc entry from the key database. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct DiscEntry { /// Disc hash (20 bytes, hex) pub disc_hash: String, @@ -110,3 +110,158 @@ pub struct DiscEntry { /// Unit keys (title keys) indexed by CPS unit number pub unit_keys: Vec<(u32, [u8; 16])>, } + +// ── Redacting `Debug` impls ────────────────────────────────────────────────── +// +// Every type above carries AACS secret material (device keys, host PRIVATE keys, +// media/volume/processing/unit keys). `#[derive(Debug)]` would print those bytes +// verbatim, so a stray `debug!("{:?}", …)` or a panic message would leak the +// keys. These hand-written impls print only NON-secret shape (presence, lengths, +// tree coordinates, indices) — never key bytes. `decrypt::DecryptKeys` follows +// the same policy by omitting `Debug` entirely; here we keep `Debug` because +// these are `PartialEq`/`Eq` value types used in `assert_eq!` and nested inside +// other `#[derive(Debug)]` structs, so the trait must exist — just not leak. +// Guarded by `redaction_tests` below. + +impl std::fmt::Debug for DeviceKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeviceKey") + .field("key", &"") + .field("node", &self.node) + .field("uv", &self.uv) + .field("u_mask_shift", &self.u_mask_shift) + .finish() + } +} + +impl std::fmt::Debug for HostCert { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HostCert") + .field("private_key", &"") + .field("certificate_len", &self.certificate.len()) + .field("private_key_v2", &self.private_key_v2.map(|_| "")) + .field( + "certificate_v2_len", + &self.certificate_v2.as_ref().map(|c| c.len()), + ) + .finish() + } +} + +impl std::fmt::Debug for Vid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Vid()") + } +} + +impl std::fmt::Debug for MediaKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("MediaKey()") + } +} + +impl std::fmt::Debug for Vuk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Vuk()") + } +} + +impl std::fmt::Debug for ProcessingKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ProcessingKey()") + } +} + +impl std::fmt::Debug for UnitKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UnitKey") + .field("idx", &self.idx) + .field("key", &"") + .field("index_number", &self.index_number) + .finish() + } +} + +impl std::fmt::Debug for DiscEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DiscEntry") + .field("disc_hash", &self.disc_hash) + .field("title", &self.title) + .field("media_key", &self.media_key.map(|_| "")) + .field("disc_id", &self.disc_id.map(|_| "")) + .field("vuk", &self.vuk.map(|_| "")) + .field("unit_keys_len", &self.unit_keys.len()) + .finish() + } +} + +#[cfg(test)] +mod redaction_tests { + use super::*; + + // Sentinel key byte 0xD5 = decimal 213. A derived `Debug` prints `[u8;N]` + // as decimal, so a leaked key surfaces the substring "213"; the redacting + // impls must not. No non-secret field below is 213, so "213" appearing means + // key bytes leaked. Each type must also carry a "redacted" marker (or omit + // the secret entirely) so re-adding `#[derive(Debug)]` fails this test. + const S: u8 = 0xD5; + + fn assert_redacted(what: &str, dbg: &str) { + assert!( + !dbg.contains("213"), + "{what}: Debug leaked key bytes (found decimal 213): {dbg}" + ); + assert!( + dbg.contains("redacted"), + "{what}: Debug missing redaction marker: {dbg}" + ); + } + + #[test] + fn device_key_debug_is_redacted() { + let d = DeviceKey { + key: [S; 16], + node: 1, + uv: 2, + u_mask_shift: 3, + }; + assert_redacted("DeviceKey", &format!("{d:?}")); + } + + #[test] + fn host_cert_debug_is_redacted() { + let h = HostCert { + private_key: [S; 20], + certificate: vec![0u8; 92], + private_key_v2: Some([S; 32]), + certificate_v2: None, + }; + assert_redacted("HostCert", &format!("{h:?}")); + } + + #[test] + fn newtype_keys_debug_is_redacted() { + assert_redacted("Vid", &format!("{:?}", Vid([S; 16]))); + assert_redacted("MediaKey", &format!("{:?}", MediaKey([S; 16]))); + assert_redacted("Vuk", &format!("{:?}", Vuk([S; 16]))); + assert_redacted("ProcessingKey", &format!("{:?}", ProcessingKey([S; 16]))); + } + + #[test] + fn unit_key_debug_is_redacted() { + assert_redacted("UnitKey", &format!("{:?}", UnitKey::new(0, [S; 16]))); + } + + #[test] + fn disc_entry_debug_is_redacted() { + let e = DiscEntry { + disc_hash: "0xAA".into(), + title: "T".into(), + media_key: Some([S; 16]), + disc_id: Some([S; 16]), + vuk: Some([S; 16]), + unit_keys: vec![(1, [S; 16])], + }; + assert_redacted("DiscEntry", &format!("{e:?}")); + } +}