87 lines
2.3 KiB
Rust
87 lines
2.3 KiB
Rust
use zeroize::Zeroize;
|
|
|
|
/// A wrapper for sensitive strings (passwords, tokens) that prevents accidental logging.
|
|
/// - Debug and Display show "[REDACTED]" instead of the actual value
|
|
/// - The inner value is zeroized on drop using volatile writes (cannot be optimized away)
|
|
#[derive(Clone)]
|
|
pub struct SecretString(String);
|
|
|
|
impl SecretString {
|
|
pub fn new(value: impl Into<String>) -> Self {
|
|
Self(value.into())
|
|
}
|
|
|
|
/// Access the secret value. Use sparingly and never log the result.
|
|
pub fn expose(&self) -> &str {
|
|
&self.0
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for SecretString {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "[REDACTED]")
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SecretString {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "[REDACTED]")
|
|
}
|
|
}
|
|
|
|
impl Drop for SecretString {
|
|
fn drop(&mut self) {
|
|
self.0.zeroize();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_secret_string_expose() {
|
|
let secret = SecretString::new("my_password");
|
|
assert_eq!(secret.expose(), "my_password");
|
|
}
|
|
|
|
#[test]
|
|
fn test_secret_string_debug_is_redacted() {
|
|
let secret = SecretString::new("my_password");
|
|
let debug_output = format!("{:?}", secret);
|
|
assert_eq!(debug_output, "[REDACTED]");
|
|
assert!(!debug_output.contains("my_password"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_secret_string_display_is_redacted() {
|
|
let secret = SecretString::new("my_password");
|
|
let display_output = format!("{}", secret);
|
|
assert_eq!(display_output, "[REDACTED]");
|
|
assert!(!display_output.contains("my_password"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_secret_string_clone() {
|
|
let secret = SecretString::new("my_password");
|
|
let cloned = secret.clone();
|
|
assert_eq!(cloned.expose(), "my_password");
|
|
}
|
|
|
|
#[test]
|
|
fn test_secret_string_empty() {
|
|
let secret = SecretString::new("");
|
|
assert!(secret.is_empty());
|
|
assert!(secret.expose().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_secret_string_is_empty() {
|
|
let secret = SecretString::new("not_empty");
|
|
assert!(!secret.is_empty());
|
|
}
|
|
}
|