1use std::{collections::HashMap, sync::Arc};
8
9use anyhow::Context;
10use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
11use futures_util::future::OptionFuture;
12use pbkdf2::Pbkdf2;
13use rand::{CryptoRng, RngCore, SeedableRng, distributions::Standard, prelude::Distribution};
14use thiserror::Error;
15use zeroize::Zeroizing;
16use zxcvbn::zxcvbn;
17
18pub type SchemeVersion = u16;
19
20#[derive(Debug, Error)]
21#[error("Password manager is disabled")]
22pub struct PasswordManagerDisabledError;
23
24#[derive(Clone)]
25pub struct PasswordManager {
26 inner: Option<Arc<InnerPasswordManager>>,
27}
28
29struct InnerPasswordManager {
30 minimum_complexity: u8,
33 current_hasher: Hasher,
34 current_version: SchemeVersion,
35
36 other_hashers: HashMap<SchemeVersion, Hasher>,
38}
39
40impl PasswordManager {
41 pub fn new<I: IntoIterator<Item = (SchemeVersion, Hasher)>>(
49 minimum_complexity: u8,
50 iter: I,
51 ) -> Result<Self, anyhow::Error> {
52 let mut iter = iter.into_iter();
53
54 let (current_version, current_hasher) = iter
56 .next()
57 .context("Iterator must have at least one item")?;
58
59 let other_hashers = iter.collect();
61
62 Ok(Self {
63 inner: Some(Arc::new(InnerPasswordManager {
64 minimum_complexity,
65 current_hasher,
66 current_version,
67 other_hashers,
68 })),
69 })
70 }
71
72 #[must_use]
74 pub const fn disabled() -> Self {
75 Self { inner: None }
76 }
77
78 #[must_use]
80 pub const fn is_enabled(&self) -> bool {
81 self.inner.is_some()
82 }
83
84 fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
90 self.inner.clone().ok_or(PasswordManagerDisabledError)
91 }
92
93 pub fn is_password_complex_enough(
100 &self,
101 password: &str,
102 ) -> Result<bool, PasswordManagerDisabledError> {
103 let inner = self.get_inner()?;
104 let score = zxcvbn(password, &[]);
105 Ok(u8::from(score.score()) >= inner.minimum_complexity)
106 }
107
108 #[tracing::instrument(name = "passwords.hash", skip_all)]
116 pub async fn hash<R: CryptoRng + RngCore + Send>(
117 &self,
118 rng: R,
119 password: Zeroizing<Vec<u8>>,
120 ) -> Result<(SchemeVersion, String), anyhow::Error> {
121 let inner = self.get_inner()?;
122
123 let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
126 let span = tracing::Span::current();
127
128 let version = inner.current_version;
131
132 let hashed = tokio::task::spawn_blocking(move || {
133 span.in_scope(move || inner.current_hasher.hash_blocking(rng, &password))
134 })
135 .await??;
136
137 Ok((version, hashed))
138 }
139
140 #[tracing::instrument(name = "passwords.verify", skip_all, fields(%scheme))]
147 pub async fn verify(
148 &self,
149 scheme: SchemeVersion,
150 password: Zeroizing<Vec<u8>>,
151 hashed_password: String,
152 ) -> Result<(), anyhow::Error> {
153 let inner = self.get_inner()?;
154 let span = tracing::Span::current();
155
156 tokio::task::spawn_blocking(move || {
157 span.in_scope(move || {
158 let hasher = if scheme == inner.current_version {
159 &inner.current_hasher
160 } else {
161 inner
162 .other_hashers
163 .get(&scheme)
164 .context("Hashing scheme not found")?
165 };
166
167 hasher.verify_blocking(&hashed_password, &password)
168 })
169 })
170 .await??;
171
172 Ok(())
173 }
174
175 #[tracing::instrument(name = "passwords.verify_and_upgrade", skip_all, fields(%scheme))]
183 pub async fn verify_and_upgrade<R: CryptoRng + RngCore + Send>(
184 &self,
185 rng: R,
186 scheme: SchemeVersion,
187 password: Zeroizing<Vec<u8>>,
188 hashed_password: String,
189 ) -> Result<Option<(SchemeVersion, String)>, anyhow::Error> {
190 let inner = self.get_inner()?;
191
192 let new_hash_fut: OptionFuture<_> = (scheme != inner.current_version)
195 .then(|| self.hash(rng, password.clone()))
196 .into();
197
198 let verify_fut = self.verify(scheme, password, hashed_password);
199
200 let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut);
201 verify_res?;
202
203 let new_hash = new_hash_res.transpose()?;
204
205 Ok(new_hash)
206 }
207}
208
209pub struct Hasher {
211 algorithm: Algorithm,
212 pepper: Option<Vec<u8>>,
213}
214
215impl Hasher {
216 #[must_use]
218 pub const fn bcrypt(cost: Option<u32>, pepper: Option<Vec<u8>>) -> Self {
219 let algorithm = Algorithm::Bcrypt { cost };
220 Self { algorithm, pepper }
221 }
222
223 #[must_use]
225 pub const fn argon2id(pepper: Option<Vec<u8>>) -> Self {
226 let algorithm = Algorithm::Argon2id;
227 Self { algorithm, pepper }
228 }
229
230 #[must_use]
232 pub const fn pbkdf2(pepper: Option<Vec<u8>>) -> Self {
233 let algorithm = Algorithm::Pbkdf2;
234 Self { algorithm, pepper }
235 }
236
237 fn hash_blocking<R: CryptoRng + RngCore>(
238 &self,
239 rng: R,
240 password: &[u8],
241 ) -> Result<String, anyhow::Error> {
242 self.algorithm
243 .hash_blocking(rng, password, self.pepper.as_deref())
244 }
245
246 fn verify_blocking(&self, hashed_password: &str, password: &[u8]) -> Result<(), anyhow::Error> {
247 self.algorithm
248 .verify_blocking(hashed_password, password, self.pepper.as_deref())
249 }
250}
251
252#[derive(Debug, Clone, Copy)]
253enum Algorithm {
254 Bcrypt { cost: Option<u32> },
255 Argon2id,
256 Pbkdf2,
257}
258
259impl Algorithm {
260 fn hash_blocking<R: CryptoRng + RngCore>(
261 self,
262 mut rng: R,
263 password: &[u8],
264 pepper: Option<&[u8]>,
265 ) -> Result<String, anyhow::Error> {
266 match self {
267 Self::Bcrypt { cost } => {
268 let mut password = Zeroizing::new(password.to_vec());
269 if let Some(pepper) = pepper {
270 password.extend_from_slice(pepper);
271 }
272
273 let salt = Standard.sample(&mut rng);
274
275 let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
276 Ok(hashed.format_for_version(bcrypt::Version::TwoB))
277 }
278
279 Self::Argon2id => {
280 let algorithm = argon2::Algorithm::default();
281 let version = argon2::Version::default();
282 let params = argon2::Params::default();
283
284 let phf = if let Some(secret) = pepper {
285 Argon2::new_with_secret(secret, algorithm, version, params)?
286 } else {
287 Argon2::new(algorithm, version, params)
288 };
289
290 let salt = SaltString::generate(rng);
291 let hashed = phf.hash_password(password.as_ref(), &salt)?;
292 Ok(hashed.to_string())
293 }
294
295 Self::Pbkdf2 => {
296 let mut password = Zeroizing::new(password.to_vec());
297 if let Some(pepper) = pepper {
298 password.extend_from_slice(pepper);
299 }
300
301 let salt = SaltString::generate(rng);
302 let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
303 Ok(hashed.to_string())
304 }
305 }
306 }
307
308 fn verify_blocking(
309 self,
310 hashed_password: &str,
311 password: &[u8],
312 pepper: Option<&[u8]>,
313 ) -> Result<(), anyhow::Error> {
314 match self {
315 Algorithm::Bcrypt { .. } => {
316 let mut password = Zeroizing::new(password.to_vec());
317 if let Some(pepper) = pepper {
318 password.extend_from_slice(pepper);
319 }
320
321 let result = bcrypt::verify(password, hashed_password)?;
322 anyhow::ensure!(result, "wrong password");
323 }
324
325 Algorithm::Argon2id => {
326 let algorithm = argon2::Algorithm::default();
327 let version = argon2::Version::default();
328 let params = argon2::Params::default();
329
330 let phf = if let Some(secret) = pepper {
331 Argon2::new_with_secret(secret, algorithm, version, params)?
332 } else {
333 Argon2::new(algorithm, version, params)
334 };
335
336 let hashed_password = PasswordHash::new(hashed_password)?;
337
338 phf.verify_password(password.as_ref(), &hashed_password)?;
339 }
340
341 Algorithm::Pbkdf2 => {
342 let mut password = Zeroizing::new(password.to_vec());
343 if let Some(pepper) = pepper {
344 password.extend_from_slice(pepper);
345 }
346
347 let hashed_password = PasswordHash::new(hashed_password)?;
348
349 Pbkdf2.verify_password(password.as_ref(), &hashed_password)?;
350 }
351 }
352
353 Ok(())
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use rand::SeedableRng;
360
361 use super::*;
362
363 #[test]
364 fn hashing_bcrypt() {
365 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
366 let password = b"hunter2";
367 let password2 = b"wrong-password";
368 let pepper = b"a-secret-pepper";
369 let pepper2 = b"the-wrong-pepper";
370
371 let alg = Algorithm::Bcrypt { cost: Some(10) };
372 let hash = alg
374 .hash_blocking(&mut rng, password, Some(pepper))
375 .expect("Couldn't hash password");
376 insta::assert_snapshot!(hash);
377
378 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
379 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
380 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
381 assert!(alg.verify_blocking(&hash, password, None).is_err());
382
383 let hash = alg
385 .hash_blocking(&mut rng, password, None)
386 .expect("Couldn't hash password");
387 insta::assert_snapshot!(hash);
388
389 assert!(alg.verify_blocking(&hash, password, None).is_ok());
390 assert!(alg.verify_blocking(&hash, password2, None).is_err());
391 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
392 }
393
394 #[test]
395 fn hashing_argon2id() {
396 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
397 let password = b"hunter2";
398 let password2 = b"wrong-password";
399 let pepper = b"a-secret-pepper";
400 let pepper2 = b"the-wrong-pepper";
401
402 let alg = Algorithm::Argon2id;
403 let hash = alg
405 .hash_blocking(&mut rng, password, Some(pepper))
406 .expect("Couldn't hash password");
407 insta::assert_snapshot!(hash);
408
409 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
410 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
411 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
412 assert!(alg.verify_blocking(&hash, password, None).is_err());
413
414 let hash = alg
416 .hash_blocking(&mut rng, password, None)
417 .expect("Couldn't hash password");
418 insta::assert_snapshot!(hash);
419
420 assert!(alg.verify_blocking(&hash, password, None).is_ok());
421 assert!(alg.verify_blocking(&hash, password2, None).is_err());
422 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
423 }
424
425 #[test]
426 #[ignore = "this is particularly slow (20s+ seconds)"]
427 fn hashing_pbkdf2() {
428 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
429 let password = b"hunter2";
430 let password2 = b"wrong-password";
431 let pepper = b"a-secret-pepper";
432 let pepper2 = b"the-wrong-pepper";
433
434 let alg = Algorithm::Pbkdf2;
435 let hash = alg
437 .hash_blocking(&mut rng, password, Some(pepper))
438 .expect("Couldn't hash password");
439 insta::assert_snapshot!(hash);
440
441 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
442 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
443 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
444 assert!(alg.verify_blocking(&hash, password, None).is_err());
445
446 let hash = alg
448 .hash_blocking(&mut rng, password, None)
449 .expect("Couldn't hash password");
450 insta::assert_snapshot!(hash);
451
452 assert!(alg.verify_blocking(&hash, password, None).is_ok());
453 assert!(alg.verify_blocking(&hash, password2, None).is_err());
454 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
455 }
456
457 #[allow(clippy::too_many_lines)]
458 #[tokio::test]
459 async fn hash_verify_and_upgrade() {
460 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
464 let password = Zeroizing::new(b"hunter2".to_vec());
465 let wrong_password = Zeroizing::new(b"wrong-password".to_vec());
466
467 let manager = PasswordManager::new(
468 0,
469 [
470 (
472 1,
473 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
474 ),
475 ],
476 )
477 .unwrap();
478
479 let (version, hash) = manager
480 .hash(&mut rng, password.clone())
481 .await
482 .expect("Failed to hash");
483
484 assert_eq!(version, 1);
485 insta::assert_snapshot!(hash);
486
487 manager
489 .verify(version, password.clone(), hash.clone())
490 .await
491 .expect("Failed to verify");
492
493 manager
495 .verify(version, wrong_password.clone(), hash.clone())
496 .await
497 .expect_err("Verification should have failed");
498
499 manager
501 .verify(2, password.clone(), hash.clone())
502 .await
503 .expect_err("Verification should have failed");
504
505 let res = manager
507 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
508 .await
509 .expect("Failed to verify");
510
511 assert!(res.is_none());
512
513 manager
515 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
516 .await
517 .expect_err("Verification should have failed");
518
519 let manager = PasswordManager::new(
520 0,
521 [
522 (2, Hasher::argon2id(None)),
523 (
524 1,
525 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
526 ),
527 ],
528 )
529 .unwrap();
530
531 manager
533 .verify(version, password.clone(), hash.clone())
534 .await
535 .expect("Failed to verify");
536
537 manager
539 .verify(version, wrong_password.clone(), hash.clone())
540 .await
541 .expect_err("Verification should have failed");
542
543 let res = manager
545 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
546 .await
547 .expect("Failed to verify");
548
549 assert!(res.is_some());
550 let (version, hash) = res.unwrap();
551
552 assert_eq!(version, 2);
553 insta::assert_snapshot!(hash);
554
555 let res = manager
557 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
558 .await
559 .expect("Failed to verify");
560
561 assert!(res.is_none());
562
563 manager
565 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
566 .await
567 .expect_err("Verification should have failed");
568
569 manager
571 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
572 .await
573 .expect_err("Verification should have failed");
574
575 let manager = PasswordManager::new(
576 0,
577 [
578 (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
579 (2, Hasher::argon2id(None)),
580 (
581 1,
582 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
583 ),
584 ],
585 )
586 .unwrap();
587
588 manager
590 .verify(version, password.clone(), hash.clone())
591 .await
592 .expect("Failed to verify");
593
594 manager
596 .verify(version, wrong_password.clone(), hash.clone())
597 .await
598 .expect_err("Verification should have failed");
599
600 let res = manager
602 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
603 .await
604 .expect("Failed to verify");
605
606 assert!(res.is_some());
607 let (version, hash) = res.unwrap();
608
609 assert_eq!(version, 3);
610 insta::assert_snapshot!(hash);
611
612 let res = manager
614 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
615 .await
616 .expect("Failed to verify");
617
618 assert!(res.is_none());
619
620 manager
622 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
623 .await
624 .expect_err("Verification should have failed");
625 }
626}