use super::{RcryptError, RcryptResult};
use blowfish::Blowfish;
const EXPECTED_PARTS: usize = 3;
const RCRYPT_BMCF_EXPECTED_SIZE: usize = 40;
const BCRYPT_EXPECTED_SIZE: usize = 60;
const BCRYPT_EXPECTED_SIZE_SALTDIGEST: usize = 53;
const BCRYPT_EXPECTED_SIZE_FIELDS: usize = 2;
const MIN_COST: u32 = 4;
const MAX_COST: u32 = 31;
type Digest = [u8; 24];
type Salt = [u8; 16];
pub fn encode_into_bmcf(input: &str) -> RcryptResult<Vec<u8>> {
if input.len() != BCRYPT_EXPECTED_SIZE {
return Err(RcryptError::WrongSize(BCRYPT_EXPECTED_SIZE, input.len()));
}
if input.as_bytes()[0] != b'$' {
return Err(RcryptError::UnsupportedHashPrefix(input.as_bytes()[0]));
}
let mut buf: Vec<u8> = Vec::with_capacity(RCRYPT_BMCF_EXPECTED_SIZE);
let parts: Vec<&str> = input.split('$').filter(|s| !s.is_empty()).collect();
if parts.len() != EXPECTED_PARTS {
return Err(RcryptError::CorruptedHash(format!(
"Expected 3 parts, found {}",
parts.len()
)));
}
match (parts[0].len(), parts[1].len(), parts[2].len()) {
(
BCRYPT_EXPECTED_SIZE_FIELDS,
BCRYPT_EXPECTED_SIZE_FIELDS,
BCRYPT_EXPECTED_SIZE_SALTDIGEST,
) => {}
(p1l, p2l, p3l) => {
return Err(RcryptError::CorruptedHash(format!(
"Expected 3 parts with lengths {}, {}, {}. Found lengths {}, {} and {} instead",
BCRYPT_EXPECTED_SIZE_FIELDS,
BCRYPT_EXPECTED_SIZE_FIELDS,
BCRYPT_EXPECTED_SIZE_SALTDIGEST,
p1l,
p2l,
p3l
)))
}
}
let scheme = parts[0];
let cost: u8 = parts[1]
.parse()
.map_err(|_| RcryptError::BadDecodedCost(parts[1].to_owned()))?;
let salt = &parts[2][0..22];
let digest = &parts[2][22..];
let salt_d = base64::decode_config(&salt, base64::BCRYPT)?;
let digest_d = base64::decode_config(&digest, base64::BCRYPT)?;
let mask = cost & 0x1F;
match scheme {
"2" => buf.push(0x20 | mask),
"2a" => buf.push(0x40 | mask),
"2x" => buf.push(0x60 | mask),
"2y" => buf.push(0x80 | mask),
"2b" => buf.push(0xA0 | mask),
_ => return Err(RcryptError::UnknownScheme(scheme.to_owned())),
};
buf.extend(salt_d);
buf.extend(digest_d);
Ok(buf)
}
pub fn decode_into_mcf(input: &[u8]) -> RcryptResult<String> {
if input.len() != RCRYPT_BMCF_EXPECTED_SIZE {
return Err(RcryptError::WrongSize(
RCRYPT_BMCF_EXPECTED_SIZE,
input.len(),
));
}
let mut st: Vec<u8> = Vec::with_capacity(BCRYPT_EXPECTED_SIZE);
st.push(b'$');
let header_octet = input[0];
let scheme_id = header_octet & 0xE0;
match scheme_id {
0x20 => st.push(b'2'),
0x40 => st.extend(b"2a"),
0x60 => st.extend(b"2x"),
0x80 => st.extend(b"2y"),
0xA0 => st.extend(b"2b"),
_ => return Err(RcryptError::UnknownScheme(scheme_id.to_string())),
};
st.push(b'$');
let costint = header_octet - scheme_id;
if costint > 31 {
return Err(RcryptError::BadDecodedCost(format!(
"expected cost is 4-31, found {}",
costint
)));
}
st.push((costint / 10) + 48);
st.push((costint % 10) + 48);
st.push(b'$');
let salt = base64::encode_config(&input[1..17], base64::BCRYPT);
st.extend(salt.bytes());
let digest = base64::encode_config(&input[17..], base64::BCRYPT);
st.extend(digest.bytes());
Ok(unsafe { String::from_utf8_unchecked(st) })
}
fn gensalt() -> Result<Salt, getrandom::Error> {
let mut s = [0u8; 16];
getrandom::getrandom(&mut s).map(|_| s)
}
fn rcrypt_genhash(password: &[u8], cost: u32, salt: &[u8]) -> RcryptResult<Digest> {
if cost > MAX_COST || cost < MIN_COST {
return Err(RcryptError::DisallowedCost(cost));
}
if salt.len() != 16 {
return Err(RcryptError::BadSalt(salt.len()));
}
if password.contains(&0) || password.is_empty() {
return Err(RcryptError::BadPassword);
}
let trunc_password = if password.len() > 72 {
&password[..72]
} else {
&password
};
let mut null_terminated_password = Vec::with_capacity(trunc_password.len() + 1);
null_terminated_password.extend(trunc_password);
null_terminated_password.push(0);
let mut digest = [0u8; 24];
let mut state = Blowfish::bc_init_state();
state.salted_expand_key(&salt, &null_terminated_password);
for _ in 0..1u32 << cost {
state.bc_expand_key(&null_terminated_password);
state.bc_expand_key(&salt);
}
let mut magic_cipher = [
0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274,
];
for i in 0..3 {
let i: usize = i * 2;
for _ in 0..64 {
let [l, r] = state.bc_encrypt([magic_cipher[i], magic_cipher[i + 1]]);
magic_cipher[i] = l;
magic_cipher[i + 1] = r;
}
let buf = magic_cipher[i].to_be_bytes();
digest[i * 4..][..4].copy_from_slice(&buf);
let buf = magic_cipher[i + 1].to_be_bytes();
digest[(i + 1) * 4..][..4].copy_from_slice(&buf);
}
Ok(digest)
}
pub fn rcrypt_hash_with(password: &[u8], cost: u32, salt: &[u8]) -> RcryptResult<Vec<u8>> {
let digest = self::rcrypt_genhash(password.as_ref(), cost, &salt)?;
let mut buf = Vec::with_capacity(RCRYPT_BMCF_EXPECTED_SIZE);
let mask = (cost as u8) & 0x1F;
buf.push(0xA0 | mask);
buf.extend(salt);
buf.extend(&digest[..23]);
Ok(buf)
}
pub fn rcrypt_hash(password: &[u8], cost: u32) -> RcryptResult<Vec<u8>> {
self::rcrypt_hash_with(password, cost, &self::gensalt()?)
}
pub fn rcrypt_verify(password: &[u8], hash: &[u8]) -> RcryptResult<bool> {
if hash.len() != RCRYPT_BMCF_EXPECTED_SIZE {
return Err(RcryptError::WrongSize(
RCRYPT_BMCF_EXPECTED_SIZE,
hash.len(),
));
}
let salt = &hash[1..17];
let digest = &hash[17..];
let header_octet = hash[0];
let scheme_id = header_octet & 0xE0;
match scheme_id {
0x20 | 0x40 | 0x60 | 0x80 | 0xA0 => {}
_ => {
return Err(RcryptError::UnknownScheme(format!(
"Expected valid rcrypt scheme ID, got {}",
scheme_id
)))
}
}
let costint = header_octet - scheme_id;
if (costint as u32) > MAX_COST || (costint as u32) < MIN_COST {
return Err(RcryptError::BadDecodedCost(format!(
"Expected cost in {min}-{max}, got {cost}",
min = MIN_COST,
max = MAX_COST,
cost = costint
)));
}
let this_digest = self::rcrypt_genhash(password, costint as u32, salt)?;
let mut delta = 0;
for (a, b) in digest.into_iter().zip(this_digest) {
delta |= a ^ b;
}
Ok(delta == 0)
}