1use argon2::password_hash::phc::PasswordHash;
2use argon2::password_hash::{PasswordHasher, PasswordVerifier};
3use argon2::{Algorithm, Argon2, Params, Version};
4use subtle::ConstantTimeEq;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PwdVerification {
9 Valid,
10 ValidLegacy,
12 Invalid,
13}
14
15fn argon2() -> Argon2<'static> {
16 Argon2::new(Algorithm::Argon2id, Version::V0x13, params())
17}
18
19#[cfg(not(all(feature = "test-support", debug_assertions)))]
21fn params() -> Params {
22 Params::default()
23}
24
25#[cfg(all(feature = "test-support", debug_assertions))]
30fn params() -> Params {
31 Params::new(
32 Params::MIN_M_COST,
33 Params::MIN_T_COST,
34 Params::MIN_P_COST,
35 None,
36 )
37 .expect("minimal argon2 parameters are valid")
38}
39
40pub async fn hash_pwd(pwd: &str) -> String {
46 let pwd = pwd.to_owned();
47 run_blocking(move || hash_pwd_sync(&pwd)).await
48}
49
50pub async fn verify_pwd(pwd: &str, stored_hash: &str, legacy_salt: &str) -> PwdVerification {
54 let (pwd, stored_hash, legacy_salt) = (
55 pwd.to_owned(),
56 stored_hash.to_owned(),
57 legacy_salt.to_owned(),
58 );
59 run_blocking(move || verify_pwd_sync(&pwd, &stored_hash, &legacy_salt)).await
60}
61
62async fn run_blocking<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
63 tokio::task::spawn_blocking(f)
64 .await
65 .expect("password hashing task panicked")
66}
67
68fn hash_pwd_sync(pwd: &str) -> String {
69 argon2()
71 .hash_password(pwd.as_bytes())
72 .expect("argon2 hashing with valid parameters cannot fail")
73 .to_string()
74}
75
76fn verify_pwd_sync(pwd: &str, stored_hash: &str, legacy_salt: &str) -> PwdVerification {
77 if stored_hash.starts_with('$') {
78 return match PasswordHash::new(stored_hash) {
79 Ok(parsed) if argon2().verify_password(pwd.as_bytes(), &parsed).is_ok() => {
80 PwdVerification::Valid
81 }
82 _ => PwdVerification::Invalid,
83 };
84 }
85
86 let legacy = legacy_hash(pwd, legacy_salt);
87 if bool::from(legacy.as_bytes().ct_eq(stored_hash.as_bytes())) {
88 PwdVerification::ValidLegacy
89 } else {
90 PwdVerification::Invalid
91 }
92}
93
94pub(crate) fn legacy_hash(pwd: &str, salt: &str) -> String {
95 sha256::digest(format!("{pwd}{salt}"))
96}
97
98pub fn hash_token(token: &str) -> String {
99 sha256::digest(token)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn hash_pwd_produces_argon2id_phc_string() {
108 let hash = hash_pwd_sync("admin");
109
110 assert!(
111 hash.starts_with("$argon2id$v=19$"),
112 "unexpected hash {hash}"
113 );
114 }
115
116 #[test]
117 fn hash_pwd_uses_random_salt() {
118 assert_ne!(hash_pwd_sync("admin"), hash_pwd_sync("admin"));
119 }
120
121 #[test]
122 fn verify_pwd_accepts_argon2_hash() {
123 let hash = hash_pwd_sync("admin");
124
125 assert_eq!(verify_pwd_sync("admin", &hash, ""), PwdVerification::Valid);
126 assert_eq!(
127 verify_pwd_sync("wrong", &hash, ""),
128 PwdVerification::Invalid
129 );
130 }
131
132 #[test]
133 fn verify_pwd_accepts_stored_argon2_hash() {
134 let hash = "$argon2id$v=19$m=19456,t=2,p=1$a2VsbG5yc2FsdHZhbHVlMTY$grIizR+2/xpDGDbimzgwYxM2uK8eVjV28lGYRXqU7cc";
137
138 assert_eq!(verify_pwd_sync("admin", hash, ""), PwdVerification::Valid);
139 assert_eq!(verify_pwd_sync("wrong", hash, ""), PwdVerification::Invalid);
140 }
141
142 #[test]
143 fn verify_pwd_accepts_legacy_hash() {
144 let salt = "C6udtgbngX";
145 let legacy = "5dcec54caf0f55652766f71c32a0eac6538e7faeeab9301f956a58b7dbad02fb";
146
147 assert_eq!(
148 verify_pwd_sync("admin", legacy, salt),
149 PwdVerification::ValidLegacy
150 );
151 assert_eq!(
152 verify_pwd_sync("wrong", legacy, salt),
153 PwdVerification::Invalid
154 );
155 assert_eq!(
156 verify_pwd_sync("admin", legacy, "other_salt"),
157 PwdVerification::Invalid
158 );
159 }
160
161 #[test]
162 fn verify_pwd_rejects_malformed_phc_string() {
163 assert_eq!(
164 verify_pwd_sync("admin", "$argon2id$garbage", ""),
165 PwdVerification::Invalid
166 );
167 }
168
169 #[test]
170 fn verify_pwd_rejects_empty_hash() {
171 assert_eq!(verify_pwd_sync("", "", ""), PwdVerification::Invalid);
172 }
173
174 #[test]
175 fn legacy_hash_computes_sha256_of_pwd_and_salt() {
176 assert_eq!(
177 legacy_hash("admin", "C6udtgbngX"),
178 "5dcec54caf0f55652766f71c32a0eac6538e7faeeab9301f956a58b7dbad02fb"
179 );
180 }
181}