use std::collections::HashMap;
use std::fmt;
use std::sync::RwLock;
use zeroize::Zeroize;
#[derive(Clone)]
pub struct PasswordEntry {
pub username: String,
pub password: Vec<u8>,
}
impl fmt::Debug for PasswordEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PasswordEntry")
.field("username", &self.username)
.field("password", &"***") .finish()
}
}
impl Drop for PasswordEntry {
fn drop(&mut self) {
self.password.zeroize();
}
}
pub struct CredentialStore {
credentials: RwLock<HashMap<String, PasswordEntry>>,
}
impl CredentialStore {
pub fn new() -> Self {
CredentialStore {
credentials: RwLock::new(HashMap::new()),
}
}
pub fn store(&self, domain: &str, username: &str, password: &[u8]) {
let mut creds = self.credentials.write().unwrap();
let entry = PasswordEntry {
username: username.to_string(),
password: password.to_vec(),
};
creds.insert(domain.to_string(), entry);
}
pub fn get(&self, domain: &str) -> Option<PasswordEntry> {
let creds = self.credentials.read().unwrap();
creds.get(domain).cloned()
}
pub fn remove(&self, domain: &str) -> Option<PasswordEntry> {
let mut creds = self.credentials.write().unwrap();
creds.remove(domain)
}
pub fn clear(&self) {
let mut creds = self.credentials.write().unwrap();
creds.clear(); }
pub fn count(&self) -> usize {
let creds = self.credentials.read().unwrap();
creds.len()
}
pub fn has_credentials(&self, domain: &str) -> bool {
let creds = self.credentials.read().unwrap();
creds.contains_key(domain)
}
pub fn list_domains(&self) -> Vec<String> {
let creds = self.credentials.read().unwrap();
creds.keys().cloned().collect()
}
}
impl Default for CredentialStore {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for CredentialStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CredentialStore")
.field("entry_count", &self.count())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credential_store_creation() {
let store = CredentialStore::new();
assert_eq!(store.count(), 0);
assert!(!store.has_credentials("example.com"));
}
#[test]
fn test_credential_store_default() {
let store = CredentialStore::default();
assert_eq!(store.count(), 0);
}
#[test]
fn test_credential_store_operations() {
let store = CredentialStore::new();
assert_eq!(store.count(), 0);
store.store("example.com", "alice", b"secret123");
assert_eq!(store.count(), 1);
assert!(store.has_credentials("example.com"));
let creds = store.get("example.com").expect("Should find credentials");
assert_eq!(creds.username, "alice");
assert_eq!(creds.password, b"secret123");
let removed = store
.remove("example.com")
.expect("Should remove credentials");
assert_eq!(removed.username, "alice");
assert_eq!(store.count(), 0);
assert!(!store.has_credentials("example.com"));
}
#[test]
fn test_credential_store_multiple_domains() {
let store = CredentialStore::new();
store.store("api.example.com", "admin", b"admin-pass");
store.store("db.example.com", "dbuser", b"db-pass");
store.store("auth.example.com", "authuser", b"auth-pass");
assert_eq!(store.count(), 3);
let api_creds = store.get("api.example.com").unwrap();
assert_eq!(api_creds.username, "admin");
let db_creds = store.get("db.example.com").unwrap();
assert_eq!(db_creds.username, "dbuser");
let domains = store.list_domains();
assert_eq!(domains.len(), 3);
assert!(domains.contains(&"api.example.com".to_string()));
assert!(domains.contains(&"db.example.com".to_string()));
assert!(domains.contains(&"auth.example.com".to_string()));
}
#[test]
fn test_credential_store_overwrite() {
let store = CredentialStore::new();
store.store("example.com", "user1", b"pass1");
store.store("example.com", "user2", b"pass2");
assert_eq!(store.count(), 1);
let creds = store.get("example.com").unwrap();
assert_eq!(creds.username, "user2");
assert_eq!(creds.password, b"pass2");
}
#[test]
fn test_credential_store_remove_nonexistent() {
let store = CredentialStore::new();
let result = store.remove("nonexistent.com");
assert!(result.is_none());
assert_eq!(store.count(), 0);
}
#[test]
fn test_credential_store_clear() {
let store = CredentialStore::new();
store.store("domain1.com", "user1", b"pass1");
store.store("domain2.com", "user2", b"pass2");
store.store("domain3.com", "user3", b"pass3");
assert_eq!(store.count(), 3);
store.clear();
assert_eq!(store.count(), 0);
assert!(!store.has_credentials("domain1.com"));
assert!(!store.has_credentials("domain2.com"));
assert!(!store.has_credentials("domain3.com"));
}
#[test]
fn test_password_entry_debug_masking() {
let entry = PasswordEntry {
username: "testuser".to_string(),
password: b"super-secret-password".to_vec(),
};
let debug_output = format!("{:?}", entry);
assert!(debug_output.contains("testuser"));
assert!(debug_output.contains("***")); assert!(!debug_output.contains("super-secret-password"));
}
#[test]
fn test_credential_store_debug() {
let store = CredentialStore::new();
store.store("example.com", "user", b"pass");
let debug_output = format!("{:?}", store);
assert!(debug_output.contains("CredentialStore"));
assert!(debug_output.contains("entry_count"));
assert!(debug_output.contains("1"));
assert!(!debug_output.contains("user"));
assert!(!debug_output.contains("pass"));
}
#[test]
fn test_credential_store_concurrent_access() {
use std::sync::Arc;
use std::thread;
let store = Arc::new(CredentialStore::new());
let mut handles = vec![];
for i in 0..10 {
let store_clone = Arc::clone(&store);
handles.push(thread::spawn(move || {
let domain = format!("domain{}.com", i);
store_clone.store(
&domain,
&format!("user{}", i),
format!("pass{}", i).as_bytes(),
);
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(store.count(), 10);
let mut read_handles = vec![];
for i in 0..10 {
let store_clone = Arc::clone(&store);
read_handles.push(thread::spawn(move || {
let domain = format!("domain{}.com", i);
let creds = store_clone.get(&domain).unwrap();
assert_eq!(creds.username, format!("user{}", i));
}));
}
for handle in read_handles {
handle.join().unwrap();
}
}
#[test]
fn test_empty_domain_and_credentials() {
let store = CredentialStore::new();
store.store("", "user", b"pass");
assert!(store.has_credentials(""));
let creds = store.get("").unwrap();
assert_eq!(creds.username, "user");
store.store("empty-user.com", "", b"pass");
let creds = store.get("empty-user.com").unwrap();
assert_eq!(creds.username, "");
store.store("empty-pass.com", "user", b"");
let creds = store.get("empty-pass.com").unwrap();
assert_eq!(creds.password, b"");
assert_eq!(store.count(), 3);
}
#[test]
fn test_binary_password_storage() {
let store = CredentialStore::new();
let binary_pass: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF];
store.store("binary-auth.com", "binary_user", &binary_pass);
let creds = store.get("binary-auth.com").unwrap();
assert_eq!(creds.password, binary_pass);
assert_eq!(creds.password.len(), 6);
}
#[test]
fn test_unicode_in_credentials() {
let store = CredentialStore::new();
store.store("例子.コム", "ユーザー名", "パスワード".as_bytes());
let creds = store.get("例子.コム").unwrap();
assert_eq!(creds.username, "ユーザー名");
assert_eq!(creds.password, "パスワード".as_bytes());
}
}