use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use std::sync::atomic::{AtomicUsize, Ordering};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub trait SensitiveData {
fn display_name(&self) -> &str;
fn is_highly_sensitive(&self) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SensitivityLevel {
#[default]
Low,
Medium,
High,
Critical,
}
impl SensitivityLevel {
pub fn is_critical_or_high(&self) -> bool {
matches!(self, SensitivityLevel::Critical | SensitivityLevel::High)
}
}
static ALLOCATED_SECURE_STRINGS: AtomicUsize = AtomicUsize::new(0);
static DEALLOCATED_SECURE_STRINGS: AtomicUsize = AtomicUsize::new(0);
pub fn allocated_secure_strings() -> usize {
ALLOCATED_SECURE_STRINGS.load(Ordering::SeqCst)
}
pub fn deallocated_secure_strings() -> usize {
DEALLOCATED_SECURE_STRINGS.load(Ordering::SeqCst)
}
#[allow(dead_code)]
pub(crate) fn reset_secure_string_counters() {
ALLOCATED_SECURE_STRINGS.store(0, Ordering::SeqCst);
DEALLOCATED_SECURE_STRINGS.store(0, Ordering::SeqCst);
}
#[derive(Eq)]
pub struct SecureString {
data: Vec<u8>,
sensitivity: SensitivityLevel,
display_name: String,
}
impl SecureString {
pub fn new(s: impl Into<String>, sensitivity: SensitivityLevel) -> Self {
let string = s.into();
ALLOCATED_SECURE_STRINGS.fetch_add(1, Ordering::SeqCst);
let display_name = match sensitivity {
SensitivityLevel::Critical => "[SENSITIVE]".to_string(),
SensitivityLevel::High => format!("[{} chars]", string.len()),
SensitivityLevel::Medium => format!("[{} chars]", string.len()),
SensitivityLevel::Low => string.clone(),
};
let data = string.into_bytes();
Self {
data,
sensitivity,
display_name,
}
}
pub fn from(s: impl Into<String>) -> Self {
Self::new(s, SensitivityLevel::Critical)
}
pub fn from_bytes(data: Vec<u8>, sensitivity: SensitivityLevel) -> Self {
ALLOCATED_SECURE_STRINGS.fetch_add(1, Ordering::SeqCst);
Self {
data,
sensitivity,
display_name: "[binary data]".to_string(),
}
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.data).unwrap_or("[invalid utf-8]")
}
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
#[allow(clippy::wrong_self_convention)] pub fn to_plain_string(self) -> String {
String::from_utf8(self.data.clone()).unwrap_or_default()
}
#[allow(clippy::result_unit_err)]
pub fn compare(&self, other: &str) -> Result<(), ()> {
if self.data.len() != other.len() {
return Err(());
}
let other_bytes = other.as_bytes();
let mut result: u8 = 0;
for (a, b) in self.data.iter().zip(other_bytes) {
result |= a ^ b;
}
if result == 0 {
Ok(())
} else {
Err(())
}
}
pub fn sensitivity(&self) -> SensitivityLevel {
self.sensitivity.clone()
}
pub fn is_highly_sensitive(&self) -> bool {
self.sensitivity.is_critical_or_high()
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn zeroize(&mut self) {
self.data.zeroize();
}
pub fn fingerprint(&self, max_len: usize) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&self.data);
let result = hasher.finalize();
let hex = hex::encode(result);
if hex.len() > max_len {
hex[..max_len].to_string()
} else {
hex
}
}
pub fn masked(&self) -> String {
if self.data.is_empty() {
return "[empty]".to_string();
}
let s = self.as_str();
let char_count = s.chars().count();
match char_count {
0 => "[empty]".to_string(),
1..=2 => "*".repeat(char_count),
3..=4 => {
let visible = if char_count == 3 { 1 } else { 2 };
let prefix: String = s.chars().take(visible).collect();
format!("{}{}", prefix, "*".repeat(char_count - visible))
}
_ => {
let visible = std::cmp::min(2, char_count / 4);
let masked_chars = std::cmp::min(6, char_count - visible);
let prefix: String = s.chars().take(visible).collect();
format!("{}{}", prefix, "*".repeat(masked_chars))
}
}
}
}
impl SensitiveData for SecureString {
fn display_name(&self) -> &str {
&self.display_name
}
fn is_highly_sensitive(&self) -> bool {
self.is_highly_sensitive()
}
}
impl Drop for SecureString {
fn drop(&mut self) {
self.data.zeroize();
DEALLOCATED_SECURE_STRINGS.fetch_add(1, Ordering::SeqCst);
}
}
impl ZeroizeOnDrop for SecureString {}
impl fmt::Debug for SecureString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecureString({})", self.masked())
}
}
impl fmt::Display for SecureString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.masked())
}
}
impl PartialEq for SecureString {
fn eq(&self, other: &Self) -> bool {
self.compare(other.as_str()).is_ok()
}
}
impl Hash for SecureString {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
impl Deref for SecureString {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[derive(Default)]
pub struct SecureStringBuilder {
data: Vec<u8>,
sensitivity: SensitivityLevel,
display_name: Option<String>,
}
impl SecureStringBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_sensitivity(mut self, sensitivity: SensitivityLevel) -> Self {
self.sensitivity = sensitivity;
self
}
pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
self.display_name = Some(name.into());
self
}
pub fn push(mut self, c: char) -> Self {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
self.data.extend_from_slice(encoded.as_bytes());
self
}
pub fn push_str(mut self, s: &str) -> Self {
self.data.extend_from_slice(s.as_bytes());
self
}
pub fn push_u8(mut self, b: u8) -> Self {
self.data.push(b);
self
}
pub fn build(self) -> SecureString {
let display_name = self
.display_name
.unwrap_or_else(|| String::from_utf8_lossy(&self.data).into_owned());
ALLOCATED_SECURE_STRINGS.fetch_add(1, Ordering::SeqCst);
SecureString {
data: self.data,
sensitivity: self.sensitivity,
display_name,
}
}
}
impl From<&str> for SecureString {
fn from(s: &str) -> Self {
Self::from(s.to_string())
}
}
impl From<String> for SecureString {
fn from(s: String) -> Self {
Self::from(s)
}
}
impl From<&String> for SecureString {
fn from(s: &String) -> Self {
Self::from(s.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secure_string_creation() {
let secret = SecureString::from("password123");
assert_eq!(secret.len(), 11);
assert!(!secret.is_empty());
}
#[test]
fn test_secure_string_compare() {
let secret = SecureString::from("password123");
assert!(secret.compare("password123").is_ok());
assert!(secret.compare("wrongpassword").is_err());
}
#[test]
fn test_secure_string_masked() {
let secret = SecureString::from("password123");
let masked = secret.masked();
assert!(masked.contains('*'));
assert!(masked.len() < 12);
}
#[test]
fn test_secure_string_display() {
let secret = SecureString::from("password123");
let display = format!("{}", secret);
assert!(display.contains('*'));
}
#[test]
fn test_secure_string_debug() {
let secret = SecureString::from("password123");
let debug = format!("{:?}", secret);
assert!(debug.contains("SecureString"));
}
#[test]
fn test_sensitivity_levels() {
let critical = SecureString::new("secret", SensitivityLevel::Critical);
let high = SecureString::new("token", SensitivityLevel::High);
let medium = SecureString::new("user", SensitivityLevel::Medium);
let low = SecureString::new("config", SensitivityLevel::Low);
assert!(critical.is_highly_sensitive());
assert!(high.is_highly_sensitive());
assert!(!medium.is_highly_sensitive());
assert!(!low.is_highly_sensitive());
}
#[test]
fn test_fingerprint() {
let secret = SecureString::from("password123");
let fp = secret.fingerprint(16);
assert_eq!(fp.len(), 16);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_secure_string_builder() {
let secret = SecureStringBuilder::new()
.push_str("pass")
.push('w')
.push_str("ord")
.build();
assert_eq!(secret.as_str(), "password");
}
#[test]
fn test_from_bytes() {
let data = vec![0x01, 0x02, 0x03, 0x04];
let secret = SecureString::from_bytes(data.clone(), SensitivityLevel::High);
assert_eq!(secret.as_bytes(), data.as_slice());
assert_eq!(secret.display_name(), "[binary data]");
}
#[test]
#[ignore = "计数器是全局的,会受其他测试影响,仅用于手动验证"]
fn test_allocation_counters() {
let initial_allocated = allocated_secure_strings();
let initial_deallocated = deallocated_secure_strings();
let _secret1 = SecureString::from("test1");
let _secret2 = SecureString::from("test2");
assert_eq!(
allocated_secure_strings(),
initial_allocated + 2,
"Should allocate 2 new SecureStrings"
);
assert_eq!(
deallocated_secure_strings(),
initial_deallocated,
"Should not deallocate any SecureStrings"
);
}
#[test]
fn test_partial_eq() {
let secret1 = SecureString::from("password");
let secret2 = SecureString::from("password");
let secret3 = SecureString::from("different");
assert_eq!(secret1, secret2);
assert_ne!(secret1, secret3);
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let secret1 = SecureString::from("password");
let secret2 = SecureString::from("password");
let secret3 = SecureString::from("different");
let mut set = HashSet::new();
set.insert(SecureString::from("password"));
set.insert(SecureString::from("password"));
set.insert(SecureString::from("different"));
assert_eq!(set.len(), 2);
assert!(set.contains(&secret1));
assert!(set.contains(&secret2));
assert!(set.contains(&secret3));
}
#[test]
fn test_secure_string_new_critical_display_name() {
let secret = SecureString::new("topsecret", SensitivityLevel::Critical);
assert_eq!(secret.display_name(), "[SENSITIVE]");
assert!(secret.is_highly_sensitive());
}
#[test]
fn test_secure_string_new_high_display_name() {
let secret = SecureString::new("token123", SensitivityLevel::High);
assert_eq!(secret.display_name(), "[8 chars]");
assert!(secret.is_highly_sensitive());
}
#[test]
fn test_secure_string_new_medium_display_name() {
let secret = SecureString::new("user_data", SensitivityLevel::Medium);
assert_eq!(secret.display_name(), "[9 chars]");
assert!(!secret.is_highly_sensitive());
}
#[test]
fn test_secure_string_new_low_display_name() {
let secret = SecureString::new("plain_config", SensitivityLevel::Low);
assert_eq!(secret.display_name(), "plain_config");
assert!(!secret.is_highly_sensitive());
}
#[test]
fn test_secure_string_as_str_valid() {
let secret = SecureString::from("hello world");
assert_eq!(secret.as_str(), "hello world");
}
#[test]
fn test_secure_string_as_str_invalid_utf8() {
let secret = SecureString::from_bytes(vec![0xFF, 0xFE, 0xFD], SensitivityLevel::High);
assert_eq!(secret.as_str(), "[invalid utf-8]");
}
#[test]
fn test_secure_string_to_plain_string() {
let secret = SecureString::from("extract-me");
let plain = secret.to_plain_string();
assert_eq!(plain, "extract-me");
}
#[test]
fn test_secure_string_compare_different_lengths() {
let secret = SecureString::from("abc");
assert!(secret.compare("ab").is_err());
assert!(secret.compare("a").is_err());
assert!(secret.compare("abcd").is_err());
assert!(secret.compare("abcdef").is_err());
}
#[test]
fn test_secure_string_compare_empty() {
let empty = SecureString::from("");
assert!(empty.compare("").is_ok());
assert!(empty.compare("nonempty").is_err());
}
#[test]
fn test_secure_string_compare_self_equivalent() {
let secret = SecureString::from("same-value");
assert!(secret.compare("same-value").is_ok());
}
#[test]
fn test_secure_string_zeroize() {
let mut secret = SecureString::from("will-be-erased");
assert_eq!(secret.len(), 14);
secret.zeroize();
assert_eq!(secret.len(), 0);
assert!(secret.is_empty());
}
#[test]
fn test_secure_string_fingerprint_zero_max_len() {
let secret = SecureString::from("password");
let fp = secret.fingerprint(0);
assert_eq!(fp.len(), 0);
}
#[test]
fn test_secure_string_fingerprint_full_length() {
let secret = SecureString::from("password");
let fp = secret.fingerprint(100);
assert_eq!(fp.len(), 64);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_secure_string_fingerprint_truncation() {
let secret = SecureString::from("password");
let fp = secret.fingerprint(32);
assert_eq!(fp.len(), 32);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_secure_string_fingerprint_deterministic() {
let s1 = SecureString::from("same-input");
let s2 = SecureString::from("same-input");
assert_eq!(s1.fingerprint(64), s2.fingerprint(64));
}
#[test]
fn test_secure_string_masked_empty() {
let secret = SecureString::from("");
assert_eq!(secret.masked(), "[empty]");
}
#[test]
fn test_secure_string_masked_single_char() {
let secret = SecureString::from("a");
assert_eq!(secret.masked(), "*");
}
#[test]
fn test_secure_string_masked_two_chars() {
let secret = SecureString::from("ab");
assert_eq!(secret.masked(), "**");
}
#[test]
fn test_secure_string_masked_three_chars() {
let secret = SecureString::from("abc");
assert_eq!(secret.masked(), "a**");
}
#[test]
fn test_secure_string_masked_four_chars() {
let secret = SecureString::from("abcd");
assert_eq!(secret.masked(), "ab**");
}
#[test]
fn test_secure_string_masked_long_string() {
let secret = SecureString::from("very-long-secret-value");
let masked = secret.masked();
assert_eq!(masked, "ve******");
}
#[test]
fn test_secure_string_is_empty_true() {
let secret = SecureString::from("");
assert!(secret.is_empty());
assert_eq!(secret.len(), 0);
}
#[test]
fn test_secure_string_len_non_empty() {
let secret = SecureString::from("12345678");
assert_eq!(secret.len(), 8);
assert!(!secret.is_empty());
}
#[test]
fn test_sensitive_data_trait_default_is_highly_sensitive() {
struct DummyData;
impl SensitiveData for DummyData {
fn display_name(&self) -> &str {
"dummy"
}
}
let d = DummyData;
assert!(!d.is_highly_sensitive());
assert_eq!(d.display_name(), "dummy");
}
#[test]
fn test_sensitivity_is_critical_or_high() {
assert!(SensitivityLevel::Critical.is_critical_or_high());
assert!(SensitivityLevel::High.is_critical_or_high());
assert!(!SensitivityLevel::Medium.is_critical_or_high());
assert!(!SensitivityLevel::Low.is_critical_or_high());
}
#[test]
fn test_sensitivity_default_is_low() {
let level = SensitivityLevel::default();
assert_eq!(level, SensitivityLevel::Low);
}
#[test]
fn test_secure_string_builder_with_sensitivity() {
let secret = SecureStringBuilder::new()
.with_sensitivity(SensitivityLevel::Low)
.push_str("data")
.build();
assert_eq!(secret.sensitivity(), SensitivityLevel::Low);
assert!(!secret.is_highly_sensitive());
}
#[test]
fn test_secure_string_builder_with_display_name() {
let secret = SecureStringBuilder::new()
.with_display_name("custom-name")
.push_str("data")
.build();
assert_eq!(secret.display_name(), "custom-name");
}
#[test]
fn test_secure_string_builder_push_u8() {
let secret = SecureStringBuilder::new()
.push_u8(b'A')
.push_u8(b'B')
.push_u8(b'C')
.build();
assert_eq!(secret.as_str(), "ABC");
}
#[test]
fn test_secure_string_builder_push_unicode_char() {
let secret = SecureStringBuilder::new().push('中').push('文').build();
assert_eq!(secret.as_str(), "中文");
assert_eq!(secret.len(), 6); }
#[test]
fn test_secure_string_builder_empty_build() {
let secret = SecureStringBuilder::new().build();
assert!(secret.is_empty());
assert_eq!(secret.len(), 0);
}
#[test]
fn test_secure_string_builder_default() {
let secret = SecureStringBuilder::default()
.push_str("default-builder")
.build();
assert_eq!(secret.as_str(), "default-builder");
}
#[test]
fn test_secure_string_deref_to_str() {
let secret = SecureString::from("deref-target");
let s: &str = &secret;
assert_eq!(s, "deref-target");
}
#[test]
fn test_secure_string_from_string() {
let input = String::from("from-string");
let secret = SecureString::from(input);
assert_eq!(secret.as_str(), "from-string");
}
#[test]
fn test_secure_string_from_str_ref() {
let input = String::from("from-str-ref");
let secret = SecureString::from(&input);
assert_eq!(secret.as_str(), "from-str-ref");
}
#[test]
fn test_secure_string_display_debug_empty() {
let empty = SecureString::from("");
assert_eq!(format!("{}", empty), "[empty]");
assert_eq!(format!("{:?}", empty), "SecureString([empty])");
}
#[test]
fn test_secure_string_sensitivity_getter() {
let critical = SecureString::new("x", SensitivityLevel::Critical);
let medium = SecureString::new("x", SensitivityLevel::Medium);
assert_eq!(critical.sensitivity(), SensitivityLevel::Critical);
assert_eq!(medium.sensitivity(), SensitivityLevel::Medium);
}
#[test]
fn test_secure_string_from_bytes_preserves_data() {
let data = vec![0x01, 0x02, 0x03, 0x04];
let secret = SecureString::from_bytes(data.clone(), SensitivityLevel::Medium);
assert_eq!(secret.as_bytes(), data.as_slice());
assert_eq!(secret.len(), 4);
assert!(!secret.is_empty());
}
#[serial_test::serial]
#[test]
fn test_secure_string_counter_lifecycle() {
reset_secure_string_counters();
assert_eq!(allocated_secure_strings(), 0);
assert_eq!(deallocated_secure_strings(), 0);
{
let _s = SecureString::from("counter-test");
assert_eq!(allocated_secure_strings(), 1);
assert_eq!(deallocated_secure_strings(), 0);
}
assert_eq!(allocated_secure_strings(), 1);
assert_eq!(deallocated_secure_strings(), 1);
reset_secure_string_counters();
assert_eq!(allocated_secure_strings(), 0);
assert_eq!(deallocated_secure_strings(), 0);
}
}