use md5::{Digest, Md5};
use snafu::{ResultExt, Snafu};
use subtle::ConstantTimeEq;
use zeroize::Zeroize;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Failed to generate random salt"))]
SaltGeneration {
source: getrandom::Error,
},
}
const ITOA64: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub const APR1_PREFIX: &str = "$apr1$";
pub const APR1_SALT_LEN: usize = 8;
const APR1_ROUNDS: u32 = 1000;
fn encode_apr1_hash(digest: &[u8; 16]) -> String {
let mut result = String::with_capacity(22);
let mut encode = |mut v: u32, n: usize| {
for _ in 0..n {
result.push(ITOA64[(v & 0x3f) as usize] as char);
v >>= 6;
}
};
encode(
u32::from(digest[0]) << 16 | u32::from(digest[6]) << 8 | u32::from(digest[12]),
4,
);
encode(
u32::from(digest[1]) << 16 | u32::from(digest[7]) << 8 | u32::from(digest[13]),
4,
);
encode(
u32::from(digest[2]) << 16 | u32::from(digest[8]) << 8 | u32::from(digest[14]),
4,
);
encode(
u32::from(digest[3]) << 16 | u32::from(digest[9]) << 8 | u32::from(digest[15]),
4,
);
encode(
u32::from(digest[4]) << 16 | u32::from(digest[10]) << 8 | u32::from(digest[5]),
4,
);
encode(u32::from(digest[11]), 2);
result
}
pub fn generate_salt() -> Result<String, Error> {
let mut salt_bytes = [0u8; 6]; getrandom::fill(&mut salt_bytes).context(SaltGenerationSnafu)?;
let mut salt = String::with_capacity(APR1_SALT_LEN);
let mut val: u32 = 0;
let mut bits: u32 = 0;
let mut n = 0;
for _ in 0..APR1_SALT_LEN {
if bits < 6 {
val |= u32::from(salt_bytes[n]) << bits;
n += 1;
bits += 8;
}
salt.push(ITOA64[(val & 0x3f) as usize] as char);
val >>= 6;
bits -= 6;
}
Ok(salt)
}
pub fn hash(password: &str, salt: &str) -> String {
let password_bytes = password.as_bytes();
let salt = &salt[..salt.len().min(8)];
let mut hasher1 = Md5::new();
hasher1.update(password_bytes);
hasher1.update(salt.as_bytes());
hasher1.update(password_bytes);
let hash1 = hasher1.finalize();
let mut context = Vec::new();
context.extend_from_slice(password_bytes);
context.extend_from_slice(APR1_PREFIX.as_bytes());
context.extend_from_slice(salt.as_bytes());
let mut i = password_bytes.len();
while i > 0 {
let chunk_len = hash1.len().min(i);
context.extend_from_slice(&hash1[..chunk_len]);
i -= chunk_len;
}
i = password_bytes.len();
while i > 0 {
if (i & 1) == 1 {
context.push(0); } else {
context.push(password_bytes[0]); }
i >>= 1;
}
let mut hasher2 = Md5::new();
hasher2.update(&context);
let mut hash2 = hasher2.finalize();
context.zeroize();
let max_input_size = password_bytes.len() + 16 + 8 + password_bytes.len() + 16;
let mut input = Vec::with_capacity(max_input_size);
for i in 0..APR1_ROUNDS {
input.clear();
if (i & 1) == 1 {
input.extend_from_slice(password_bytes);
} else {
input.extend_from_slice(&hash2);
}
if i % 3 != 0 {
input.extend_from_slice(salt.as_bytes());
}
if i % 7 != 0 {
input.extend_from_slice(password_bytes);
}
if (i & 1) == 1 {
input.extend_from_slice(&hash2);
} else {
input.extend_from_slice(password_bytes);
}
let mut hasher = Md5::new();
hasher.update(&input);
hash2 = hasher.finalize();
}
input.zeroize();
let encoded_hash = encode_apr1_hash(&hash2.into());
format!("{}{}${}", APR1_PREFIX, salt, encoded_hash)
}
pub fn verify(password: &str, hash_str: &str) -> bool {
if !hash_str.starts_with(APR1_PREFIX) {
return false;
}
let after_prefix = &hash_str[APR1_PREFIX.len()..];
let salt_end = after_prefix.find('$').unwrap_or(after_prefix.len());
let salt = &after_prefix[..salt_end];
let computed = hash(password, salt);
computed.as_bytes().ct_eq(hash_str.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apr1_hash_hello() {
let password = "hello";
let salt = "xlWep/gn";
let result = hash(password, salt);
println!("Generated hash for 'hello': {}", result);
assert!(result.starts_with("$apr1$xlWep/gn$"));
assert_eq!(result, "$apr1$xlWep/gn$6UNiHq3WE714EKfeH2X5c.");
}
#[test]
fn test_apr1_hash_password() {
let password = "password";
let salt = "lZL6V/ci";
let result = hash(password, salt);
println!("Generated hash for 'password': {}", result);
assert!(result.starts_with("$apr1$lZL6V/ci$"));
assert_eq!(result, "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00");
}
#[test]
fn test_apr1_verify_hello() {
let hash_str = "$apr1$xlWep/gn$6UNiHq3WE714EKfeH2X5c.";
assert!(verify("hello", hash_str));
assert!(!verify("wrong", hash_str));
}
#[test]
fn test_apr1_verify_password() {
let hash_str = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00";
assert!(verify("password", hash_str));
assert!(!verify("wrong", hash_str));
}
#[test]
fn test_apr1_verify_testpass123() {
let hash_str = "$apr1$WxrZ8P3I$XD2BykvOa82I1l5jCMtbW0";
assert!(verify("testpass123", hash_str));
assert!(!verify("wrongpass", hash_str));
}
#[test]
fn test_apr1_hash_and_verify_roundtrip() {
let password = "my_secure_password_123";
let salt = generate_salt().unwrap();
let hash_str = hash(password, &salt);
assert!(verify(password, &hash_str));
assert!(!verify("wrong_password", &hash_str));
}
#[test]
fn test_generate_salt_length() {
let salt = generate_salt().unwrap();
assert_eq!(salt.len(), APR1_SALT_LEN);
}
#[test]
fn test_generate_salt_valid_chars() {
let salt = generate_salt().unwrap();
for ch in salt.chars() {
assert!(
ITOA64.contains(&(ch as u8)),
"Invalid salt character: {}",
ch
);
}
}
#[test]
fn test_salt_truncation() {
let password = "test";
let long_salt = "abcdefgh12345678";
let hash1 = hash(password, long_salt);
let hash2 = hash(password, "abcdefgh");
assert_eq!(hash1, hash2);
}
}