use super::{Result, HashSetup, consteq};
use crate::enc_dec::{bcrypt_hash64_encode,bcrypt_hash64_decode};
use crate::error::Error;
use crate::random;
use crate::parse::{self, HashIterator};
use std::{iter, fmt};
use std::cmp::min;
use std::default::Default;
use blowfish::Blowfish;
use byteorder::{BE, ByteOrder};
const MAX_PASS_LEN: usize = 72;
const DEFAULT_VARIANT: BcryptVariant = BcryptVariant::V2b;
const ENC_SALT_LEN: usize = 22;
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 {
write!(f, "{}", match *self {
BcryptVariant::V2a => "2a",
BcryptVariant::V2b => "2b",
BcryptVariant::V2y => "2y",
})
}
}
pub struct BcryptSetup<'a> {
pub salt: Option<&'a str>,
pub cost: Option<u32>,
pub variant: Option<BcryptVariant>,
}
pub trait IntoBcryptSetup<'a> {
fn into_bcrypt_setup(self) -> Result<BcryptSetup<'a>>;
}
const MAGIC_LEN: usize = 4;
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 = if let Some(cost_str) = hs.take_until(b'$') {
if cost_str.len() != 2 {
return Err(Error::InvalidHashString);
}
let cost = cost_str.parse::<u32>().map_err(|_e| Error::InvalidRounds)?;
if cost < 10 && !cost_str.starts_with('0') {
return Err(Error::InvalidHashString);
}
cost
} else {
return Err(Error::InvalidHashString);
};
let salt = if let Some(salt) = hs.take(ENC_SALT_LEN) {
salt
} else {
return Err(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 = pass.iter().copied().chain(iter::repeat(0u8)).take(min(pass.len() + 1, MAX_PASS_LEN)).collect::<Vec<_>>();
let mut output = [0u8; 24];
bcrypt(cost, &salt, &upd_pass[..], &mut output);
for b in &mut upd_pass {
*b = 0u8;
}
Ok(format!("${}${:02}${}{}", variant, cost,
bcrypt_hash64_encode(&salt), bcrypt_hash64_encode(&output[..23])))
}
pub fn hash<B: AsRef<[u8]>>(pass: B) -> Result<String> {
let mut salt_buf = [0u8; 16];
random::gen_salt_bytes(&mut salt_buf);
do_bcrypt(pass.as_ref(), &salt_buf, DEFAULT_COST, DEFAULT_VARIANT)
}
pub fn hash_with<'a, IBS, B>(param: IBS, pass: B) -> Result<String>
where IBS: IntoBcryptSetup<'a>, B: AsRef<[u8]>
{
let bs = param.into_bcrypt_setup()?;
let cost = if let Some(c) = bs.cost {
if c < MIN_COST || c > MAX_COST {
return Err(Error::InvalidRounds);
}
c
} else { DEFAULT_COST };
let variant = if let Some(v) = bs.variant {
v
} else { DEFAULT_VARIANT };
let mut salt_buf = [0u8; 16];
if bs.salt.is_some() {
bcrypt_hash64_decode(bs.salt.unwrap(), &mut salt_buf)?;
} else {
random::gen_salt_bytes(&mut salt_buf);
}
do_bcrypt(pass.as_ref(), &salt_buf, cost, variant)
}
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());
}
}