use std::cmp::min;
use std::default::Default;
use std::{fmt, iter};
use blowfish::Blowfish;
use byteorder::{BE, ByteOrder};
use crate::{
HashSetup, consteq,
encode::{bcrypt_hash64_decode, bcrypt_hash64_encode},
error::{Error, Result},
hash::{Hash, HashV},
parse::{self, HashIterator},
random,
};
const MAX_PASS_LEN: usize = 72;
const DEFAULT_VARIANT: BcryptVariant = BcryptVariant::V2b;
const ENC_SALT_LEN: usize = 22;
const MAGIC_LEN: usize = 4;
pub(crate) const HASH_LENGTH: usize = MAGIC_LEN + 2 + 22 + 1 + 31;
pub const MIN_COST: u32 = 4;
pub const MAX_COST: u32 = 31;
pub const DEFAULT_COST: u32 = 10;
pub enum BcryptVariant {
V2a,
V2b,
V2y,
}
impl fmt::Display for BcryptVariant {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let variant = match self {
BcryptVariant::V2a => "2a",
BcryptVariant::V2b => "2b",
BcryptVariant::V2y => "2y",
};
write!(f, "{variant}")
}
}
pub struct BcryptSetup<'a> {
pub salt: Option<&'a str>,
pub cost: Option<u32>,
pub variant: Option<BcryptVariant>,
}
impl<'a> BcryptSetup<'a> {
pub fn salt(mut self, salt: &'a str) -> Self {
self.salt = Some(salt);
self
}
pub fn cost(mut self, cost: u32) -> Self {
self.cost = Some(cost);
self
}
pub fn variant(mut self, variant: BcryptVariant) -> Self {
self.variant = Some(variant);
self
}
}
pub trait IntoBcryptSetup<'a> {
fn into_bcrypt_setup(self) -> Result<BcryptSetup<'a>>;
}
impl<'a> IntoBcryptSetup<'a> for &'a str {
fn into_bcrypt_setup(self) -> Result<BcryptSetup<'a>> {
let mut hs = parse::HashSlice::new(self);
let variant = match hs.take(MAGIC_LEN).unwrap_or("X") {
"$2a$" => BcryptVariant::V2a,
"$2b$" => BcryptVariant::V2b,
"$2y$" => BcryptVariant::V2y,
_ => return Err(Error::InvalidHashString),
};
let cost_str = hs.take_until(b'$').ok_or(Error::InvalidHashString)?;
if cost_str.len() != 2 {
return Err(Error::InvalidHashString);
}
let cost: u32 = cost_str.parse().map_err(|_e| Error::InvalidRounds)?;
if cost < 10 && !cost_str.starts_with('0') {
return Err(Error::InvalidHashString);
}
let salt = hs.take(ENC_SALT_LEN).ok_or(Error::InvalidHashString)?;
Ok(BcryptSetup {
salt: Some(salt),
cost: Some(cost),
variant: Some(variant),
})
}
}
impl<'a> IntoBcryptSetup<'a> for HashSetup<'a> {
fn into_bcrypt_setup(self) -> Result<BcryptSetup<'a>> {
Ok(BcryptSetup {
salt: self.salt,
cost: self.rounds,
variant: Some(DEFAULT_VARIANT),
})
}
}
impl<'a> IntoBcryptSetup<'a> for BcryptSetup<'a> {
fn into_bcrypt_setup(self) -> Result<BcryptSetup<'a>> {
Ok(self)
}
}
impl<'a> Default for BcryptSetup<'a> {
fn default() -> Self {
BcryptSetup {
salt: None,
cost: Some(DEFAULT_COST),
variant: Some(DEFAULT_VARIANT),
}
}
}
fn bcrypt(cost: u32, salt: &[u8], password: &[u8], output: &mut [u8]) {
assert!(cost < 32);
assert!(salt.len() == 16);
assert!(password.len() <= 72 && !password.is_empty());
assert!(output.len() == 24);
let mut state = Blowfish::bc_init_state();
state.salted_expand_key(salt, password);
for _ in 0..1u32 << cost {
state.bc_expand_key(password);
state.bc_expand_key(salt);
}
let mut ctext = [
0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274,
];
for i in (0..6).step_by(2) {
for _ in 0..64 {
let [l, r] = state.bc_encrypt([ctext[i], ctext[i + 1]]);
ctext[i] = l;
ctext[i + 1] = r;
}
BE::write_u32(&mut output[i * 4..(i + 1) * 4], ctext[i]);
BE::write_u32(&mut output[(i + 1) * 4..(i + 2) * 4], ctext[i + 1]);
}
}
fn do_bcrypt(pass: &[u8], salt: &[u8], cost: u32, variant: BcryptVariant) -> Result<String> {
let mut upd_pass: Vec<_> = pass
.iter()
.copied()
.chain(iter::repeat(0u8))
.take(min(pass.len() + 1, MAX_PASS_LEN))
.collect();
let mut output = [0u8; 24];
bcrypt(cost, salt, &upd_pass[..], &mut output);
upd_pass.fill(0u8);
Ok(format!(
"${variant}${cost:02}${}{}",
bcrypt_hash64_encode(salt),
bcrypt_hash64_encode(&output[..23])
))
}
#[inline]
pub fn hash<B: AsRef<[u8]>>(pass: B) -> Result<Hash> {
let mut salt_buf = [0u8; 16];
random::gen_salt_bytes(&mut salt_buf);
let hash = do_bcrypt(pass.as_ref(), &salt_buf, DEFAULT_COST, DEFAULT_VARIANT)?;
Ok(Hash::Bcrypt(HashV(hash)))
}
pub fn hash_with<'a, IBS, B>(param: IBS, pass: B) -> Result<Hash>
where
IBS: IntoBcryptSetup<'a>,
B: AsRef<[u8]>,
{
let bs = param.into_bcrypt_setup()?;
let cost = if let Some(c) = bs.cost {
if !(MIN_COST..=MAX_COST).contains(&c) {
return Err(Error::InvalidRounds);
}
c
} else {
DEFAULT_COST
};
let variant = bs.variant.unwrap_or(DEFAULT_VARIANT);
let mut salt_buf = [0u8; 16];
match bs.salt {
Some(salt) => bcrypt_hash64_decode(salt, &mut salt_buf)?,
None => random::gen_salt_bytes(&mut salt_buf),
}
let hash = do_bcrypt(pass.as_ref(), &salt_buf, cost, variant)?;
Ok(Hash::Bcrypt(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::{BcryptSetup, BcryptVariant};
#[test]
fn variant() {
assert_eq!(
"$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe",
super::hash_with(
BcryptSetup {
salt: Some("bvIG6Nmid91Mu9RcmmWZfO"),
cost: Some(5),
variant: Some(BcryptVariant::V2y)
},
"password"
)
.unwrap()
);
}
}