Menu
Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8
AkurAI-Framework / crates/crypto/src/password.rs
Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson
//! Password hashing: PBKDF2-HMAC-SHA-256 in a PHC-like encoded string,
//! `pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>`.
use crate::{ct_eq, from_hex, pbkdf2_sha256};
/// PBKDF2 rounds for new hashes (OWASP 2023 guidance for HMAC-SHA-256).
pub const PASSWORD_ITERATIONS: u32 = 210_000;
const SCHEME: &str = "pbkdf2-sha256";
#[cfg(not(target_arch = "wasm32"))]
const SALT_LEN: usize = 16;
#[cfg(not(target_arch = "wasm32"))]
const HASH_LEN: usize = 32;
/// Hash `password` with a fresh 16-byte random salt and
/// [`PASSWORD_ITERATIONS`] rounds; returns the encoded string.
#[cfg(not(target_arch = "wasm32"))]
pub fn hash_password(password: &str) -> std::io::Result<String> {
let mut salt = [0u8; SALT_LEN];
crate::random_bytes(&mut salt)?;
let mut hash = [0u8; HASH_LEN];
pbkdf2_sha256(password.as_bytes(), &salt, PASSWORD_ITERATIONS, &mut hash);
Ok(format!(
"{SCHEME}${PASSWORD_ITERATIONS}${}${}",
crate::hex(&salt),
crate::hex(&hash)
))
}
/// `true` iff `encoded` is a well-formed hash string and `password` matches
/// it. Malformed input is simply a mismatch.
pub fn verify_password(password: &str, encoded: &str) -> bool {
let Some(parsed) = Encoded::parse(encoded) else {
return false;
};
let mut actual = vec![0u8; parsed.hash.len()];
pbkdf2_sha256(
password.as_bytes(),
&parsed.salt,
parsed.iterations,
&mut actual,
);
ct_eq(&actual, &parsed.hash)
}
struct Encoded {
iterations: u32,
salt: Vec<u8>,
hash: Vec<u8>,
}
impl Encoded {
fn parse(s: &str) -> Option<Self> {
let mut parts = s.split('$');
let (scheme, iterations, salt, hash) =
(parts.next()?, parts.next()?, parts.next()?, parts.next()?);
if scheme != SCHEME || parts.next().is_some() {
return None;
}
let iterations = iterations.parse::<u32>().ok().filter(|&n| n > 0)?;
let hash = from_hex(hash).filter(|h| !h.is_empty())?;
Some(Self {
iterations,
salt: from_hex(salt)?,
hash,
})
}
}