use password_hash::{PasswordVerifier, phc};
use crate::{
errors::Error,
secret::{PasswordHash, Secret},
};
pub const SESSION_TOKEN_BYTES: usize = 18;
pub const CHALLENGE_CODE_BYTES: usize = 18;
pub const TEMPORARY_PASSWORD_BYTES: usize = 9;
pub(crate) fn check_password(stored_hash: &PasswordHash, given_password: &Secret) -> Result<bool, Error> {
let Some(stored_hash) = stored_hash.expose() else {
return Ok(false);
};
let hash = phc::PasswordHash::new(stored_hash)
.map_err(Error::StoredPasswordHash)?;
let algs: &[&dyn PasswordVerifier<phc::PasswordHash>] = &[
&argon2::Argon2::default(),
#[cfg(feature = "pbkdf2")] &pbkdf2::Pbkdf2::default(),
#[cfg(feature = "scrypt")] &scrypt::Scrypt::default(),
];
let given_password_bytes = given_password.expose().as_bytes();
for alg in algs {
use password_hash::Error as E;
let result = alg.verify_password(given_password_bytes, &hash);
match result {
Ok(()) => return Ok(true),
Err(E::PasswordInvalid) => return Ok(false),
Err(_) => continue,
}
}
Ok(false)
}
pub(crate) fn generate_password_hash(new_password: &Secret) -> Result<PasswordHash, Error> {
use argon2::{Argon2, PasswordHasher};
let salt = phc::Salt::from_rng(&mut rand::rng());
let hash = Argon2::default()
.hash_password_with_salt(new_password.expose().as_bytes(), &salt)
.map_err(Error::NewPasswordHash)?;
Ok(PasswordHash(Some(Secret(hash.to_string()))))
}
pub(crate) fn generate_password_and_hash() -> Result<(Secret, PasswordHash), Error> {
let raw = generate_base64_token::<TEMPORARY_PASSWORD_BYTES>();
let hash = generate_password_hash(&raw)?;
Ok((raw, hash))
}
pub(crate) fn generate_session_token_and_hash() -> (Secret, Secret) {
generate_token_and_fast_hash::<SESSION_TOKEN_BYTES>()
}
pub(crate) fn generate_challenge_code_and_hash() -> (Secret, Secret) {
generate_token_and_fast_hash::<CHALLENGE_CODE_BYTES>()
}
fn generate_token_and_fast_hash<const N: usize>() -> (Secret, Secret) {
let raw = generate_base64_token::<N>();
let hash = fast_hash(&raw);
(raw, hash)
}
pub(crate) fn check_fast_hash(raw: &Secret, hash: &Secret) -> bool {
let hash2 = fast_hash(raw);
constant_time_eq::constant_time_eq(hash.0.as_bytes(), hash2.0.as_bytes())
}
fn fast_hash(s: &Secret) -> Secret {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&s.0);
let hash = &hasher.finalize();
Secret(base64_encode(hash))
}
fn generate_base64_token<const N: usize>() -> Secret {
let mut bytes = [0u8; N];
_generate_base64_token(&mut bytes)
}
fn _generate_base64_token(buffer: &mut [u8]) -> Secret {
use rand::{rng, RngExt};
rng().fill(buffer);
Secret(base64_encode(buffer))
}
fn base64_encode(bytes: &[u8]) -> String {
use base64::{engine::general_purpose::URL_SAFE, Engine};
URL_SAFE.encode(bytes)
}
#[cfg(test)]
mod test {
use super::{
check_fast_hash, fast_hash, generate_password_hash, generate_token_and_fast_hash,
check_password, Secret,
};
#[test]
fn test_password_hash() {
let password = Secret("example".to_string());
let wrong_password = Secret("something else".to_string());
let hash = generate_password_hash(&password).unwrap();
assert!(check_password(&hash, &password).expect("Correct password should verify"));
assert!(!check_password(&hash, &wrong_password).expect("Incorrect password should at least compare"));
}
#[test]
fn test_token_hash() {
let (raw, hash) = generate_token_and_fast_hash::<18>();
assert_eq!(
&hash.0,
&fast_hash(&raw).0,
"Hash of raw token should equal the generated hash",
);
assert!(check_fast_hash(&raw, &hash));
}
#[test]
fn test_hash_distinct() {
let secret1 = Secret("example".to_string());
let secret2 = Secret("something else".to_string());
let hash1 = fast_hash(&secret1);
let hash2 = fast_hash(&secret2);
assert_ne!(&hash1.0, &hash2.0, "Hashes should be distinct");
assert!(check_fast_hash(&secret1, &hash1));
assert!(!check_fast_hash(&secret2, &hash1));
assert!(!check_fast_hash(&secret1, &hash2));
assert!(check_fast_hash(&secret2, &hash2));
}
}