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<String>,
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<String>,
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<String>,
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 unicode_normalization: bool,
213 pepper: Option<Vec<u8>>,
214}
215
216impl Hasher {
217 #[must_use]
219 pub const fn bcrypt(
220 cost: Option<u32>,
221 pepper: Option<Vec<u8>>,
222 unicode_normalization: bool,
223 ) -> Self {
224 let algorithm = Algorithm::Bcrypt { cost };
225 Self {
226 algorithm,
227 unicode_normalization,
228 pepper,
229 }
230 }
231
232 #[must_use]
234 pub const fn argon2id(pepper: Option<Vec<u8>>, unicode_normalization: bool) -> Self {
235 let algorithm = Algorithm::Argon2id;
236 Self {
237 algorithm,
238 unicode_normalization,
239 pepper,
240 }
241 }
242
243 #[must_use]
245 pub const fn pbkdf2(pepper: Option<Vec<u8>>, unicode_normalization: bool) -> Self {
246 let algorithm = Algorithm::Pbkdf2;
247 Self {
248 algorithm,
249 unicode_normalization,
250 pepper,
251 }
252 }
253
254 fn normalize_password(&self, password: Zeroizing<String>) -> Zeroizing<String> {
255 if self.unicode_normalization {
256 let normalizer = icu_normalizer::ComposingNormalizer::new_nfkc();
258 Zeroizing::new(normalizer.normalize(&password))
259 } else {
260 password
261 }
262 }
263
264 fn hash_blocking<R: CryptoRng + RngCore>(
265 &self,
266 rng: R,
267 password: Zeroizing<String>,
268 ) -> Result<String, anyhow::Error> {
269 let password = self.normalize_password(password);
270
271 self.algorithm
272 .hash_blocking(rng, password.as_bytes(), self.pepper.as_deref())
273 }
274
275 fn verify_blocking(
276 &self,
277 hashed_password: &str,
278 password: Zeroizing<String>,
279 ) -> Result<(), anyhow::Error> {
280 let password = self.normalize_password(password);
281
282 self.algorithm
283 .verify_blocking(hashed_password, password.as_bytes(), self.pepper.as_deref())
284 }
285}
286
287#[derive(Debug, Clone, Copy)]
288enum Algorithm {
289 Bcrypt { cost: Option<u32> },
290 Argon2id,
291 Pbkdf2,
292}
293
294impl Algorithm {
295 fn hash_blocking<R: CryptoRng + RngCore>(
296 self,
297 mut rng: R,
298 password: &[u8],
299 pepper: Option<&[u8]>,
300 ) -> Result<String, anyhow::Error> {
301 match self {
302 Self::Bcrypt { cost } => {
303 let mut password = Zeroizing::new(password.to_vec());
304 if let Some(pepper) = pepper {
305 password.extend_from_slice(pepper);
306 }
307
308 let salt = Standard.sample(&mut rng);
309
310 let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
311 Ok(hashed.format_for_version(bcrypt::Version::TwoB))
312 }
313
314 Self::Argon2id => {
315 let algorithm = argon2::Algorithm::default();
316 let version = argon2::Version::default();
317 let params = argon2::Params::default();
318
319 let phf = if let Some(secret) = pepper {
320 Argon2::new_with_secret(secret, algorithm, version, params)?
321 } else {
322 Argon2::new(algorithm, version, params)
323 };
324
325 let salt = SaltString::generate(rng);
326 let hashed = phf.hash_password(password.as_ref(), &salt)?;
327 Ok(hashed.to_string())
328 }
329
330 Self::Pbkdf2 => {
331 let mut password = Zeroizing::new(password.to_vec());
332 if let Some(pepper) = pepper {
333 password.extend_from_slice(pepper);
334 }
335
336 let salt = SaltString::generate(rng);
337 let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
338 Ok(hashed.to_string())
339 }
340 }
341 }
342
343 fn verify_blocking(
344 self,
345 hashed_password: &str,
346 password: &[u8],
347 pepper: Option<&[u8]>,
348 ) -> Result<(), anyhow::Error> {
349 match self {
350 Algorithm::Bcrypt { .. } => {
351 let mut password = Zeroizing::new(password.to_vec());
352 if let Some(pepper) = pepper {
353 password.extend_from_slice(pepper);
354 }
355
356 let result = bcrypt::verify(password, hashed_password)?;
357 anyhow::ensure!(result, "wrong password");
358 }
359
360 Algorithm::Argon2id => {
361 let algorithm = argon2::Algorithm::default();
362 let version = argon2::Version::default();
363 let params = argon2::Params::default();
364
365 let phf = if let Some(secret) = pepper {
366 Argon2::new_with_secret(secret, algorithm, version, params)?
367 } else {
368 Argon2::new(algorithm, version, params)
369 };
370
371 let hashed_password = PasswordHash::new(hashed_password)?;
372
373 phf.verify_password(password.as_ref(), &hashed_password)?;
374 }
375
376 Algorithm::Pbkdf2 => {
377 let mut password = Zeroizing::new(password.to_vec());
378 if let Some(pepper) = pepper {
379 password.extend_from_slice(pepper);
380 }
381
382 let hashed_password = PasswordHash::new(hashed_password)?;
383
384 Pbkdf2.verify_password(password.as_ref(), &hashed_password)?;
385 }
386 }
387
388 Ok(())
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use rand::SeedableRng;
395
396 use super::*;
397
398 #[test]
399 fn hashing_bcrypt() {
400 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
401 let password = b"hunter2";
402 let password2 = b"wrong-password";
403 let pepper = b"a-secret-pepper";
404 let pepper2 = b"the-wrong-pepper";
405
406 let alg = Algorithm::Bcrypt { cost: Some(10) };
407 let hash = alg
409 .hash_blocking(&mut rng, password, Some(pepper))
410 .expect("Couldn't hash password");
411 insta::assert_snapshot!(hash);
412
413 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
414 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
415 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
416 assert!(alg.verify_blocking(&hash, password, None).is_err());
417
418 let hash = alg
420 .hash_blocking(&mut rng, password, None)
421 .expect("Couldn't hash password");
422 insta::assert_snapshot!(hash);
423
424 assert!(alg.verify_blocking(&hash, password, None).is_ok());
425 assert!(alg.verify_blocking(&hash, password2, None).is_err());
426 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
427 }
428
429 #[test]
430 fn hashing_argon2id() {
431 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
432 let password = b"hunter2";
433 let password2 = b"wrong-password";
434 let pepper = b"a-secret-pepper";
435 let pepper2 = b"the-wrong-pepper";
436
437 let alg = Algorithm::Argon2id;
438 let hash = alg
440 .hash_blocking(&mut rng, password, Some(pepper))
441 .expect("Couldn't hash password");
442 insta::assert_snapshot!(hash);
443
444 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
445 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
446 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
447 assert!(alg.verify_blocking(&hash, password, None).is_err());
448
449 let hash = alg
451 .hash_blocking(&mut rng, password, None)
452 .expect("Couldn't hash password");
453 insta::assert_snapshot!(hash);
454
455 assert!(alg.verify_blocking(&hash, password, None).is_ok());
456 assert!(alg.verify_blocking(&hash, password2, None).is_err());
457 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
458 }
459
460 #[test]
461 #[ignore = "this is particularly slow (20s+ seconds)"]
462 fn hashing_pbkdf2() {
463 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
464 let password = b"hunter2";
465 let password2 = b"wrong-password";
466 let pepper = b"a-secret-pepper";
467 let pepper2 = b"the-wrong-pepper";
468
469 let alg = Algorithm::Pbkdf2;
470 let hash = alg
472 .hash_blocking(&mut rng, password, Some(pepper))
473 .expect("Couldn't hash password");
474 insta::assert_snapshot!(hash);
475
476 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
477 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
478 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
479 assert!(alg.verify_blocking(&hash, password, None).is_err());
480
481 let hash = alg
483 .hash_blocking(&mut rng, password, None)
484 .expect("Couldn't hash password");
485 insta::assert_snapshot!(hash);
486
487 assert!(alg.verify_blocking(&hash, password, None).is_ok());
488 assert!(alg.verify_blocking(&hash, password2, None).is_err());
489 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
490 }
491
492 #[allow(clippy::too_many_lines)]
493 #[tokio::test]
494 async fn hash_verify_and_upgrade() {
495 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
499 let password = Zeroizing::new("hunter2".to_owned());
500 let wrong_password = Zeroizing::new("wrong-password".to_owned());
501
502 let manager = PasswordManager::new(
503 0,
504 [
505 (
507 1,
508 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
509 ),
510 ],
511 )
512 .unwrap();
513
514 let (version, hash) = manager
515 .hash(&mut rng, password.clone())
516 .await
517 .expect("Failed to hash");
518
519 assert_eq!(version, 1);
520 insta::assert_snapshot!(hash);
521
522 manager
524 .verify(version, password.clone(), hash.clone())
525 .await
526 .expect("Failed to verify");
527
528 manager
530 .verify(version, wrong_password.clone(), hash.clone())
531 .await
532 .expect_err("Verification should have failed");
533
534 manager
536 .verify(2, password.clone(), hash.clone())
537 .await
538 .expect_err("Verification should have failed");
539
540 let res = manager
542 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
543 .await
544 .expect("Failed to verify");
545
546 assert!(res.is_none());
547
548 manager
550 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
551 .await
552 .expect_err("Verification should have failed");
553
554 let manager = PasswordManager::new(
555 0,
556 [
557 (2, Hasher::argon2id(None, false)),
558 (
559 1,
560 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
561 ),
562 ],
563 )
564 .unwrap();
565
566 manager
568 .verify(version, password.clone(), hash.clone())
569 .await
570 .expect("Failed to verify");
571
572 manager
574 .verify(version, wrong_password.clone(), hash.clone())
575 .await
576 .expect_err("Verification should have failed");
577
578 let res = manager
580 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
581 .await
582 .expect("Failed to verify");
583
584 assert!(res.is_some());
585 let (version, hash) = res.unwrap();
586
587 assert_eq!(version, 2);
588 insta::assert_snapshot!(hash);
589
590 let res = manager
592 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
593 .await
594 .expect("Failed to verify");
595
596 assert!(res.is_none());
597
598 manager
600 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
601 .await
602 .expect_err("Verification should have failed");
603
604 manager
606 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
607 .await
608 .expect_err("Verification should have failed");
609
610 let manager = PasswordManager::new(
611 0,
612 [
613 (
614 3,
615 Hasher::argon2id(Some(b"a-secret-pepper".to_vec()), false),
616 ),
617 (2, Hasher::argon2id(None, false)),
618 (
619 1,
620 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec()), false),
621 ),
622 ],
623 )
624 .unwrap();
625
626 manager
628 .verify(version, password.clone(), hash.clone())
629 .await
630 .expect("Failed to verify");
631
632 manager
634 .verify(version, wrong_password.clone(), hash.clone())
635 .await
636 .expect_err("Verification should have failed");
637
638 let res = manager
640 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
641 .await
642 .expect("Failed to verify");
643
644 assert!(res.is_some());
645 let (version, hash) = res.unwrap();
646
647 assert_eq!(version, 3);
648 insta::assert_snapshot!(hash);
649
650 let res = manager
652 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
653 .await
654 .expect("Failed to verify");
655
656 assert!(res.is_none());
657
658 manager
660 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
661 .await
662 .expect_err("Verification should have failed");
663 }
664}