use bcrypt::{hash, verify};
use sha_crypt::{Algorithm, Params, PasswordHasher, PasswordVerifier, ShaCrypt};
use snafu::{OptionExt, ResultExt, Snafu};
use std::str::FromStr;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Unknown hash algorithm: {algorithm}"))]
UnknownAlgorithm {
algorithm: String,
},
#[snafu(display("Failed to create bcrypt hash"))]
BcryptHash {
source: bcrypt::BcryptError,
},
#[snafu(display("Failed to verify bcrypt hash"))]
BcryptVerify {
source: bcrypt::BcryptError,
},
#[snafu(display("Failed to create SHA-crypt parameters"))]
ShaCryptParams {
source: sha_crypt::Error,
},
#[snafu(display("Failed to generate random salt"))]
SaltGeneration {
source: getrandom::Error,
},
#[snafu(display("Failed to compute SHA-crypt hash"))]
ShaCryptHash {
source: sha_crypt::password_hash::Error,
},
#[snafu(display("Invalid hash format: cannot determine algorithm from '{hash}'"))]
InvalidHashFormat {
hash: String,
},
#[snafu(display("Failed to generate APR1-MD5 salt"))]
Apr1Salt {
source: crate::apr1_md5::Error,
},
}
const BCRYPT_COST: u32 = 12;
const BCRYPT_PREFIX: &str = "$2";
const SHA256_PREFIX: &str = "$5$";
const SHA512_PREFIX: &str = "$6$";
const APR1_PREFIX: &str = "$apr1$";
const DEFAULT_ROUNDS: u32 = 5000;
const SALT_BYTE_LEN: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgorithm {
Bcrypt,
Sha256,
Sha512,
Apr1Md5,
}
impl FromStr for HashAlgorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"bcrypt" => Ok(HashAlgorithm::Bcrypt),
"sha256" => Ok(HashAlgorithm::Sha256),
"sha512" => Ok(HashAlgorithm::Sha512),
"md5" | "apr1" | "apr1-md5" => Ok(HashAlgorithm::Apr1Md5),
_ => UnknownAlgorithmSnafu { algorithm: s }.fail(),
}
}
}
impl std::fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HashAlgorithm::Bcrypt => write!(f, "bcrypt"),
HashAlgorithm::Sha256 => write!(f, "sha256"),
HashAlgorithm::Sha512 => write!(f, "sha512"),
HashAlgorithm::Apr1Md5 => write!(f, "md5"),
}
}
}
pub fn hash_password(password: &str, algorithm: HashAlgorithm) -> Result<String, Error> {
match algorithm {
HashAlgorithm::Bcrypt => {
hash(password, BCRYPT_COST).context(BcryptHashSnafu)
}
HashAlgorithm::Sha256 => {
let params = Params::new(DEFAULT_ROUNDS).context(ShaCryptParamsSnafu)?;
let sha_crypt = ShaCrypt::new(Algorithm::Sha256Crypt, params);
let mut salt_bytes = [0u8; SALT_BYTE_LEN];
getrandom::fill(&mut salt_bytes).context(SaltGenerationSnafu)?;
let hash = sha_crypt
.hash_password_with_salt(password.as_bytes(), &salt_bytes)
.context(ShaCryptHashSnafu)?;
Ok(hash.to_string())
}
HashAlgorithm::Sha512 => {
let params = Params::new(DEFAULT_ROUNDS).context(ShaCryptParamsSnafu)?;
let sha_crypt = ShaCrypt::new(Algorithm::Sha512Crypt, params);
let mut salt_bytes = [0u8; SALT_BYTE_LEN];
getrandom::fill(&mut salt_bytes).context(SaltGenerationSnafu)?;
let hash = sha_crypt
.hash_password_with_salt(password.as_bytes(), &salt_bytes)
.context(ShaCryptHashSnafu)?;
Ok(hash.to_string())
}
HashAlgorithm::Apr1Md5 => {
let salt = crate::apr1_md5::generate_salt().context(Apr1SaltSnafu)?;
Ok(crate::apr1_md5::hash(password, &salt))
}
}
}
pub fn verify_password(password: &str, hash_str: &str) -> Result<bool, Error> {
let algorithm =
detect_algorithm(hash_str).context(InvalidHashFormatSnafu { hash: hash_str })?;
match algorithm {
HashAlgorithm::Bcrypt => {
verify(password, hash_str).context(BcryptVerifySnafu)
}
HashAlgorithm::Sha256 | HashAlgorithm::Sha512 => {
let sha_crypt = ShaCrypt::default();
Ok(sha_crypt
.verify_password(password.as_bytes(), hash_str)
.is_ok())
}
HashAlgorithm::Apr1Md5 => {
Ok(crate::apr1_md5::verify(password, hash_str))
}
}
}
pub fn detect_algorithm(hash: &str) -> Option<HashAlgorithm> {
if hash.starts_with(BCRYPT_PREFIX) {
Some(HashAlgorithm::Bcrypt)
} else if hash.starts_with(SHA256_PREFIX) {
Some(HashAlgorithm::Sha256)
} else if hash.starts_with(SHA512_PREFIX) {
Some(HashAlgorithm::Sha512)
} else if hash.starts_with(APR1_PREFIX) {
Some(HashAlgorithm::Apr1Md5)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bcrypt_hash_and_verify() {
let password = "test_password_123";
let hash = hash_password(password, HashAlgorithm::Bcrypt).unwrap();
assert!(hash.starts_with("$2b$"));
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_sha256_hash_and_verify() {
let password = "test_password_123";
let hash = hash_password(password, HashAlgorithm::Sha256).unwrap();
assert!(hash.starts_with("$5$"));
println!("Generated hash: {}", hash);
use sha_crypt::{PasswordVerifier, ShaCrypt};
let sha_crypt = ShaCrypt::default();
match sha_crypt.verify_password(password.as_bytes(), hash.as_str()) {
Ok(_) => println!("Direct verification SUCCESS"),
Err(e) => println!("Direct verification FAILED: {:?}", e),
}
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_sha512_hash_and_verify() {
let password = "test_password_123";
let hash = hash_password(password, HashAlgorithm::Sha512).unwrap();
assert!(hash.starts_with("$6$"));
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_detect_algorithm() {
assert_eq!(detect_algorithm("$2b$12$abc"), Some(HashAlgorithm::Bcrypt));
assert_eq!(detect_algorithm("$2y$12$abc"), Some(HashAlgorithm::Bcrypt));
assert_eq!(detect_algorithm("$2a$12$abc"), Some(HashAlgorithm::Bcrypt));
assert_eq!(
detect_algorithm("$5$rounds=5000$salt$hash"),
Some(HashAlgorithm::Sha256)
);
assert_eq!(
detect_algorithm("$5$salt$hash"),
Some(HashAlgorithm::Sha256)
);
assert_eq!(
detect_algorithm("$6$rounds=5000$salt$hash"),
Some(HashAlgorithm::Sha512)
);
assert_eq!(
detect_algorithm("$6$salt$hash"),
Some(HashAlgorithm::Sha512)
);
assert_eq!(detect_algorithm("invalid"), None);
}
#[test]
fn test_algorithm_from_str() {
assert_eq!(
HashAlgorithm::from_str("bcrypt").unwrap(),
HashAlgorithm::Bcrypt
);
assert_eq!(
HashAlgorithm::from_str("BCRYPT").unwrap(),
HashAlgorithm::Bcrypt
);
assert_eq!(
HashAlgorithm::from_str("sha256").unwrap(),
HashAlgorithm::Sha256
);
assert_eq!(
HashAlgorithm::from_str("sha512").unwrap(),
HashAlgorithm::Sha512
);
assert!(HashAlgorithm::from_str("invalid").is_err());
}
#[test]
fn test_apache_sha256_format() {
let apache_hash = "$5$6.O43dJ8bymsjcrp$QzfN4hZywImpvc6uE0O8TR.Xe87tgyvATwyV61rQbR5";
assert!(detect_algorithm(apache_hash) == Some(HashAlgorithm::Sha256));
use sha_crypt::{PasswordVerifier, ShaCrypt};
let sha_crypt = ShaCrypt::default();
match sha_crypt.verify_password(b"testpass123", apache_hash) {
Ok(_) => println!("Direct Apache verification SUCCESS"),
Err(e) => println!("Direct Apache verification FAILED: {:?}", e),
}
assert!(verify_password("testpass123", apache_hash).unwrap());
assert!(!verify_password("wrongpass", apache_hash).unwrap());
}
#[test]
fn test_apache_sha512_format() {
let apache_hash = "$6$On58VpHvsS95KvxU$lbtz65RFxG2fcos.C3vi2wllW82efo8fDcO2PRVEnHwKpvS4tGE4OJ6He98QevZYUrenCYl9QiG0woAexDYkZ/";
assert!(detect_algorithm(apache_hash) == Some(HashAlgorithm::Sha512));
assert!(verify_password("testpass123", apache_hash).unwrap());
assert!(!verify_password("wrongpass", apache_hash).unwrap());
}
}