extensible_encrypter/hasher/
pbkdf2.rs

1use crate::error;
2use pbkdf2::{
3    password_hash::{Ident, PasswordHasher, SaltString},
4    Pbkdf2,
5};
6use rand_chacha::rand_core::SeedableRng;
7use rand_chacha::ChaCha12Rng;
8
9pub enum Algorithm {
10    Pbkdf2Sha256,
11    Pbkdf2Sha512,
12}
13
14pub struct Hasher;
15
16impl Hasher {
17    /// Hash a password using PBKDF2 with SHA-256 or SHA-512
18    ///
19    /// # Arguments
20    ///
21    /// * `password` - The password to hash
22    /// * `rounds` - The number of rounds to hash the password
23    /// * `algorithm` - The algorithm to use for hashing
24    /// * `override_salt` - Salt is optional, if not provided a random salt will be generated, this should be the default usage.  This is included for testing purposes.
25    ///
26    pub fn hash(
27        password: &str,
28        rounds: &u32,
29        algorithm: Algorithm,
30        override_salt: Option<SaltString>,
31    ) -> error::Result<HasherResult>
32where {
33        // A salt for PBKDF2 (should be unique per encryption)
34        let mut rng = ChaCha12Rng::from_os_rng();
35        let mut salt = SaltString::from_rng(&mut rng);
36        if let Some(value) = override_salt {
37            salt = SaltString::from_b64(value.as_str()).expect("salt is base64 encoded");
38        }
39
40        // Derive a 32-byte key using PBKDF2 with SHA-512
41        let algo = match algorithm {
42            Algorithm::Pbkdf2Sha256 => Ident::new("pbkdf2-sha256").expect("use SHA-256"),
43            Algorithm::Pbkdf2Sha512 => Ident::new("pbkdf2-sha512").expect("use SHA-512"),
44        };
45        let key = Pbkdf2
46            .hash_password_customized(
47                password.as_bytes(),
48                Some(algo),
49                None,
50                pbkdf2::Params {
51                    rounds: *rounds,
52                    output_length: 32,
53                },
54                &salt,
55            )
56            .expect("32-byte key generated");
57
58        // Convert the key to a fixed-size array
59        let key_hash = key.hash.unwrap();
60        let key_bytes = key_hash.as_bytes();
61        let key_array: [u8; 32] = key_bytes.try_into().unwrap();
62        let hash_hex = hex::encode(key_array);
63
64        let result = HasherResult::new(hash_hex, salt.to_string());
65
66        Ok(result)
67    }
68}
69
70pub struct HasherResult {
71    hash: String,
72    salt: String,
73}
74
75impl HasherResult {
76    pub fn new(hash: String, salt: String) -> Self {
77        Self { hash, salt }
78    }
79
80    pub fn hash(&self) -> String {
81        self.hash.to_string()
82    }
83
84    pub fn salt(&self) -> String {
85        self.salt.to_string()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::hasher::pbkdf2::Algorithm;
92    use pbkdf2::password_hash::SaltString;
93
94    use super::Hasher;
95
96    #[test]
97    fn assert_32_byte_key_length() {
98        const PBKDF_ROUNDS: u32 = 2;
99
100        // NOTE: uses a static salt value for testing purposes
101        let result = Hasher::hash(
102            "password",
103            &PBKDF_ROUNDS,
104            Algorithm::Pbkdf2Sha512,
105            Some(SaltString::from_b64("salt").unwrap()),
106        )
107        .unwrap();
108
109        let decoded_hash = hex::decode(&result.hash).unwrap();
110        assert_eq!(decoded_hash.len(), 32_usize);
111        assert_eq!(
112            &result.hash,
113            &"8eb89352a0724cd4dfd8230e895c0ed0182574c37a1173b40489366cd0a78723".to_string()
114        );
115        assert_eq!(result.salt, "salt".to_string());
116    }
117}