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(&self, password: &str) -> Result<bool, anyhow::Error> {
100 let inner = self.get_inner()?;
101 let score = zxcvbn(password, &[]);
102 Ok(u8::from(score.score()) >= inner.minimum_complexity)
103 }
104
105 #[tracing::instrument(name = "passwords.hash", skip_all)]
113 pub async fn hash<R: CryptoRng + RngCore + Send>(
114 &self,
115 rng: R,
116 password: Zeroizing<Vec<u8>>,
117 ) -> Result<(SchemeVersion, String), anyhow::Error> {
118 let inner = self.get_inner()?;
119
120 let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
123 let span = tracing::Span::current();
124
125 let version = inner.current_version;
128
129 let hashed = tokio::task::spawn_blocking(move || {
130 span.in_scope(move || inner.current_hasher.hash_blocking(rng, &password))
131 })
132 .await??;
133
134 Ok((version, hashed))
135 }
136
137 #[tracing::instrument(name = "passwords.verify", skip_all, fields(%scheme))]
144 pub async fn verify(
145 &self,
146 scheme: SchemeVersion,
147 password: Zeroizing<Vec<u8>>,
148 hashed_password: String,
149 ) -> Result<(), anyhow::Error> {
150 let inner = self.get_inner()?;
151 let span = tracing::Span::current();
152
153 tokio::task::spawn_blocking(move || {
154 span.in_scope(move || {
155 let hasher = if scheme == inner.current_version {
156 &inner.current_hasher
157 } else {
158 inner
159 .other_hashers
160 .get(&scheme)
161 .context("Hashing scheme not found")?
162 };
163
164 hasher.verify_blocking(&hashed_password, &password)
165 })
166 })
167 .await??;
168
169 Ok(())
170 }
171
172 #[tracing::instrument(name = "passwords.verify_and_upgrade", skip_all, fields(%scheme))]
180 pub async fn verify_and_upgrade<R: CryptoRng + RngCore + Send>(
181 &self,
182 rng: R,
183 scheme: SchemeVersion,
184 password: Zeroizing<Vec<u8>>,
185 hashed_password: String,
186 ) -> Result<Option<(SchemeVersion, String)>, anyhow::Error> {
187 let inner = self.get_inner()?;
188
189 let new_hash_fut: OptionFuture<_> = (scheme != inner.current_version)
192 .then(|| self.hash(rng, password.clone()))
193 .into();
194
195 let verify_fut = self.verify(scheme, password, hashed_password);
196
197 let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut);
198 verify_res?;
199
200 let new_hash = new_hash_res.transpose()?;
201
202 Ok(new_hash)
203 }
204}
205
206pub struct Hasher {
208 algorithm: Algorithm,
209 pepper: Option<Vec<u8>>,
210}
211
212impl Hasher {
213 #[must_use]
215 pub const fn bcrypt(cost: Option<u32>, pepper: Option<Vec<u8>>) -> Self {
216 let algorithm = Algorithm::Bcrypt { cost };
217 Self { algorithm, pepper }
218 }
219
220 #[must_use]
222 pub const fn argon2id(pepper: Option<Vec<u8>>) -> Self {
223 let algorithm = Algorithm::Argon2id;
224 Self { algorithm, pepper }
225 }
226
227 #[must_use]
229 pub const fn pbkdf2(pepper: Option<Vec<u8>>) -> Self {
230 let algorithm = Algorithm::Pbkdf2;
231 Self { algorithm, pepper }
232 }
233
234 fn hash_blocking<R: CryptoRng + RngCore>(
235 &self,
236 rng: R,
237 password: &[u8],
238 ) -> Result<String, anyhow::Error> {
239 self.algorithm
240 .hash_blocking(rng, password, self.pepper.as_deref())
241 }
242
243 fn verify_blocking(&self, hashed_password: &str, password: &[u8]) -> Result<(), anyhow::Error> {
244 self.algorithm
245 .verify_blocking(hashed_password, password, self.pepper.as_deref())
246 }
247}
248
249#[derive(Debug, Clone, Copy)]
250enum Algorithm {
251 Bcrypt { cost: Option<u32> },
252 Argon2id,
253 Pbkdf2,
254}
255
256impl Algorithm {
257 fn hash_blocking<R: CryptoRng + RngCore>(
258 self,
259 mut rng: R,
260 password: &[u8],
261 pepper: Option<&[u8]>,
262 ) -> Result<String, anyhow::Error> {
263 match self {
264 Self::Bcrypt { cost } => {
265 let mut password = Zeroizing::new(password.to_vec());
266 if let Some(pepper) = pepper {
267 password.extend_from_slice(pepper);
268 }
269
270 let salt = Standard.sample(&mut rng);
271
272 let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
273 Ok(hashed.format_for_version(bcrypt::Version::TwoB))
274 }
275
276 Self::Argon2id => {
277 let algorithm = argon2::Algorithm::default();
278 let version = argon2::Version::default();
279 let params = argon2::Params::default();
280
281 let phf = if let Some(secret) = pepper {
282 Argon2::new_with_secret(secret, algorithm, version, params)?
283 } else {
284 Argon2::new(algorithm, version, params)
285 };
286
287 let salt = SaltString::generate(rng);
288 let hashed = phf.hash_password(password.as_ref(), &salt)?;
289 Ok(hashed.to_string())
290 }
291
292 Self::Pbkdf2 => {
293 let mut password = Zeroizing::new(password.to_vec());
294 if let Some(pepper) = pepper {
295 password.extend_from_slice(pepper);
296 }
297
298 let salt = SaltString::generate(rng);
299 let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
300 Ok(hashed.to_string())
301 }
302 }
303 }
304
305 fn verify_blocking(
306 self,
307 hashed_password: &str,
308 password: &[u8],
309 pepper: Option<&[u8]>,
310 ) -> Result<(), anyhow::Error> {
311 match self {
312 Algorithm::Bcrypt { .. } => {
313 let mut password = Zeroizing::new(password.to_vec());
314 if let Some(pepper) = pepper {
315 password.extend_from_slice(pepper);
316 }
317
318 let result = bcrypt::verify(password, hashed_password)?;
319 anyhow::ensure!(result, "wrong password");
320 }
321
322 Algorithm::Argon2id => {
323 let algorithm = argon2::Algorithm::default();
324 let version = argon2::Version::default();
325 let params = argon2::Params::default();
326
327 let phf = if let Some(secret) = pepper {
328 Argon2::new_with_secret(secret, algorithm, version, params)?
329 } else {
330 Argon2::new(algorithm, version, params)
331 };
332
333 let hashed_password = PasswordHash::new(hashed_password)?;
334
335 phf.verify_password(password.as_ref(), &hashed_password)?;
336 }
337
338 Algorithm::Pbkdf2 => {
339 let mut password = Zeroizing::new(password.to_vec());
340 if let Some(pepper) = pepper {
341 password.extend_from_slice(pepper);
342 }
343
344 let hashed_password = PasswordHash::new(hashed_password)?;
345
346 Pbkdf2.verify_password(password.as_ref(), &hashed_password)?;
347 }
348 }
349
350 Ok(())
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use rand::SeedableRng;
357
358 use super::*;
359
360 #[test]
361 fn hashing_bcrypt() {
362 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
363 let password = b"hunter2";
364 let password2 = b"wrong-password";
365 let pepper = b"a-secret-pepper";
366 let pepper2 = b"the-wrong-pepper";
367
368 let alg = Algorithm::Bcrypt { cost: Some(10) };
369 let hash = alg
371 .hash_blocking(&mut rng, password, Some(pepper))
372 .expect("Couldn't hash password");
373 insta::assert_snapshot!(hash);
374
375 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
376 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
377 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
378 assert!(alg.verify_blocking(&hash, password, None).is_err());
379
380 let hash = alg
382 .hash_blocking(&mut rng, password, None)
383 .expect("Couldn't hash password");
384 insta::assert_snapshot!(hash);
385
386 assert!(alg.verify_blocking(&hash, password, None).is_ok());
387 assert!(alg.verify_blocking(&hash, password2, None).is_err());
388 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
389 }
390
391 #[test]
392 fn hashing_argon2id() {
393 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
394 let password = b"hunter2";
395 let password2 = b"wrong-password";
396 let pepper = b"a-secret-pepper";
397 let pepper2 = b"the-wrong-pepper";
398
399 let alg = Algorithm::Argon2id;
400 let hash = alg
402 .hash_blocking(&mut rng, password, Some(pepper))
403 .expect("Couldn't hash password");
404 insta::assert_snapshot!(hash);
405
406 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
407 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
408 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
409 assert!(alg.verify_blocking(&hash, password, None).is_err());
410
411 let hash = alg
413 .hash_blocking(&mut rng, password, None)
414 .expect("Couldn't hash password");
415 insta::assert_snapshot!(hash);
416
417 assert!(alg.verify_blocking(&hash, password, None).is_ok());
418 assert!(alg.verify_blocking(&hash, password2, None).is_err());
419 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
420 }
421
422 #[test]
423 #[ignore = "this is particularly slow (20s+ seconds)"]
424 fn hashing_pbkdf2() {
425 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
426 let password = b"hunter2";
427 let password2 = b"wrong-password";
428 let pepper = b"a-secret-pepper";
429 let pepper2 = b"the-wrong-pepper";
430
431 let alg = Algorithm::Pbkdf2;
432 let hash = alg
434 .hash_blocking(&mut rng, password, Some(pepper))
435 .expect("Couldn't hash password");
436 insta::assert_snapshot!(hash);
437
438 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
439 assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
440 assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
441 assert!(alg.verify_blocking(&hash, password, None).is_err());
442
443 let hash = alg
445 .hash_blocking(&mut rng, password, None)
446 .expect("Couldn't hash password");
447 insta::assert_snapshot!(hash);
448
449 assert!(alg.verify_blocking(&hash, password, None).is_ok());
450 assert!(alg.verify_blocking(&hash, password2, None).is_err());
451 assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
452 }
453
454 #[allow(clippy::too_many_lines)]
455 #[tokio::test]
456 async fn hash_verify_and_upgrade() {
457 let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
461 let password = Zeroizing::new(b"hunter2".to_vec());
462 let wrong_password = Zeroizing::new(b"wrong-password".to_vec());
463
464 let manager = PasswordManager::new(
465 0,
466 [
467 (
469 1,
470 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
471 ),
472 ],
473 )
474 .unwrap();
475
476 let (version, hash) = manager
477 .hash(&mut rng, password.clone())
478 .await
479 .expect("Failed to hash");
480
481 assert_eq!(version, 1);
482 insta::assert_snapshot!(hash);
483
484 manager
486 .verify(version, password.clone(), hash.clone())
487 .await
488 .expect("Failed to verify");
489
490 manager
492 .verify(version, wrong_password.clone(), hash.clone())
493 .await
494 .expect_err("Verification should have failed");
495
496 manager
498 .verify(2, password.clone(), hash.clone())
499 .await
500 .expect_err("Verification should have failed");
501
502 let res = manager
504 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
505 .await
506 .expect("Failed to verify");
507
508 assert!(res.is_none());
509
510 manager
512 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
513 .await
514 .expect_err("Verification should have failed");
515
516 let manager = PasswordManager::new(
517 0,
518 [
519 (2, Hasher::argon2id(None)),
520 (
521 1,
522 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
523 ),
524 ],
525 )
526 .unwrap();
527
528 manager
530 .verify(version, password.clone(), hash.clone())
531 .await
532 .expect("Failed to verify");
533
534 manager
536 .verify(version, wrong_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_some());
547 let (version, hash) = res.unwrap();
548
549 assert_eq!(version, 2);
550 insta::assert_snapshot!(hash);
551
552 let res = manager
554 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
555 .await
556 .expect("Failed to verify");
557
558 assert!(res.is_none());
559
560 manager
562 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
563 .await
564 .expect_err("Verification should have failed");
565
566 manager
568 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
569 .await
570 .expect_err("Verification should have failed");
571
572 let manager = PasswordManager::new(
573 0,
574 [
575 (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
576 (2, Hasher::argon2id(None)),
577 (
578 1,
579 Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
580 ),
581 ],
582 )
583 .unwrap();
584
585 manager
587 .verify(version, password.clone(), hash.clone())
588 .await
589 .expect("Failed to verify");
590
591 manager
593 .verify(version, wrong_password.clone(), hash.clone())
594 .await
595 .expect_err("Verification should have failed");
596
597 let res = manager
599 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
600 .await
601 .expect("Failed to verify");
602
603 assert!(res.is_some());
604 let (version, hash) = res.unwrap();
605
606 assert_eq!(version, 3);
607 insta::assert_snapshot!(hash);
608
609 let res = manager
611 .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
612 .await
613 .expect("Failed to verify");
614
615 assert!(res.is_none());
616
617 manager
619 .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
620 .await
621 .expect_err("Verification should have failed");
622 }
623}