use std::fmt;
use zeroize::Zeroizing;
#[derive(Clone)]
pub struct Secret(Zeroizing<String>);
impl Secret {
pub fn new(value: impl Into<String>) -> Self {
Self(Zeroizing::new(value.into()))
}
#[must_use]
pub fn expose(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for Secret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Secret(<redacted>)")
}
}
impl PartialEq for Secret {
fn eq(&self, other: &Self) -> bool {
let (a, b) = (self.0.as_bytes(), other.0.as_bytes());
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
}
impl Eq for Secret {}
impl From<String> for Secret {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for Secret {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_secret_does_not_print_itself() {
let s = Secret::new("sk-live-abcdef");
assert_eq!(format!("{s:?}"), "Secret(<redacted>)");
assert!(!format!("{s:#?}").contains("abcdef"));
}
#[test]
fn equality_still_works() {
assert_eq!(Secret::new("a"), Secret::new("a"));
assert_ne!(Secret::new("a"), Secret::new("b"));
assert_ne!(Secret::new("a"), Secret::new("aa"));
}
#[test]
fn exposing_gives_the_value() {
assert_eq!(Secret::new("k").expose(), "k");
}
}