aacs/types: redact Debug for all key-bearing types (test-guarded)
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).
This commit is contained in:
+163
-8
@@ -6,7 +6,7 @@
|
|||||||
//! owns only the crypto and these value types that flow through it.
|
//! owns only the crypto and these value types that flow through it.
|
||||||
|
|
||||||
/// A device key for MKB subset-difference tree processing.
|
/// A device key for MKB subset-difference tree processing.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DeviceKey {
|
pub struct DeviceKey {
|
||||||
pub key: [u8; 16],
|
pub key: [u8; 16],
|
||||||
pub node: u16,
|
pub node: u16,
|
||||||
@@ -15,7 +15,7 @@ pub struct DeviceKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Host certificate + private key for AACS SCSI authentication.
|
/// Host certificate + private key for AACS SCSI authentication.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HostCert {
|
pub struct HostCert {
|
||||||
/// AACS 1.0: 20 bytes. AACS 2.0: 32 bytes.
|
/// AACS 1.0: 20 bytes. AACS 2.0: 32 bytes.
|
||||||
pub private_key: [u8; 20],
|
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.
|
/// 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]);
|
pub struct Vid(pub [u8; 16]);
|
||||||
|
|
||||||
/// Media Key (Km, 16 bytes) — the MKB-scoped key derived from device keys.
|
/// 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]);
|
pub struct MediaKey(pub [u8; 16]);
|
||||||
|
|
||||||
/// Volume Unique Key (VUK / Kvu, 16 bytes) — derived from `MediaKey` + `Vid`,
|
/// Volume Unique Key (VUK / Kvu, 16 bytes) — derived from `MediaKey` + `Vid`,
|
||||||
/// decrypts the per-disc encrypted title keys in `Unit_Key_RO.inf`.
|
/// 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]);
|
pub struct Vuk(pub [u8; 16]);
|
||||||
|
|
||||||
/// Processing Key (Kp, 16 bytes) — an MKB Subset-Difference key that yields the
|
/// 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
|
/// Media Key. A leaked/precomputed PK in the keydb, or the intermediate PK a
|
||||||
/// device-key walk derives at its matching SD node.
|
/// 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]);
|
pub struct ProcessingKey(pub [u8; 16]);
|
||||||
|
|
||||||
/// One decrypted per-CPS-unit AACS title key.
|
/// 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
|
/// 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
|
/// [`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.
|
/// 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 struct UnitKey {
|
||||||
pub idx: u32,
|
pub idx: u32,
|
||||||
pub key: [u8; 16],
|
pub key: [u8; 16],
|
||||||
@@ -95,7 +95,7 @@ impl UnitKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A per-disc entry from the key database.
|
/// A per-disc entry from the key database.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DiscEntry {
|
pub struct DiscEntry {
|
||||||
/// Disc hash (20 bytes, hex)
|
/// Disc hash (20 bytes, hex)
|
||||||
pub disc_hash: String,
|
pub disc_hash: String,
|
||||||
@@ -110,3 +110,158 @@ pub struct DiscEntry {
|
|||||||
/// Unit keys (title keys) indexed by CPS unit number
|
/// Unit keys (title keys) indexed by CPS unit number
|
||||||
pub unit_keys: Vec<(u32, [u8; 16])>,
|
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", &"<redacted>")
|
||||||
|
.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", &"<redacted>")
|
||||||
|
.field("certificate_len", &self.certificate.len())
|
||||||
|
.field("private_key_v2", &self.private_key_v2.map(|_| "<redacted>"))
|
||||||
|
.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(<redacted>)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for MediaKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("MediaKey(<redacted>)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Vuk {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("Vuk(<redacted>)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ProcessingKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("ProcessingKey(<redacted>)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", &"<redacted>")
|
||||||
|
.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(|_| "<redacted>"))
|
||||||
|
.field("disc_id", &self.disc_id.map(|_| "<redacted>"))
|
||||||
|
.field("vuk", &self.vuk.map(|_| "<redacted>"))
|
||||||
|
.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:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user