use std::{cmp::min, ops::RangeInclusive};
use md5::{Digest, Md5};
use crate::{
HashSetup, IntoHashSetup, consteq,
encode::{bcrypt_hash64_decode, md5_sha2_hash64_encode},
error::{Error, Result},
hash::{Hash, HashV},
parse::{self, HashIterator},
random,
};
pub(crate) const HASH_LENGTH_MIN: usize = MAGIC_LEN + 0 + 1 + 22;
pub(crate) const HASH_LENGTH_MAX: usize = MAGIC_LEN + 8 + 1 + 22;
pub(crate) const HASH_LENGTH: RangeInclusive<usize> = HASH_LENGTH_MIN..=HASH_LENGTH_MAX;
pub const MAX_SALT_LEN: usize = 8;
const MD5_MAGIC: &str = "$1$";
const MD5_TRANSPOSE: &[u8] = b"\x0c\x06\x00\x0d\x07\x01\x0e\x08\x02\x0f\x09\x03\x05\x0a\x04\x0b";
pub(crate) fn do_md5_crypt(pass: &[u8], salt: &str, magic: &str) -> Result<String> {
let mut dummy_buf = [0u8; 6];
bcrypt_hash64_decode(salt, &mut dummy_buf)?;
let mut dgst_b = Md5::new();
dgst_b.update(pass);
dgst_b.update(salt.as_bytes());
dgst_b.update(pass);
let mut hash_b = dgst_b.finalize();
let mut dgst_a = Md5::new();
dgst_a.update(pass);
dgst_a.update(magic.as_bytes());
dgst_a.update(salt.as_bytes());
let mut plen = pass.len();
while plen > 0 {
dgst_a.update(&hash_b[..min(plen, 16)]);
if plen < 16 {
break;
}
plen -= 16;
}
plen = pass.len();
while plen > 0 {
match plen & 1 {
0 => dgst_a.update(&pass[..1]),
1 => dgst_a.update([0u8]),
_ => unreachable!(),
}
plen >>= 1;
}
let mut hash_a = dgst_a.finalize();
for r in 0..1000 {
let mut dgst_a = Md5::new();
if r % 2 == 1 {
dgst_a.update(pass);
} else {
dgst_a.update(hash_a);
}
if r % 3 > 0 {
dgst_a.update(salt.as_bytes());
}
if r % 7 > 0 {
dgst_a.update(pass);
}
if r % 2 == 0 {
dgst_a.update(pass);
} else {
dgst_a.update(hash_a);
}
hash_a = dgst_a.finalize();
}
for (i, &ti) in MD5_TRANSPOSE.iter().enumerate() {
hash_b[i] = hash_a[ti as usize];
}
Ok(format!("{magic}{salt}${}", md5_sha2_hash64_encode(&hash_b)))
}
#[deprecated(since = "0.2.0", note = "don't use this algorithm for new passwords")]
#[inline]
pub fn hash<B: AsRef<[u8]>>(pass: B) -> Result<Hash> {
let saltstr = random::gen_salt_str(MAX_SALT_LEN);
let hash = do_md5_crypt(pass.as_ref(), &saltstr, MD5_MAGIC)?;
Ok(Hash::Md5(HashV(hash)))
}
const MAGIC_LEN: usize = 3;
fn parse_md5_hash(hash: &str) -> Result<HashSetup> {
let mut hs = parse::HashSlice::new(hash);
if hs.take(MAGIC_LEN).unwrap_or("X") != MD5_MAGIC {
return Err(Error::InvalidHashString);
}
let salt = hs.take_until(b'$').ok_or(Error::InvalidHashString)?;
Ok(HashSetup {
salt: Some(salt),
rounds: None,
})
}
#[deprecated(since = "0.2.0", note = "don't use this algorithm for new passwords")]
pub fn hash_with<'a, IHS, B>(param: IHS, pass: B) -> Result<Hash>
where
IHS: IntoHashSetup<'a>,
B: AsRef<[u8]>,
{
let hs = IHS::into_hash_setup(param, parse_md5_hash)?;
let salt = match hs.salt {
None => &random::gen_salt_str(MAX_SALT_LEN),
Some(salt) => (salt.len() <= MAX_SALT_LEN)
.then_some(salt)
.or_else(|| parse::HashSlice::new(salt).take(MAX_SALT_LEN))
.ok_or(Error::InvalidHashString)?,
};
let hash = do_md5_crypt(pass.as_ref(), salt, MD5_MAGIC)?;
Ok(Hash::Md5(HashV(hash)))
}
#[inline]
pub fn verify<B: AsRef<[u8]>>(pass: B, hash: &str) -> bool {
#[allow(deprecated)]
consteq(hash, hash_with(hash, pass))
}
#[cfg(test)]
mod tests {
use super::HashSetup;
#[test]
#[allow(deprecated)]
fn custom() {
assert_eq!(
super::hash_with("$1$5pZSV9va$azfrPr6af3Fc7dLblQXVa0", "password").unwrap(),
"$1$5pZSV9va$azfrPr6af3Fc7dLblQXVa0"
);
assert_eq!(
super::hash_with(
HashSetup {
salt: Some("5pZSV9va"),
rounds: None
},
"password"
)
.unwrap(),
"$1$5pZSV9va$azfrPr6af3Fc7dLblQXVa0"
);
}
}