bibeam_core/redaction.rs
1#![forbid(unsafe_code)]
2//! Keyed-BLAKE3 redaction helpers for PII surfaces (peer IDs, IP addresses).
3//!
4//! Log output and metrics labels must never carry raw peer identifiers or IP
5//! addresses. This module derives a short, opaque hex token from each value
6//! using a per-node [`RedactionKey`] so:
7//!
8//! - the same input under the same key always produces the same token
9//! (operators can still correlate events for one peer across log lines), and
10//! - the token reveals nothing about the input without the key.
11//!
12//! The key is 32 bytes — exactly BLAKE3's keyed-hash key size — and is
13//! held in a [`ZeroizeOnDrop`] wrapper so it gets wiped from memory on
14//! drop.
15
16use core::net::IpAddr;
17
18use zeroize::ZeroizeOnDrop;
19
20use crate::ids::PeerId;
21
22/// Number of leading hex characters returned by every redaction helper.
23///
24/// 16 hex characters = 64 bits of the BLAKE3 output, which is enough to keep
25/// collisions vanishingly unlikely while remaining readable in log lines.
26const REDACTED_HEX_LEN: usize = 16;
27
28/// 32-byte key for BLAKE3 keyed-hash PII redaction.
29///
30/// Each node should hold one [`RedactionKey`] for the lifetime of its
31/// process. The key MUST stay private — anyone with the key can replay the
32/// hash and de-anonymise any token this module emits.
33#[derive(Clone, ZeroizeOnDrop)]
34pub struct RedactionKey([u8; 32]);
35
36impl core::fmt::Debug for RedactionKey {
37 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 // Never print key material — even on a panic path.
39 formatter.debug_struct("RedactionKey").finish_non_exhaustive()
40 }
41}
42
43impl RedactionKey {
44 /// Construct a [`RedactionKey`] from a 32-byte buffer.
45 ///
46 /// The buffer should come from a CSPRNG; callers SHOULD NOT derive it
47 /// from any low-entropy source.
48 #[must_use]
49 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
50 Self(bytes)
51 }
52
53 /// Borrow the underlying 32 bytes.
54 ///
55 /// Intended for one place only — passing the key into BLAKE3. Callers
56 /// outside this module should not need this.
57 const fn as_bytes(&self) -> &[u8; 32] {
58 &self.0
59 }
60}
61
62/// Hash `input` under `key` and return its first [`REDACTED_HEX_LEN`]
63/// lowercase hex characters.
64fn redact_bytes(key: &RedactionKey, input: &[u8]) -> String {
65 let digest = blake3::keyed_hash(key.as_bytes(), input);
66 let hex = digest.to_hex();
67 hex[..REDACTED_HEX_LEN].to_owned()
68}
69
70/// Produce a redacted token for the given [`PeerId`].
71///
72/// Equivalent to `BLAKE3-keyed(peer_id.as_bytes())` truncated to 16 hex
73/// characters.
74#[must_use]
75pub fn redact_peer_id(key: &RedactionKey, peer_id: &PeerId) -> String {
76 let bytes = peer_id.into_ulid().to_bytes();
77 redact_bytes(key, &bytes)
78}
79
80/// Produce a redacted token for the given IP address.
81///
82/// Hashes the textual form (e.g. `203.0.113.4` or
83/// `2001:db8::1`) so v4 and v6 share the same code path.
84#[must_use]
85pub fn redact_ip(key: &RedactionKey, ip: IpAddr) -> String {
86 redact_bytes(key, ip.to_string().as_bytes())
87}