1use 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
17fn 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}