use zeroize::{Zeroize, Zeroizing};
#[derive(Clone)]
pub struct Redacted<T: Zeroize = String>(Zeroizing<T>);
impl<T: Zeroize> Redacted<T> {
pub fn new(value: T) -> Self {
Self(Zeroizing::new(value))
}
}
impl<T: Zeroize + AsRef<str>> Redacted<T> {
#[must_use]
pub fn expose_secret(&self) -> &str {
self.0.as_ref()
}
}
impl<T: Zeroize> std::fmt::Debug for Redacted<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Redacted(***)")
}
}
impl<T: Zeroize> std::fmt::Display for Redacted<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("***")
}
}
impl<T: Zeroize + PartialEq> PartialEq for Redacted<T> {
fn eq(&self, other: &Self) -> bool {
*self.0 == *other.0
}
}
impl<T: Zeroize + Eq> Eq for Redacted<T> {}
impl<T: Zeroize + std::hash::Hash> std::hash::Hash for Redacted<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(*self.0).hash(state);
}
}
impl<T: Zeroize> zeroize::ZeroizeOnDrop for Redacted<T> {}
#[cfg(test)]
mod tests {
use super::Redacted;
#[test]
fn debug_redacts() {
let secret = Redacted::new("hunter2".to_string());
assert_eq!(format!("{secret:?}"), "Redacted(***)");
}
#[test]
fn display_redacts() {
let secret = Redacted::new("hunter2".to_string());
assert_eq!(format!("{secret}"), "***");
}
#[test]
fn expose_secret_recovers_the_value() {
let secret = Redacted::new("hunter2".to_string());
assert_eq!(secret.expose_secret(), "hunter2");
}
#[test]
fn equality_compares_the_wrapped_value() {
assert_eq!(
Redacted::new("hunter2".to_string()),
Redacted::new("hunter2".to_string())
);
assert_ne!(
Redacted::new("hunter2".to_string()),
Redacted::new("other".to_string())
);
}
#[test]
fn embedding_in_a_debug_derive_still_redacts() {
#[derive(Debug)]
struct Wrapper {
token: Redacted,
}
let wrapper = Wrapper {
token: Redacted::new("hunter2".to_string()),
};
assert_eq!(wrapper.token.expose_secret(), "hunter2");
let debug_output = format!("{wrapper:?}");
assert!(debug_output.contains("Redacted(***)"), "{debug_output}");
assert!(!debug_output.contains("hunter2"), "{debug_output}");
}
#[test]
fn implements_zeroize_on_drop() {
fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
assert_zeroize_on_drop::<Redacted<String>>();
}
}