hashed_password/
lib.rs

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/// [HashedPassword] store hashed password string.
9#[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    /// convert a string-like `HashedPassword` to Self
40    pub fn new<T: ToString>(hashed_password: T) -> Self {
41        Self(hashed_password.to_string())
42    }
43
44    /// convert `plain` to [HashedPassword], with `secret`
45    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    /// To validate `plain` is the plain password or not.
63    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(); // safe unwrap
68        let hash = split.next().unwrap_or_default(); // safe unwrap
69
70        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    /// convert self into string
79    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}