Skip to main content

rustauth_core/crypto/
password.rs

1//! Password hashing and verification.
2
3use rand::rngs::OsRng;
4use rand::RngCore;
5use scrypt::{scrypt, Params};
6use unicode_normalization::UnicodeNormalization;
7
8use crate::crypto::buffer::constant_time_equal;
9use crate::error::RustAuthError;
10
11const SALT_LEN: usize = 16;
12const HASH_LEN: usize = 64;
13
14fn scrypt_params() -> Result<Params, RustAuthError> {
15    Params::new(14, 16, 1, HASH_LEN).map_err(|error| RustAuthError::PasswordHash(error.to_string()))
16}
17
18fn normalize_password(password: &str) -> String {
19    password.nfkc().collect()
20}
21
22/// Hash a password using Better Auth's legacy-compatible scrypt format.
23pub fn hash_password(password: &str) -> Result<String, RustAuthError> {
24    let mut salt = [0_u8; SALT_LEN];
25    OsRng.fill_bytes(&mut salt);
26
27    let mut derived = [0_u8; HASH_LEN];
28    scrypt(
29        normalize_password(password).as_bytes(),
30        &salt,
31        &scrypt_params()?,
32        &mut derived,
33    )
34    .map_err(|error| RustAuthError::PasswordHash(error.to_string()))?;
35
36    Ok(format!("{}:{}", hex::encode(salt), hex::encode(derived)))
37}
38
39/// Verify a password against a `salt:hash` scrypt password hash.
40pub fn verify_password(hash: &str, password: &str) -> Result<bool, RustAuthError> {
41    let Some((salt_hex, hash_hex)) = hash.split_once(':') else {
42        return Err(RustAuthError::PasswordHash(
43            "password hash must use `salt:hash` format".to_owned(),
44        ));
45    };
46
47    let salt =
48        hex::decode(salt_hex).map_err(|error| RustAuthError::PasswordHash(error.to_string()))?;
49    let expected =
50        hex::decode(hash_hex).map_err(|error| RustAuthError::PasswordHash(error.to_string()))?;
51
52    if expected.len() != HASH_LEN {
53        return Err(RustAuthError::PasswordHash(format!(
54            "password hash must decode to {HASH_LEN} bytes"
55        )));
56    }
57
58    let mut derived = [0_u8; HASH_LEN];
59    scrypt(
60        normalize_password(password).as_bytes(),
61        &salt,
62        &scrypt_params()?,
63        &mut derived,
64    )
65    .map_err(|error| RustAuthError::PasswordHash(error.to_string()))?;
66
67    Ok(constant_time_equal(derived, expected))
68}