use argon2::Argon2;
use password_hash::{
PasswordHasher,
PasswordVerifier,
SaltString,
};
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.0 else {
return Ok(false);
};
let hash = password_hash::PasswordHash::new(&stored_hash.0)
.map_err(Error::Hasher)?;
let algs: &[&dyn PasswordVerifier] = &[&Argon2::default()];
let result = hash.verify_password(algs, &given_password.0);
match result {
Ok(()) => Ok(true),
Err(password_hash::Error::Password) => Ok(false),
Err(e) => Err(Error::Hasher(e)),
}
}
pub(crate) fn generate_password_hash(new_password: &Secret) -> Result<PasswordHash, Error> {
let salt = SaltString::generate(rand::thread_rng());
let hash = Argon2::default()
.hash_password(new_password.0.as_bytes(), &salt)
.map_err(Error::Hasher)?;
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 {
use rand::{thread_rng, Rng};
let mut bytes = [0u8; N];
thread_rng().fill(&mut bytes as &mut [u8]);
Secret(base64_encode(&bytes))
}
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));
}
}