use zeroize::Zeroizing;
#[derive(Clone)]
pub struct Password(Zeroizing<Vec<u8>>);
impl Password {
pub fn new(bytes: impl AsRef<[u8]>) -> Self {
Self(Zeroizing::new(bytes.as_ref().to_vec()))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[cfg(feature = "password-strength")]
pub fn strength(&self) -> zxcvbn::Entropy {
let s = std::str::from_utf8(&self.0).unwrap_or("");
zxcvbn::zxcvbn(s, &[])
}
}
impl From<&str> for Password {
fn from(s: &str) -> Self {
Self::new(s.as_bytes())
}
}
impl From<String> for Password {
fn from(s: String) -> Self {
Self(Zeroizing::new(s.into_bytes()))
}
}
impl From<&[u8]> for Password {
fn from(bytes: &[u8]) -> Self {
Self::new(bytes)
}
}
impl From<Vec<u8>> for Password {
fn from(bytes: Vec<u8>) -> Self {
Self(Zeroizing::new(bytes))
}
}
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Password({} bytes)", self.0.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_construction() {
let p = Password::from("hello");
assert_eq!(p.as_bytes(), b"hello");
assert_eq!(p.len(), 5);
assert!(!p.is_empty());
}
#[test]
fn debug_does_not_leak() {
let p = Password::from("supersecret");
let s = format!("{:?}", p);
assert!(!s.contains("supersecret"));
assert!(s.contains("11 bytes"));
}
#[test]
fn empty_password_allowed() {
let p = Password::from("");
assert!(p.is_empty());
assert_eq!(p.len(), 0);
}
#[test]
fn clone_independent() {
let p1 = Password::from("abc");
let p2 = p1.clone();
assert_eq!(p1.as_bytes(), p2.as_bytes());
}
#[test]
fn all_from_impls_preserve_bytes() {
let expected = b"correct horse";
assert_eq!(Password::from("correct horse").as_bytes(), expected);
assert_eq!(
Password::from(String::from("correct horse")).as_bytes(),
expected
);
assert_eq!(
Password::from(b"correct horse".as_slice()).as_bytes(),
expected
);
assert_eq!(
Password::from(b"correct horse".to_vec()).as_bytes(),
expected
);
let raw = vec![0xFF, 0x00, 0xFE];
assert_eq!(Password::from(raw.clone()).as_bytes(), raw.as_slice());
assert_eq!(Password::from(raw.as_slice()).as_bytes(), raw.as_slice());
}
#[cfg(feature = "password-strength")]
#[test]
fn strength_discriminates_and_tolerates_non_utf8() {
let weak = Password::from("aaaaaaaa");
let strong = Password::from("9!Kp$3vQ_z@Wm2#L");
assert!(weak.strength().score() < strong.strength().score());
let non_utf8 = Password::from(vec![0xFF, 0xFE, 0xFD]);
let _ = non_utf8.strength(); }
}