use rand::rngs::OsRng;
use rand::RngCore;
use scrypt::{scrypt, Params};
use unicode_normalization::UnicodeNormalization;
use crate::crypto::buffer::constant_time_equal;
use crate::error::OpenAuthError;
const SALT_LEN: usize = 16;
const HASH_LEN: usize = 64;
fn scrypt_params() -> Result<Params, OpenAuthError> {
Params::new(14, 16, 1, HASH_LEN).map_err(|error| OpenAuthError::PasswordHash(error.to_string()))
}
fn normalize_password(password: &str) -> String {
password.nfkc().collect()
}
pub fn hash_password(password: &str) -> Result<String, OpenAuthError> {
let mut salt = [0_u8; SALT_LEN];
OsRng.fill_bytes(&mut salt);
let mut derived = [0_u8; HASH_LEN];
scrypt(
normalize_password(password).as_bytes(),
&salt,
&scrypt_params()?,
&mut derived,
)
.map_err(|error| OpenAuthError::PasswordHash(error.to_string()))?;
Ok(format!("{}:{}", hex::encode(salt), hex::encode(derived)))
}
pub fn verify_password(hash: &str, password: &str) -> Result<bool, OpenAuthError> {
let Some((salt_hex, hash_hex)) = hash.split_once(':') else {
return Err(OpenAuthError::PasswordHash(
"password hash must use `salt:hash` format".to_owned(),
));
};
let salt =
hex::decode(salt_hex).map_err(|error| OpenAuthError::PasswordHash(error.to_string()))?;
let expected =
hex::decode(hash_hex).map_err(|error| OpenAuthError::PasswordHash(error.to_string()))?;
if expected.len() != HASH_LEN {
return Err(OpenAuthError::PasswordHash(format!(
"password hash must decode to {HASH_LEN} bytes"
)));
}
let mut derived = [0_u8; HASH_LEN];
scrypt(
normalize_password(password).as_bytes(),
&salt,
&scrypt_params()?,
&mut derived,
)
.map_err(|error| OpenAuthError::PasswordHash(error.to_string()))?;
Ok(constant_time_equal(derived, expected))
}