use std::fmt;
use serde::{Deserialize, Deserializer};
use zeroize::Zeroizing;
#[derive(Clone)]
pub struct SecretString {
inner: Zeroizing<String>,
}
pub const REDACTED: &str = "<redactado>";
impl SecretString {
pub fn new(value: String) -> Self {
Self {
inner: Zeroizing::new(value),
}
}
pub fn expose(&self) -> &str {
self.inner.as_str()
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("SecretString").field(&REDACTED).finish()
}
}
impl fmt::Display for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(REDACTED)
}
}
impl<'de> Deserialize<'de> for SecretString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let valor = String::deserialize(deserializer)?;
Ok(Self::new(valor))
}
}
#[cfg(test)]
mod tests {
use super::SecretString;
const VALOR: &str = "s3cr3t-muy-largo-y-distinguible";
#[test]
fn debug_no_revela_el_secreto() {
let secreto = SecretString::new(VALOR.to_owned());
let salida = format!("{secreto:?}");
assert!(!salida.contains(VALOR), "Debug publicó el valor: {salida}");
assert!(
salida.to_lowercase().contains("redact"),
"Debug no indica que el valor está redactado: {salida}"
);
}
#[test]
fn display_no_revela_el_secreto() {
let secreto = SecretString::new(VALOR.to_owned());
let salida = format!("{secreto}");
assert!(
!salida.contains(VALOR),
"Display publicó el valor: {salida}"
);
}
#[test]
fn el_valor_solo_sale_por_el_metodo_dedicado() {
let secreto = SecretString::new(VALOR.to_owned());
assert_eq!(secreto.expose(), VALOR);
}
}