1use std::fmt::{Display, Formatter};
2use rand::{Rng, thread_rng};
3use rand::distributions::Alphanumeric;
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8#[derive(Debug, Eq, PartialEq, Hash, Clone)]
10#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
12#[cfg_attr(feature = "sqlx", sqlx(transparent))]
13pub struct HashedPassword(String);
14
15impl HashedPassword {
16 #[inline]
17 fn separator() -> char {
18 '$'
19 }
20
21 #[inline]
22 fn salt() -> String {
23 thread_rng()
24 .sample_iter(&Alphanumeric)
25 .take(6)
26 .map(char::from)
27 .collect()
28 }
29
30 fn hash<T: AsRef<[u8]>>(s: T) -> String {
31 let mut hasher = Sha256::new();
32 hasher.update(s.as_ref());
33 let result = hasher.finalize();
34 let result = &result[..];
35
36 hex::encode(result)
37 }
38
39 pub fn new<T: ToString>(hashed_password: T) -> Self {
41 Self(hashed_password.to_string())
42 }
43
44 pub fn from_plain<T: AsRef<str>, S: AsRef<[u8]>>(plain: T, secret: S) -> Self {
46 let salt = Self::salt();
47 Self::hash_with_salt(plain, secret, salt)
48 }
49
50 fn hash_with_salt<T, S, V>(plain: T, secret: S, salt: V) -> Self
51 where
52 T: AsRef<str>,
53 S: AsRef<[u8]>,
54 V: AsRef<str>,
55 {
56 let row = format!("{}{}{}", salt.as_ref(), plain.as_ref(), hex::encode(secret));
57 let hash = Self::hash(&row);
58
59 Self(format!("{}{}{}", salt.as_ref(), Self::separator(), hash))
60 }
61
62 pub fn validate<T: AsRef<str>, S: AsRef<[u8]>>(&self, plain: T, secret: S) -> bool {
64 let plain = plain.as_ref();
65 let mut split = self.as_str().splitn(2, '$');
66
67 let salt = split.next().unwrap_or_default(); let hash = split.next().unwrap_or_default(); if salt.is_empty() || hash.is_empty() {
71 return false;
72 }
73
74 let result = Self::hash_with_salt(plain, secret, salt);
75 result.0 == self.as_str()
76 }
77
78 pub fn as_str(&self) -> &str {
80 &self.0
81 }
82}
83
84impl Display for HashedPassword {
85 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
86 f.write_str(self.as_str())
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use crate::HashedPassword;
93
94 #[test]
95 fn all() {
96 let secret = b"1234567890";
97 let plain = "abcdefg";
98
99 let password = HashedPassword::from_plain(plain, secret);
100 assert!(password.validate("abcdefg", secret));
101 assert!(!password.validate("abcdef", secret));
102 }
103}