rustauth_core/crypto/
password.rs1use 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
22pub 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
39pub 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}