rskit_auth/password/
hasher.rs1use argon2::{
2 Algorithm, Argon2, Params, Version,
3 password_hash::{PasswordHash, PasswordHasher as Argon2Hasher, PasswordVerifier, SaltString},
4};
5use rskit_errors::{AppError, AppResult};
6
7const ARGON2_MEMORY_COST_KIB: u32 = 65_536;
8const ARGON2_TIME_COST: u32 = 3;
9const ARGON2_PARALLELISM: u32 = 4;
10const ARGON2_HASH_LEN: usize = 32;
11
12#[derive(Debug, Clone, Default)]
14#[non_exhaustive]
15pub enum HashAlgorithm {
16 #[default]
18 Argon2id,
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct PasswordHasher {
24 pub algorithm: HashAlgorithm,
26}
27
28impl PasswordHasher {
29 pub const fn new(algorithm: HashAlgorithm) -> Self {
31 Self { algorithm }
32 }
33
34 pub fn hash(&self, password: &str) -> AppResult<String> {
36 let mut rng = argon2::password_hash::rand_core::OsRng;
37 let salt = SaltString::generate(&mut rng);
38 let argon2 = configured_argon2()?;
39 argon2
40 .hash_password(password.as_bytes(), &salt)
41 .map(|h| h.to_string())
42 .map_err(|e| {
43 AppError::new(
44 rskit_errors::ErrorCode::Internal,
45 format!("password hash error: {e}"),
46 )
47 })
48 }
49
50 pub fn verify(&self, password: &str, hash: &str) -> AppResult<bool> {
52 let parsed = PasswordHash::new(hash).map_err(|e| {
53 AppError::new(
54 rskit_errors::ErrorCode::Internal,
55 format!("invalid hash format: {e}"),
56 )
57 })?;
58 Ok(configured_argon2()?
59 .verify_password(password.as_bytes(), &parsed)
60 .is_ok())
61 }
62}
63
64fn configured_argon2() -> AppResult<Argon2<'static>> {
65 let params = Params::new(
66 ARGON2_MEMORY_COST_KIB,
67 ARGON2_TIME_COST,
68 ARGON2_PARALLELISM,
69 Some(ARGON2_HASH_LEN),
70 )
71 .map_err(|_e| AppError::internal(std::io::Error::other("invalid argon2 params")))?;
72 Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn hash_and_verify_roundtrip() {
81 let h = PasswordHasher::default();
82 let hash = h.hash("hunter2").unwrap();
83 assert!(h.verify("hunter2", &hash).unwrap());
84 assert!(!h.verify("wrong", &hash).unwrap());
85 }
86
87 #[test]
88 fn hash_uses_locked_group_05_parameters() {
89 let hash = PasswordHasher::default().hash("hunter2hunter2").unwrap();
90 assert!(hash.contains("$argon2id$"));
91 assert!(hash.contains("m=65536"));
92 assert!(hash.contains("t=3"));
93 assert!(hash.contains("p=4"));
94 }
95}