From a03b738c7a4ac6af98510a6e37d01f35b4a7ce18 Mon Sep 17 00:00:00 2001 From: Barry Pederson Date: Tue, 3 Feb 2026 12:59:01 -0800 Subject: [PATCH] Add argon2 password hashing --- Cargo.lock | 118 +++++++++++++++++++++++++++++++++ server/Cargo.toml | 1 + server/src/repository.rs | 137 ++++++++++++++++++++++++++++++++++++--- test_passwords/bp | 10 ++- 4 files changed, 255 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8343745..ec0666b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,42 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "clap" version = "4.5.53" @@ -110,6 +146,46 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" name = "common" version = "0.3.1" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "heck" version = "0.5.0" @@ -122,12 +198,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "proc-macro2" version = "1.0.104" @@ -146,10 +239,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "server" version = "0.3.1" dependencies = [ + "argon2", "clap", "common", ] @@ -160,6 +260,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.112" @@ -171,6 +277,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -183,6 +295,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index 7db84ed..2c87cc8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] clap = { workspace = true } common = { workspace = true } +argon2 = "0.5.3" [[bin]] name = "saslauthd" diff --git a/server/src/repository.rs b/server/src/repository.rs index 2446a61..532394d 100644 --- a/server/src/repository.rs +++ b/server/src/repository.rs @@ -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 { + 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(); + + 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 { + 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); diff --git a/test_passwords/bp b/test_passwords/bp index dff6ee9..c8a77cb 100644 --- a/test_passwords/bp +++ b/test_passwords/bp @@ -2,4 +2,12 @@ some-rad-password -#commented-out \ No newline at end of file +#commented-out + +# Example password with from commandline: +# +# echo -n "hunter2" | argon2 "$(openssl rand -base64 16)" -id +# +# the user would be sending the password: "[id-1]hunter2" +# +[id-1]$argon2id$v=19$m=4096,t=3,p=1$VTMrc2wyaW93L01ibVFPOGNQcHcxQT09$UxX86sGpknkc45CnXq+4CZ0coiTYDvSWIN7JgbeAZUs \ No newline at end of file