radixdb_executor/
credentials.rs1use argon2::{
4 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
5 Argon2, Params,
6};
7use radixdb_core::{Error, Result};
8
9pub const MAX_PASSWORD_BYTES: usize = 1024;
10const MIN_MEMORY_COST_KIB: u32 = Params::DEFAULT_M_COST;
11const MAX_MEMORY_COST_KIB: u32 = 256 * 1024;
12const MIN_TIME_COST: u32 = Params::DEFAULT_T_COST;
13const MAX_TIME_COST: u32 = 10;
14const MAX_PARALLELISM: u32 = 16;
15
16pub fn hash_password_verifier(password: &str) -> Result<String> {
18 validate_password_length(password)?;
19 let salt_seed = uuid::Uuid::now_v7();
20 let salt = SaltString::encode_b64(salt_seed.as_bytes())
21 .map_err(|_| Error::internal("failed to encode credential salt"))?;
22 Argon2::default()
23 .hash_password(password.as_bytes(), &salt)
24 .map(|hash| hash.to_string())
25 .map_err(|_| Error::internal("failed to derive credential verifier"))
26}
27
28pub fn validate_password_verifier(encoded: &str) -> Result<()> {
30 let parsed = PasswordHash::new(encoded)
31 .map_err(|_| Error::invalid_argument("password verifier must be a valid PHC string"))?;
32 if parsed.algorithm.as_str() != "argon2id" {
33 return Err(Error::invalid_argument(
34 "password verifier must use the argon2id algorithm",
35 ));
36 }
37 if parsed.version != Some(19) || parsed.salt.is_none() || parsed.hash.is_none() {
38 return Err(Error::invalid_argument(
39 "password verifier must contain Argon2 version 19, salt and hash",
40 ));
41 }
42 let params = Params::try_from(&parsed)
43 .map_err(|_| Error::invalid_argument("password verifier parameters are invalid"))?;
44 let output_len = params.output_len().unwrap_or(Params::DEFAULT_OUTPUT_LEN);
45 if !(MIN_MEMORY_COST_KIB..=MAX_MEMORY_COST_KIB).contains(¶ms.m_cost())
46 || !(MIN_TIME_COST..=MAX_TIME_COST).contains(¶ms.t_cost())
47 || !(Params::MIN_P_COST..=MAX_PARALLELISM).contains(¶ms.p_cost())
48 || !(Params::DEFAULT_OUTPUT_LEN..=64).contains(&output_len)
49 {
50 return Err(Error::invalid_argument(
51 "password verifier parameters are outside the supported security bounds",
52 ));
53 }
54 Ok(())
55}
56
57pub fn verify_password_verifier(encoded: &str, password: &str) -> bool {
59 if password.is_empty() || password.len() > MAX_PASSWORD_BYTES {
60 return false;
61 }
62 if validate_password_verifier(encoded).is_err() {
63 return false;
64 }
65 let parsed = PasswordHash::new(encoded).expect("validated password verifier must parse");
66 Argon2::default()
67 .verify_password(password.as_bytes(), &parsed)
68 .is_ok()
69}
70
71fn validate_password_length(password: &str) -> Result<()> {
72 if password.is_empty() || password.len() > MAX_PASSWORD_BYTES {
73 return Err(Error::invalid_argument(
74 "password length must be in 1..=1024 bytes",
75 ));
76 }
77 Ok(())
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn generated_verifier_is_argon2id_and_matches_only_its_password() {
86 let encoded = hash_password_verifier("correct horse battery staple").unwrap();
87 assert!(encoded.starts_with("$argon2id$"));
88 assert_eq!(validate_password_verifier(&encoded), Ok(()));
89 assert!(verify_password_verifier(
90 &encoded,
91 "correct horse battery staple"
92 ));
93 assert!(!verify_password_verifier(&encoded, "wrong"));
94 }
95
96 #[test]
97 fn malformed_and_non_argon2id_verifiers_are_rejected() {
98 assert!(validate_password_verifier("not-a-phc-string").is_err());
99 assert!(validate_password_verifier("$argon2i$v=19$m=4096,t=3,p=1$c2FsdA$YWJj").is_err());
100 assert!(validate_password_verifier("$argon2id$v=19$m=19456,t=2,p=1$c2FsdA").is_err());
101 assert!(validate_password_verifier(
102 "$argon2id$v=19$m=4294967295,t=2,p=1$c2FsdA$YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo"
103 )
104 .is_err());
105 assert!(!verify_password_verifier("not-a-phc-string", "secret"));
106 }
107
108 #[test]
109 fn password_length_is_bounded() {
110 assert!(hash_password_verifier("").is_err());
111 assert!(hash_password_verifier(&"x".repeat(MAX_PASSWORD_BYTES + 1)).is_err());
112 }
113}