AkurAI Build
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,
        })
    }
}