use std::ops::RangeInclusive;
use hmac::{Hmac, Mac};
use sha1::Sha1;
use crate::{
HashSetup, IntoHashSetup, consteq,
encode::{bcrypt_hash64_decode, sha1crypt_hash64_encode},
error::{Error, Result},
hash::{Hash, HashV},
parse::{self, HashIterator},
random,
};
const MIN_ROUNDS: u32 = 1;
const MAX_SALT_LEN: usize = 64;
pub(crate) const HASH_LENGTH_MIN: usize = MAGIC_LEN + 1 + 1 + 1 + 1 + 28;
pub(crate) const HASH_LENGTH_MAX: usize = MAGIC_LEN + 9 + 1 + 64 + 1 + 28;
pub(crate) const HASH_LENGTH: RangeInclusive<usize> = HASH_LENGTH_MIN..=HASH_LENGTH_MAX;
pub const DEFAULT_ROUNDS: u32 = 24680;
pub const DEFAULT_SALT_LEN: usize = 8;
fn do_sha1_crypt(pass: &[u8], salt: &str, rounds: u32) -> Result<String> {
let mut dummy_buf = [0u8; 48];
bcrypt_hash64_decode(salt, &mut dummy_buf)?;
let mut hmac = Hmac::<Sha1>::new_from_slice(pass).map_err(|_| Error::InsufficientLength)?;
hmac.update(format!("{salt}$sha1${rounds}").as_bytes());
let mut result = hmac.finalize();
for _ in 1..rounds {
hmac = Hmac::new_from_slice(pass).map_err(|_| Error::InsufficientLength)?;
hmac.update(&result.into_bytes());
result = hmac.finalize();
}
Ok(format!(
"$sha1${rounds}${salt}${}",
sha1crypt_hash64_encode(&result.into_bytes())
))
}
#[inline]
pub fn hash<B: AsRef<[u8]>>(pass: B) -> Result<Hash> {
let saltstr = random::gen_salt_str(DEFAULT_SALT_LEN);
let hash = do_sha1_crypt(pass.as_ref(), &saltstr, random::vary_rounds(DEFAULT_ROUNDS))?;
Ok(Hash::Sha1(HashV(hash)))
}
const MAGIC_LEN: usize = 6;
fn parse_sha1_hash(hash: &str) -> Result<HashSetup> {
let mut hs = parse::HashSlice::new(hash);
if hs.take(MAGIC_LEN).unwrap_or("X") != "$sha1$" {
return Err(Error::InvalidHashString);
}
let rounds = hs
.take_until(b'$')
.ok_or(Error::InvalidHashString)?
.parse::<u32>()
.map_err(|_e| Error::InvalidRounds)?;
let salt = hs.take_until(b'$').ok_or(Error::InvalidHashString)?;
Ok(HashSetup {
salt: Some(salt),
rounds: Some(rounds),
})
}
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_sha1_hash)?;
let rounds = if let Some(r) = hs.rounds {
if r < MIN_ROUNDS {
return Err(Error::InvalidRounds);
}
r
} else {
random::vary_rounds(DEFAULT_ROUNDS)
};
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_sha1_crypt(pass.as_ref(), &salt, rounds)?;
Ok(Hash::Sha1(HashV(hash)))
}
#[inline]
pub fn verify<B: AsRef<[u8]>>(pass: B, hash: &str) -> bool {
consteq(hash, hash_with(hash, pass))
}
#[cfg(test)]
mod tests {
use super::HashSetup;
#[test]
fn custom() {
assert_eq!(
super::hash_with(
"$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a",
"password"
)
.unwrap(),
"$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"
);
assert_eq!(
super::hash_with(
HashSetup {
salt: Some("iVdJqfSE"),
rounds: Some(19703)
},
"password"
)
.unwrap(),
"$sha1$19703$iVdJqfSE$v4qYKl1zqYThwpjJAoKX6UvlHq/a"
);
}
#[test]
#[should_panic(expected = "value: InvalidRounds")]
fn bad_rounds() {
let _ = super::hash_with(
HashSetup {
salt: Some("K0Ay"),
rounds: Some(0),
},
"password",
)
.unwrap();
}
}