use std::{
fs::{self, File},
io::{BufRead, BufReader, BufWriter, Write},
path::Path,
sync::Arc,
};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use async_trait::async_trait;
use dashmap::DashMap;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::server::address_pool::AddressPool;
use quincy::{
auth::{ClientAuthenticator, ServerAuthenticator},
config::{ClientAuthenticationConfig, ServerAuthenticationConfig},
error::AuthError,
Result,
};
pub struct UsersFileServerAuthenticator {
user_database: UserDatabase,
address_pool: Arc<AddressPool>,
}
pub struct UsersFileClientAuthenticator {
username: String,
password: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UsersFilePayload {
username: String,
password: String,
}
pub struct UserDatabase {
users: DashMap<String, User>,
hasher: Argon2<'static>,
}
pub struct User {
pub username: String,
pub password_hash: String,
}
impl UsersFileServerAuthenticator {
pub fn new(
config: &ServerAuthenticationConfig,
address_pool: Arc<AddressPool>,
) -> Result<Self> {
let users_file =
load_users_file(&config.users_file).map_err(|_| AuthError::StoreUnavailable)?;
let user_database = UserDatabase::new(users_file);
Ok(Self {
user_database,
address_pool,
})
}
}
impl UsersFileClientAuthenticator {
pub fn new(config: &ClientAuthenticationConfig) -> Self {
Self {
username: config.username.clone(),
password: config.password.clone(),
}
}
}
#[async_trait]
impl ServerAuthenticator for UsersFileServerAuthenticator {
async fn authenticate_user(&self, authentication_payload: Value) -> Result<(String, IpNet)> {
let payload: UsersFilePayload = serde_json::from_value(authentication_payload)
.map_err(|_| AuthError::InvalidPayload)?;
self.user_database
.authenticate(&payload.username, payload.password)
.await?;
let client_address = self
.address_pool
.next_available_address()
.ok_or(AuthError::StoreUnavailable)?;
Ok((payload.username, client_address))
}
}
#[async_trait]
impl ClientAuthenticator for UsersFileClientAuthenticator {
async fn generate_payload(&self) -> Result<Value> {
let payload = UsersFilePayload {
username: self.username.clone(),
password: self.password.clone(),
};
Ok(serde_json::to_value(payload).map_err(|_| AuthError::InvalidPayload)?)
}
}
impl User {
pub fn new(username: String, password_hash: String) -> Self {
Self {
username,
password_hash,
}
}
}
impl TryFrom<String> for User {
type Error = quincy::QuincyError;
fn try_from(user_string: String) -> Result<Self> {
let split: Vec<String> = user_string.split(':').map(|str| str.to_owned()).collect();
let name = split.first().ok_or(AuthError::InvalidPayload)?.clone();
let password_hash_string = split.get(1).ok_or(AuthError::InvalidPayload)?.clone();
Ok(User::new(name, password_hash_string))
}
}
impl UserDatabase {
pub fn new(users: DashMap<String, User>) -> Self {
Self {
users,
hasher: Argon2::default(),
}
}
pub async fn authenticate(&self, username: &str, password: String) -> Result<()> {
let user = self.users.get(username).ok_or(AuthError::UserNotFound)?;
let password_hash =
PasswordHash::new(&user.password_hash).map_err(|_| AuthError::PasswordHashingFailed)?;
self.hasher
.verify_password(password.as_bytes(), &password_hash)
.map_err(|_| AuthError::InvalidCredentials)?;
Ok(())
}
}
pub fn load_users_file(users_file: &Path) -> Result<DashMap<String, User>> {
let file = File::open(users_file).map_err(|_| AuthError::StoreUnavailable)?;
let lines = BufReader::new(file).lines();
let result: DashMap<String, User> = DashMap::new();
for line in lines {
let line_content = line.map_err(|_| AuthError::StoreUnavailable)?;
let user: User = line_content.try_into()?;
result.insert(user.username.clone(), user);
}
Ok(result)
}
pub fn save_users_file(users_file: &Path, users: DashMap<String, User>) -> Result<()> {
if users_file.exists() {
fs::remove_file(users_file).map_err(|_| AuthError::StoreUnavailable)?;
}
let file = File::create(users_file).map_err(|_| AuthError::StoreUnavailable)?;
let mut writer = BufWriter::new(file);
for (username, user) in users {
writer
.write_all(format!("{username}:{}\n", user.password_hash).as_bytes())
.map_err(|_| AuthError::StoreUnavailable)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::users_file::{User, UserDatabase};
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHasher};
use dashmap::DashMap;
use rand_core::OsRng;
#[tokio::test]
async fn test_authentication() {
let users: DashMap<String, User> = DashMap::new();
let argon = Argon2::default();
let username = "test".to_owned();
let password = "password".to_owned();
let salt = SaltString::generate(&mut OsRng);
let password_hash = argon.hash_password(password.as_bytes(), &salt).unwrap();
let test_user = User::new(username.clone(), password_hash.to_string());
users.insert(username.clone(), test_user);
let user_db = UserDatabase::new(users);
user_db
.authenticate(&username, password)
.await
.expect("Credentials are valid");
}
}