Skip to main content

reifydb_auth/
password.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::collections::HashMap;
5
6use argon2::{
7	Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version,
8	password_hash::Error as PasswordHashError,
9};
10use reifydb_core::interface::auth::AuthenticationProvider;
11use reifydb_type::{Result, error::Error};
12
13use crate::error::AuthError;
14
15pub struct PasswordProvider;
16
17/// OWASP-recommended Argon2id parameters:
18/// 19 MiB memory, 2 iterations, parallelism 1, 32-byte output.
19fn argon2_instance() -> Argon2<'static> {
20	let params = Params::new(19 * 1024, 2, 1, Some(32)).expect("valid Argon2 params");
21	Argon2::new(Algorithm::Argon2id, Version::V0x13, params)
22}
23
24impl AuthenticationProvider for PasswordProvider {
25	fn method(&self) -> &str {
26		"password"
27	}
28
29	fn create(&self, config: &HashMap<String, String>) -> Result<HashMap<String, String>> {
30		let password = config.get("password").ok_or_else(|| Error::from(AuthError::PasswordRequired))?;
31
32		let argon2 = argon2_instance();
33
34		let phc = argon2
35			.hash_password(password.as_bytes())
36			.map_err(|e| {
37				Error::from(AuthError::HashingFailed {
38					reason: e.to_string(),
39				})
40			})?
41			.to_string();
42
43		Ok(HashMap::from([("phc".into(), phc), ("algorithm_version".into(), "1".into())]))
44	}
45
46	fn validate(&self, stored: &HashMap<String, String>, credential: &str) -> Result<bool> {
47		let phc_str = stored.get("phc").ok_or_else(|| {
48			Error::from(AuthError::InvalidHash {
49				reason: "missing 'phc' field".to_string(),
50			})
51		})?;
52
53		let parsed_hash = PasswordHash::new(phc_str).map_err(|e| {
54			Error::from(AuthError::InvalidHash {
55				reason: e.to_string(),
56			})
57		})?;
58
59		let argon2 = argon2_instance();
60
61		match argon2.verify_password(credential.as_bytes(), &parsed_hash) {
62			Ok(()) => Ok(true),
63			Err(PasswordHashError::PasswordInvalid) => Ok(false),
64			Err(e) => Err(Error::from(AuthError::VerificationFailed {
65				reason: e.to_string(),
66			})),
67		}
68	}
69}
70
71#[cfg(test)]
72mod tests {
73	use super::*;
74
75	#[test]
76	fn test_password_create_and_validate() {
77		let provider = PasswordProvider;
78		let config = HashMap::from([("password".to_string(), "secret123".to_string())]);
79
80		let stored = provider.create(&config).unwrap();
81		assert!(stored.contains_key("phc"));
82		assert!(stored.get("phc").unwrap().starts_with("$argon2id$"));
83		assert_eq!(stored.get("algorithm_version").unwrap(), "1");
84
85		assert!(provider.validate(&stored, "secret123").unwrap());
86		assert!(!provider.validate(&stored, "wrong_password").unwrap());
87	}
88
89	#[test]
90	fn test_password_requires_password_field() {
91		let provider = PasswordProvider;
92		let config = HashMap::new();
93		assert!(provider.create(&config).is_err());
94	}
95
96	#[test]
97	fn test_validate_corrupted_hash() {
98		let provider = PasswordProvider;
99		let stored = HashMap::from([
100			("phc".into(), "not-a-valid-phc-string".to_string()),
101			("algorithm_version".into(), "1".into()),
102		]);
103		assert!(provider.validate(&stored, "anything").is_err());
104	}
105
106	#[test]
107	fn test_validate_missing_phc() {
108		let provider = PasswordProvider;
109		let stored = HashMap::new();
110		assert!(provider.validate(&stored, "anything").is_err());
111	}
112}