Skip to main content

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}