Add argon2 password hashing
This commit is contained in:
Generated
+118
@@ -52,6 +52,42 @@ dependencies = [
|
|||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.5.53"
|
||||||
@@ -110,6 +146,46 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
|||||||
name = "common"
|
name = "common"
|
||||||
version = "0.3.1"
|
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]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -122,12 +198,29 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.180"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.104"
|
version = "1.0.104"
|
||||||
@@ -146,10 +239,17 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"clap",
|
"clap",
|
||||||
"common",
|
"common",
|
||||||
]
|
]
|
||||||
@@ -160,6 +260,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.112"
|
version = "2.0.112"
|
||||||
@@ -171,6 +277,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -183,6 +295,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
common = { workspace = true }
|
common = { workspace = true }
|
||||||
|
argon2 = "0.5.3"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "saslauthd"
|
name = "saslauthd"
|
||||||
|
|||||||
+127
-10
@@ -1,4 +1,10 @@
|
|||||||
use common::request::Request;
|
use common::request::Request;
|
||||||
|
|
||||||
|
use argon2::{
|
||||||
|
Argon2,
|
||||||
|
password_hash::{PasswordHash, PasswordVerifier},
|
||||||
|
};
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
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 {
|
pub fn check_auth(&self, request: &Request) -> bool {
|
||||||
// Don't allow empty userids or test_passwords
|
// Don't allow empty userids or test_passwords
|
||||||
//
|
//
|
||||||
@@ -23,28 +94,43 @@ impl PasswordDirectory {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file_name = self.dir_path.clone();
|
let lines = self.read_password_lines(request);
|
||||||
file_name.push(&request.userid);
|
|
||||||
|
|
||||||
if let Ok(file) = File::open(file_name) {
|
if request.password.starts_with('[') && request.password.contains("]") {
|
||||||
for line in BufReader::new(file).lines().map_while(Result::ok) {
|
self.check_hashed_password(&lines, &request.password)
|
||||||
if !line.starts_with('#') && line.trim() == request.password {
|
} else {
|
||||||
return true;
|
self.check_plain_password(&lines, &request.password)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::repository::PasswordDirectory;
|
use crate::repository::PasswordDirectory;
|
||||||
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||||
use common::request::Request;
|
use common::request::Request;
|
||||||
|
|
||||||
const TEST_PATH: &str = "../test_passwords";
|
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]
|
#[test]
|
||||||
fn it_checks_good_password() {
|
fn it_checks_good_password() {
|
||||||
let repository = PasswordDirectory::new(TEST_PATH);
|
let repository = PasswordDirectory::new(TEST_PATH);
|
||||||
@@ -54,6 +140,37 @@ mod tests {
|
|||||||
assert_eq!(repository.check_auth(&request), true);
|
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]
|
#[test]
|
||||||
fn it_fails_bad_password() {
|
fn it_fails_bad_password() {
|
||||||
let repository = PasswordDirectory::new(TEST_PATH);
|
let repository = PasswordDirectory::new(TEST_PATH);
|
||||||
|
|||||||
+9
-1
@@ -2,4 +2,12 @@
|
|||||||
|
|
||||||
some-rad-password
|
some-rad-password
|
||||||
|
|
||||||
#commented-out
|
#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
|
||||||
Reference in New Issue
Block a user