#![cfg_attr(test, allow(unsafe_code))]
use std::fmt;
use zeroize::Zeroizing;
pub struct Password(Zeroizing<Box<str>>);
impl Password {
#[must_use]
pub fn new(value: String) -> Self {
Self(Zeroizing::new(value.into_boxed_str()))
}
pub(crate) fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Password([REDACTED])")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_does_not_leak() {
let p = Password::new("super-secret".to_string());
let s = format!("{p:?}");
assert_eq!(s, "Password([REDACTED])");
assert!(!s.contains("super-secret"));
}
#[test]
fn expose_returns_inner() {
let p = Password::new("abc123".to_string());
assert_eq!(p.expose(), "abc123");
}
#[test]
fn zeroize_clears_buffer() {
use std::mem::ManuallyDrop;
use zeroize::Zeroize;
let mut p = ManuallyDrop::new(Password::new("ABCDEFGH".to_string()));
let len = p.0.len();
let ptr = p.0.as_ptr();
let before = unsafe { std::slice::from_raw_parts(ptr, len) };
assert_eq!(before, b"ABCDEFGH");
p.0.zeroize();
let after = unsafe { std::slice::from_raw_parts(ptr, len) };
assert!(
after.iter().all(|&b| b == 0),
"expected zeroized buffer, got {after:?}"
);
unsafe { ManuallyDrop::drop(&mut p) };
}
}