extensible_encrypter/hasher/
pbkdf2.rs

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