Add argon2 password hashing

This commit is contained in:
Barry Pederson
2026-02-03 12:59:01 -08:00
parent 120db0bd90
commit a03b738c7a
4 changed files with 255 additions and 11 deletions
+1
View File
@@ -6,6 +6,7 @@ edition.workspace = true
[dependencies]
clap = { workspace = true }
common = { workspace = true }
argon2 = "0.5.3"
[[bin]]
name = "saslauthd"
+127 -10
View File
@@ -1,4 +1,10 @@
use common::request::Request;
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordVerifier},
};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
@@ -15,6 +21,71 @@ impl PasswordDirectory {
}
}
/// Read all the non-blank and non-comment lines in a password file
///
fn read_password_lines(&self, request: &Request) -> Vec<String> {
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<String>, password: &str) -> bool {
let (key, split_password) = password.strip_prefix('[').unwrap().split_once(']').unwrap();
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<String>, password: &str) -> bool {
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
//
@@ -23,28 +94,43 @@ impl PasswordDirectory {
return false;
}
let mut file_name = self.dir_path.clone();
file_name.push(&request.userid);
let lines = self.read_password_lines(request);
if let Ok(file) = File::open(file_name) {
for line in BufReader::new(file).lines().map_while(Result::ok) {
if !line.starts_with('#') && line.trim() == request.password {
return true;
}
}
if request.password.starts_with('[') && request.password.contains("]") {
self.check_hashed_password(&lines, &request.password)
} else {
self.check_plain_password(&lines, &request.password)
}
false
}
}
#[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);
@@ -54,6 +140,37 @@ mod tests {
assert_eq!(repository.check_auth(&request), true);
}
#[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_eq!(repository.check_auth(&request), true);
}
/// 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_eq!(repository.check_auth(&request), false);
}
/// 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_eq!(repository.check_auth(&request), false);
}
#[test]
fn it_fails_bad_password() {
let repository = PasswordDirectory::new(TEST_PATH);