use std::fmt;
pub struct SecureString {
inner: Vec<u8>,
}
impl SecureString {
pub fn new(s: &str) -> Self {
Self {
inner: s.as_bytes().to_vec(),
}
}
pub fn from_string(s: String) -> Self {
Self {
inner: s.into_bytes(),
}
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.inner).unwrap_or("")
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl Drop for SecureString {
fn drop(&mut self) {
for byte in self.inner.iter_mut() {
unsafe {
std::ptr::write_volatile(byte, 0);
}
}
std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
self.inner.clear();
}
}
impl From<String> for SecureString {
fn from(s: String) -> Self {
Self::from_string(s)
}
}
impl Clone for SecureString {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl fmt::Debug for SecureString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecureString(***)")
}
}
impl fmt::Display for SecureString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "***")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secure_string_from_and_expose() {
let ss = SecureString::new("my-secret-token");
assert_eq!(ss.as_str(), "my-secret-token");
let ss2 = SecureString::from_string("another-token".to_string());
assert_eq!(ss2.as_str(), "another-token");
let ss3: SecureString = "from-trait".to_string().into();
assert_eq!(ss3.as_str(), "from-trait");
}
#[test]
fn test_secure_string_debug_masked() {
let ss = SecureString::new("super-secret-password-12345");
let debug_output = format!("{:?}", ss);
assert_eq!(debug_output, "SecureString(***)");
assert!(
!debug_output.contains("super-secret"),
"Debug output must not leak the secret value"
);
}
#[test]
fn test_secure_string_display_masked() {
let ss = SecureString::new("super-secret-password-12345");
let display_output = format!("{}", ss);
assert_eq!(display_output, "***");
assert!(
!display_output.contains("super-secret"),
"Display output must not leak the secret value"
);
}
#[test]
fn test_secure_string_clone_independent() {
let ss1 = SecureString::new("original-value");
let ss2 = ss1.clone();
assert_eq!(ss1.as_str(), "original-value");
assert_eq!(ss2.as_str(), "original-value");
drop(ss1);
assert_eq!(ss2.as_str(), "original-value");
}
#[test]
fn test_secure_string_empty() {
let ss = SecureString::new("");
assert_eq!(ss.as_str(), "");
assert!(ss.is_empty());
let ss2 = SecureString::from_string(String::new());
assert_eq!(ss2.as_str(), "");
assert!(ss2.is_empty());
}
#[test]
fn test_secure_string_long_value() {
let long_value: String = "A".repeat(10_240);
let ss = SecureString::new(&long_value);
assert_eq!(ss.as_str(), long_value.as_str());
assert_eq!(ss.as_str().len(), 10_240);
assert!(!ss.is_empty());
}
#[test]
fn test_secure_string_special_chars() {
let ss_unicode = SecureString::new("p\u{00e4}ssw\u{00f6}rd-\u{1f512}");
assert_eq!(ss_unicode.as_str(), "p\u{00e4}ssw\u{00f6}rd-\u{1f512}");
let ss_control = SecureString::new("token\twith\nnewlines\r\nand\ttabs");
assert_eq!(ss_control.as_str(), "token\twith\nnewlines\r\nand\ttabs");
let ss_mixed = SecureString::new("ghp_\u{00e9}\u{00e8}\u{00ea}!@#$%^&*()");
assert_eq!(ss_mixed.as_str(), "ghp_\u{00e9}\u{00e8}\u{00ea}!@#$%^&*()");
}
}