use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use sha1::Sha1;
use sha2::{Digest, Sha256};
pub fn verify_password(password: &str, stored: &str) -> crate::Result<bool> {
if let Some(stripped) = stored.strip_prefix("{SSHA}") {
verify_ssha(password, stripped)
} else if let Some(stripped) = stored.strip_prefix("{SHA}") {
verify_sha(password, stripped)
} else if let Some(stripped) = stored.strip_prefix("{SHA256}") {
verify_sha256(password, stripped)
} else if stored.starts_with("{BCRYPT}") || stored.starts_with("$2") {
verify_bcrypt(password, stored.trim_start_matches("{BCRYPT}"))
} else {
Ok(password == stored)
}
}
fn verify_ssha(password: &str, encoded: &str) -> crate::Result<bool> {
let decoded = BASE64
.decode(encoded)
.map_err(|e| crate::YamlLdapError::Auth(format!("Invalid SSHA encoding: {}", e)))?;
if decoded.len() < 20 {
return Err(crate::YamlLdapError::Auth(
"Invalid SSHA hash length".to_string(),
));
}
let (hash, salt) = decoded.split_at(20);
let mut hasher = Sha1::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let computed_hash = hasher.finalize();
Ok(computed_hash.as_slice() == hash)
}
fn verify_sha(password: &str, encoded: &str) -> crate::Result<bool> {
let decoded = BASE64
.decode(encoded)
.map_err(|e| crate::YamlLdapError::Auth(format!("Invalid SHA encoding: {}", e)))?;
let mut hasher = Sha1::new();
hasher.update(password.as_bytes());
let computed_hash = hasher.finalize();
Ok(computed_hash.as_slice() == decoded)
}
fn verify_sha256(password: &str, encoded: &str) -> crate::Result<bool> {
let decoded = BASE64
.decode(encoded)
.map_err(|e| crate::YamlLdapError::Auth(format!("Invalid SHA256 encoding: {}", e)))?;
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let computed_hash = hasher.finalize();
Ok(computed_hash.as_slice() == decoded)
}
fn verify_bcrypt(password: &str, hash: &str) -> crate::Result<bool> {
bcrypt::verify(password, hash)
.map_err(|e| crate::YamlLdapError::Auth(format!("Bcrypt verification failed: {}", e)))
}
pub fn hash_password(password: &str, method: &str) -> crate::Result<String> {
match method {
"plain" => Ok(password.to_string()),
"sha" => {
let mut hasher = Sha1::new();
hasher.update(password.as_bytes());
let hash = hasher.finalize();
Ok(format!("{{SHA}}{}", BASE64.encode(hash)))
}
"sha256" => {
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let hash = hasher.finalize();
Ok(format!("{{SHA256}}{}", BASE64.encode(hash)))
}
"ssha" => {
use rand::Rng;
let salt: [u8; 8] = rand::thread_rng().gen();
let mut hasher = Sha1::new();
hasher.update(password.as_bytes());
hasher.update(salt);
let hash = hasher.finalize();
let mut result = hash.to_vec();
result.extend_from_slice(&salt);
Ok(format!("{{SSHA}}{}", BASE64.encode(result)))
}
"bcrypt" => {
let hash = bcrypt::hash(password, bcrypt::DEFAULT_COST)
.map_err(|e| crate::YamlLdapError::Auth(format!("Bcrypt hashing failed: {}", e)))?;
Ok(format!("{{BCRYPT}}{}", hash))
}
_ => Err(crate::YamlLdapError::Auth(format!(
"Unknown password hash method: {}",
method
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plain_password() {
assert!(verify_password("test123", "test123").unwrap());
assert!(!verify_password("test123", "wrong").unwrap());
}
#[test]
fn test_sha_password() {
let hashed = hash_password("test123", "sha").unwrap();
assert!(verify_password("test123", &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
}
#[test]
fn test_bcrypt_password() {
let hashed = hash_password("test123", "bcrypt").unwrap();
assert!(verify_password("test123", &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
}
#[test]
fn test_sha256_password() {
let hashed = hash_password("test123", "sha256").unwrap();
assert!(verify_password("test123", &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
let known_hash = "{SHA256}7NcYcNGWMxapfjrDQIyYNa2M8PPBvHA1J8MCZVNPda4=";
assert!(verify_password("test123", known_hash).unwrap());
assert!(!verify_password("test456", known_hash).unwrap());
}
#[test]
fn test_ssha_password() {
let hashed = hash_password("test123", "ssha").unwrap();
assert!(hashed.starts_with("{SSHA}"));
assert!(verify_password("test123", &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
let empty_hash = hash_password("", "ssha").unwrap();
assert!(verify_password("", &empty_hash).unwrap());
assert!(!verify_password("test", &empty_hash).unwrap());
}
#[test]
fn test_verify_sha256_directly() {
let valid_base64 = "7NcYcNGWMxapfjrDQIyYNa2M8PPBvHA1J8MCZVNPda4=";
assert!(verify_sha256("test123", valid_base64).unwrap());
assert!(!verify_sha256("wrong", valid_base64).unwrap());
let result = verify_sha256("test", "invalid@base64!");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid SHA256 encoding"));
}
#[test]
fn test_verify_ssha_edge_cases() {
let result = verify_password("test", "{SSHA}invalid@base64!");
assert!(result.is_err());
let result = verify_password("test", "{SSHA}YWI="); assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid SSHA hash"));
}
#[test]
fn test_hash_password_unknown_method() {
let result = hash_password("test", "unknown");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unknown password hash method"));
}
#[test]
fn test_empty_passwords() {
assert!(verify_password("", "").unwrap());
assert!(!verify_password("test", "").unwrap());
let sha_empty = hash_password("", "sha").unwrap();
assert!(verify_password("", &sha_empty).unwrap());
let sha256_empty = hash_password("", "sha256").unwrap();
assert!(verify_password("", &sha256_empty).unwrap());
}
#[test]
fn test_special_characters_in_password() {
let special_pass = "test!@#$%^&*()_+-=[]{}|;':\",./<>?";
for method in &["plain", "sha", "sha256", "ssha", "bcrypt"] {
let hashed = hash_password(special_pass, method).unwrap();
assert!(verify_password(special_pass, &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
}
}
#[test]
fn test_unicode_passwords() {
let unicode_pass = "测试密码🔐";
for method in &["plain", "sha", "sha256", "ssha"] {
let hashed = hash_password(unicode_pass, method).unwrap();
assert!(verify_password(unicode_pass, &hashed).unwrap());
assert!(!verify_password("wrong", &hashed).unwrap());
}
}
#[test]
fn test_bcrypt_with_prefix() {
let password = "test123";
let hashed = hash_password(password, "bcrypt").unwrap();
assert!(hashed.starts_with("{BCRYPT}"));
assert!(verify_password(password, &hashed).unwrap());
let raw_hash = hashed.trim_start_matches("{BCRYPT}");
assert!(verify_password(password, raw_hash).unwrap());
}
#[test]
fn test_invalid_hash_format() {
assert!(verify_password("test", "{SHA}invalid!!!").is_err());
assert!(verify_password("test", "{SHA256}invalid!!!").is_err());
assert!(verify_password("test", "{SSHA}invalid!!!").is_err());
assert!(verify_password("test", "{SSHA}dGVzdA==").is_err()); }
#[test]
fn test_bcrypt_error_handling() {
let hash_without_prefix = bcrypt::hash("test123", bcrypt::DEFAULT_COST).unwrap();
let hash_with_prefix = format!("{{BCRYPT}}{}", hash_without_prefix);
assert!(verify_password("test123", &hash_without_prefix).unwrap());
assert!(verify_password("test123", &hash_with_prefix).unwrap());
}
#[test]
fn test_unknown_hash_method() {
assert!(hash_password("test", "md5").is_err());
assert!(hash_password("test", "unknown").is_err());
}
#[test]
fn test_empty_password() {
assert!(verify_password("", "").unwrap());
assert!(!verify_password("something", "").unwrap());
let sha_empty = hash_password("", "sha").unwrap();
assert!(verify_password("", &sha_empty).unwrap());
let sha256_empty = hash_password("", "sha256").unwrap();
assert!(verify_password("", &sha256_empty).unwrap());
}
}