use common::request::Request; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; use log::info; #[derive(Clone)] pub struct PasswordDirectory { dir_path: PathBuf, } impl PasswordDirectory { pub fn new(path: &str) -> PasswordDirectory { PasswordDirectory { dir_path: PathBuf::from(path), } } /// Read all the non-blank and non-comment lines in a password file /// fn read_password_lines(&self, request: &Request) -> Vec { let mut file_name = self.dir_path.clone(); file_name.push(&request.userid); let mut result = Vec::new(); if let Ok(file) = File::open(file_name) { for line in BufReader::new(file).lines().map_while(Result::ok) { if !line.starts_with('#') { let trimmed = line.trim(); if !trimmed.is_empty() { result.push(line); } } } } result } /// The password sent by the user is like "[{id}]{plain-text-password}" /// so try to find a line with a matching "[{id}]" and see if the hash stored /// there corresponds to the {plain-text-password} /// fn check_hashed_password(&self, lines: &Vec, password: &str) -> bool { let (key, split_password) = password.strip_prefix('[').unwrap().split_once(']').unwrap(); info!(" looking for password-key: [{}]", key); let mut result = false; for line in lines { if line.starts_with('[') && line.contains(']') { let (line_key, line_hash) = line.strip_prefix('[').unwrap().split_once(']').unwrap(); if line_key == key && let Ok(parsed_hash) = PasswordHash::new(line_hash) && Argon2::default() .verify_password(split_password.as_bytes(), &parsed_hash) .is_ok() { result = true; } } } result } fn check_plain_password(&self, lines: &Vec, password: &str) -> bool { info!(" looking for plain password"); let mut result = false; for line in lines { // Ignore lines that are the hashed passwords with ids, but still take // lines that contain ']' if they don't also start with '[' // if !(line.starts_with('[') && line.contains(']')) && line == password { result = true; } } result } pub fn check_auth(&self, request: &Request) -> bool { // Don't allow empty userids or test_passwords // if request.userid.is_empty() || request.userid.contains('/') || request.password.is_empty() { return false; } let lines = self.read_password_lines(request); if request.password.starts_with('[') && request.password.contains("]") { self.check_hashed_password(&lines, &request.password) } else { self.check_plain_password(&lines, &request.password) } } } #[cfg(test)] mod tests { use crate::repository::PasswordDirectory; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use common::request::Request; const TEST_PATH: &str = "../test_passwords"; #[test] /// Make sure hash checking is working as expected /// fn it_checks_argon2() { let password = b"hunter2"; // Generated with // echo -n "hunter2" | argon2 "$(openssl rand -base64 16)" -id let password_hash = "$argon2id$v=19$m=4096,t=3,p=1$VTMrc2wyaW93L01ibVFPOGNQcHcxQT09$UxX86sGpknkc45CnXq+4CZ0coiTYDvSWIN7JgbeAZUs"; let parsed_hash = PasswordHash::new(password_hash).unwrap(); assert!( Argon2::default() .verify_password(password, &parsed_hash) .is_ok() ); } #[test] fn it_checks_good_password() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp", "some-rad-password", "ignore-this", "also-ignore-this"); assert!(repository.check_auth(&request)); } #[test] fn it_checks_good_hashed_password() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp", "[id-1]hunter2", "ignore-this", "also-ignore-this"); assert!(repository.check_auth(&request)); } /// The key [id-1] is present in the test file, but the hash shouldn't match /// #[test] fn it_checks_bad_hashed_password() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp", "[id-1]hunter-bad", "ignore-this", "also-ignore-this"); assert!(!repository.check_auth(&request)); } /// The key [id-2] is not present in the test file /// #[test] fn it_checks_missing_hashed_password() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp", "[id-2]hunter", "ignore-this", "also-ignore-this"); assert!(!repository.check_auth(&request)); } #[test] fn it_fails_bad_password() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp", "not-correct", "ignore-this", "also-ignore-this"); assert!(!repository.check_auth(&request)); } #[test] fn it_fails_bad_userid() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new( "bp-xxx", "some-rad-password", "ignore-this", "also-ignore-this", ); assert!(!repository.check_auth(&request)); } #[test] fn it_ignores_comments() { let repository = PasswordDirectory::new(TEST_PATH); let request = Request::new("bp-xxx", "commented-out", "ignore-this", "also-ignore-this"); assert!(!repository.check_auth(&request)); } }