use parse_rust_core::{ErrorCode, ParseError};
pub const BCRYPT_COST: u32 = 10;
pub fn hash(password: &str) -> Result<String, ParseError> {
bcrypt::hash(password, BCRYPT_COST).map_err(|e| {
ParseError::new(
ErrorCode::InternalServerError,
format!("password hashing failed: {}", kind_of(&e)),
)
})
}
pub fn verify(password: &str, hashed: &str) -> bool {
if password.is_empty() || hashed.is_empty() {
return false;
}
bcrypt::verify(password, hashed).unwrap_or(false)
}
fn kind_of(e: &bcrypt::BcryptError) -> &'static str {
match e {
bcrypt::BcryptError::CostNotAllowed(_) => "cost not allowed",
bcrypt::BcryptError::InvalidHash(_) => "invalid hash",
_ => "internal",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips() {
let h = hash("hunter2").expect("hash");
assert!(verify("hunter2", &h));
assert!(!verify("hunter3", &h));
}
#[test]
fn uses_upstreams_cost_factor() {
let h = hash("x").expect("hash");
let cost = h.split('$').nth(2).expect("cost field");
assert_eq!(
cost, "10",
"cost must match upstream's bcrypt.hash(password, 10)"
);
}
#[test]
fn empty_inputs_are_a_failed_login_not_an_error() {
let h = hash("x").expect("hash");
assert!(!verify("", &h));
assert!(!verify("x", ""));
}
#[test]
fn a_corrupt_stored_hash_fails_login_rather_than_panicking() {
assert!(!verify("x", "not-a-bcrypt-hash"));
assert!(!verify("x", "$2b$10$tooshort"));
}
}