klick_domain/authentication/
password.rs1use std::{fmt, str::FromStr};
2
3use pwhash::bcrypt;
4use thiserror::Error;
5
6pub struct Password(String);
7
8impl fmt::Display for Password {
9 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
10 write!(f, "***")
11 }
12}
13
14impl fmt::Debug for Password {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
16 write!(f, "Password(***)")
17 }
18}
19
20#[derive(Debug, Clone)]
21pub struct HashedPassword(String);
22
23#[derive(Debug, Error)]
24#[cfg_attr(test, derive(PartialEq))]
25pub enum ParseError {
26 #[error("password is too short (min. {0}")]
27 TooShort(usize),
28 #[error("password is too long (max. {0}")]
29 TooLong(usize),
30 #[error("password does not contain a digit")]
31 NoDigit,
32 #[error("invalid password")]
33 Invalid,
34}
35
36const MIN_LEN: usize = 3;
37const MAX_LEN: usize = 255;
38
39impl Password {
40 pub const MIN_LEN: usize = MIN_LEN;
41 pub const MAX_LEN: usize = MAX_LEN;
42
43 #[must_use]
44 #[allow(clippy::missing_panics_doc)] pub fn to_hashed(&self) -> HashedPassword {
46 debug_assert!(validate(&self.0, Self::MIN_LEN, Self::MAX_LEN).is_ok());
47 let hash = bcrypt::hash(&self.0).expect("bcrypt hash should never fail");
50 HashedPassword::from_hash(hash)
51 }
52}
53
54impl FromStr for Password {
55 type Err = ParseError;
56
57 fn from_str(password: &str) -> Result<Self, Self::Err> {
58 validate(password, Self::MIN_LEN, Self::MAX_LEN)?;
59 Ok(Self(password.to_string()))
60 }
61}
62
63impl HashedPassword {
64 #[must_use]
65 pub const fn from_hash(hashed: String) -> Self {
66 Self(hashed)
67 }
68
69 #[must_use]
70 pub fn verify(&self, password: &Password) -> bool {
71 pwhash::bcrypt::verify(&password.0, &self.0)
72 }
73
74 #[must_use]
75 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78}
79
80impl fmt::Display for HashedPassword {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
82 self.0.fmt(f)
83 }
84}
85
86const fn validate(password: &str, min_len: usize, max_len: usize) -> Result<(), ParseError> {
88 if password.len() < min_len {
89 return Err(ParseError::TooShort(min_len));
90 }
91 if password.len() > max_len {
92 return Err(ParseError::TooLong(max_len));
93 }
94 Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn min_length() {
103 let password = ["0"; MIN_LEN - 1].join("");
104 let result = password.parse::<Password>();
105 let err = result.err().unwrap();
106 assert_eq!(err, ParseError::TooShort(MIN_LEN));
107
108 let password = ["0"; MIN_LEN].join("");
109 let result = password.parse::<Password>();
110 assert!(result.is_ok());
111 }
112
113 #[test]
114 fn max_length() {
115 let password = ["x"; MAX_LEN + 1].join("");
116 let result = password.parse::<Password>();
117 let err = result.err().unwrap();
118 assert_eq!(err, ParseError::TooLong(MAX_LEN));
119
120 let password = ["0"; MAX_LEN].join("");
121 let result = password.parse::<Password>();
122 assert!(result.is_ok());
123 }
124}