use std::collections::HashMap;
use std::fmt;
use crate::local::password::PasswordHash;
#[derive(Debug, Clone)]
pub struct StoredCredential {
username: String,
password_hash: PasswordHash,
}
impl StoredCredential {
#[must_use]
pub fn new(username: String, password_hash: PasswordHash) -> Self {
Self {
username,
password_hash,
}
}
#[must_use]
#[inline]
pub fn username(&self) -> &str {
&self.username
}
#[must_use]
#[inline]
pub fn password_hash(&self) -> &PasswordHash {
&self.password_hash
}
}
pub trait CredentialStore {
type Error: std::error::Error;
fn lookup(&self, username: &str) -> Result<Option<StoredCredential>, Self::Error>;
fn store(&mut self, username: &str, hash: PasswordHash) -> Result<(), Self::Error>;
fn remove(&mut self, username: &str) -> Result<bool, Self::Error>;
}
#[derive(Debug, Default)]
pub struct InMemoryCredentialStore {
credentials: HashMap<String, PasswordHash>,
}
impl InMemoryCredentialStore {
#[must_use]
pub fn new() -> Self {
Self {
credentials: HashMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InMemoryStoreError {
_private: (),
}
impl fmt::Display for InMemoryStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "in-memory store: unexpected error")
}
}
impl std::error::Error for InMemoryStoreError {}
impl CredentialStore for InMemoryCredentialStore {
type Error = InMemoryStoreError;
fn lookup(&self, username: &str) -> Result<Option<StoredCredential>, Self::Error> {
Ok(self
.credentials
.get(username)
.map(|hash| StoredCredential::new(username.to_owned(), hash.clone())))
}
fn store(&mut self, username: &str, hash: PasswordHash) -> Result<(), Self::Error> {
self.credentials.insert(username.to_owned(), hash);
Ok(())
}
fn remove(&mut self, username: &str) -> Result<bool, Self::Error> {
Ok(self.credentials.remove(username).is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::local::password::{MIN_MEMORY_KIB, PasswordConfig};
fn test_config() -> PasswordConfig {
PasswordConfig::new()
.with_memory_kib(MIN_MEMORY_KIB)
.with_iterations(1)
.with_parallelism(1)
}
#[test]
fn store_and_lookup() {
let mut store = InMemoryCredentialStore::new();
let config = test_config();
let hash = PasswordHash::generate(b"password", &config).unwrap();
store.store("alice", hash.clone()).unwrap();
let cred = store.lookup("alice").unwrap().unwrap();
assert_eq!(cred.username(), "alice");
assert_eq!(cred.password_hash().to_phc_string(), hash.to_phc_string());
}
#[test]
fn stored_credential_constructor_and_accessors() {
let config = test_config();
let hash = PasswordHash::generate(b"password", &config).unwrap();
let cred = StoredCredential::new("alice".to_owned(), hash.clone());
assert_eq!(cred.username(), "alice");
assert_eq!(cred.password_hash().to_phc_string(), hash.to_phc_string());
}
#[test]
fn lookup_nonexistent_returns_none() {
let store = InMemoryCredentialStore::new();
assert!(store.lookup("nobody").unwrap().is_none());
}
#[test]
fn store_overwrites_existing() {
let mut store = InMemoryCredentialStore::new();
let config = test_config();
let hash1 = PasswordHash::generate(b"password1", &config).unwrap();
let hash2 = PasswordHash::generate(b"password2", &config).unwrap();
store.store("alice", hash1).unwrap();
store.store("alice", hash2.clone()).unwrap();
let cred = store.lookup("alice").unwrap().unwrap();
assert_eq!(cred.password_hash().to_phc_string(), hash2.to_phc_string());
}
#[test]
fn remove_existing() {
let mut store = InMemoryCredentialStore::new();
let config = test_config();
let hash = PasswordHash::generate(b"password", &config).unwrap();
store.store("alice", hash).unwrap();
assert!(store.remove("alice").unwrap());
assert!(store.lookup("alice").unwrap().is_none());
}
#[test]
fn remove_nonexistent_returns_false() {
let mut store = InMemoryCredentialStore::new();
assert!(!store.remove("nobody").unwrap());
}
#[test]
fn store_multiple_users() {
let mut store = InMemoryCredentialStore::new();
let config = test_config();
let hash_a = PasswordHash::generate(b"pass-a", &config).unwrap();
let hash_b = PasswordHash::generate(b"pass-b", &config).unwrap();
store.store("alice", hash_a.clone()).unwrap();
store.store("bob", hash_b.clone()).unwrap();
let cred_a = store.lookup("alice").unwrap().unwrap();
let cred_b = store.lookup("bob").unwrap().unwrap();
assert_eq!(
cred_a.password_hash().to_phc_string(),
hash_a.to_phc_string()
);
assert_eq!(
cred_b.password_hash().to_phc_string(),
hash_b.to_phc_string()
);
}
#[test]
fn stored_hash_verifies_password() {
let mut store = InMemoryCredentialStore::new();
let config = test_config();
let hash = PasswordHash::generate(b"my-secret", &config).unwrap();
store.store("alice", hash).unwrap();
let cred = store.lookup("alice").unwrap().unwrap();
assert!(cred.password_hash().verify(b"my-secret"));
assert!(!cred.password_hash().verify(b"wrong"));
}
#[test]
fn default_store_is_empty() {
let store = InMemoryCredentialStore::default();
assert!(store.lookup("anyone").unwrap().is_none());
}
}