argon_hash_password/
lib.rs

1//! Functions for creating hashed passwords with salt using argon2
2//! ### Create a hashed password with salt
3//! ```
4//! let (hash, salt) = argon_hash_password::create_hash_and_salt("PlaintextPassword").unwrap();
5//! ```
6//! The hash and salt can then be stored
7//!
8//! ### Check a Hash
9//! ```ignore
10//! let check = argon_hash_password::check_password_matches_hash("PlaintextPassword", hash, salt).unwrap();
11//! match check {
12//!     true => println!("Correct plaintext password provided"),
13//!     false => println!("Incorrect plaintext password provided"),
14//! }
15//! ```
16
17use argon2::{
18    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
19    Argon2,
20};
21use rand::{distributions::Alphanumeric, Rng};
22use std::error::Error;
23
24/// Given a plaintext password return a password hash and a generated salt
25pub fn create_hash_and_salt(password: &str) -> Result<(String, String), Box<dyn Error>> {
26    let salt = SaltString::generate(&mut OsRng);
27    let hash = match hash_and_verify(password, salt.clone()) {
28        Ok(hash) => hash,
29        Err(e) => return Err(e),
30    };
31    Ok((hash, salt.to_string()))
32}
33
34/// Check that password and salt matches generated hash
35pub fn check_password_matches_hash(
36    password: &str,
37    expected_hash: &str,
38    salt: &str,
39) -> Result<bool, Box<dyn Error>> {
40    let parsed_salt = parse_saltstring(salt)?;
41    let hash = match hash_and_verify(password, parsed_salt) {
42        Ok(hash) => hash,
43        Err(e) => return Err(e),
44    };
45    if hash != expected_hash {
46        return Ok(false);
47    }
48    Ok(true)
49}
50
51/// Verify that the password matches a certain length and the confirmation password provided
52pub fn verify_password_len(password: &str) -> bool {
53    if password.len() < 8 {
54        return false;
55    }
56    if password.len() > 128 {
57        return false;
58    }
59    true
60}
61
62pub fn parse_saltstring(salt: &str) -> Result<SaltString, Box<dyn Error>> {
63    match SaltString::new(salt) {
64        Ok(parsed_salt) => Ok(parsed_salt),
65        Err(e) => return Err(format!("Failed to parse provided salt: {}", e).into()),
66    }
67}
68
69/// Generate a secure 128-bit session ID of alphanumeric characters
70pub fn gen_session_id() -> String {
71    rand::thread_rng()
72        .sample_iter(&Alphanumeric)
73        .take(128)
74        .map(char::from)
75        .collect()
76}
77
78/// Given a plaintext password and a SaltString, return the hash of the password
79pub fn hash_and_verify(password: &str, salt: SaltString) -> Result<String, Box<dyn Error>> {
80    let argon2 = Argon2::default();
81    let hash = match argon2.hash_password(password.as_bytes(), &salt) {
82        Ok(hash) => hash.to_string(),
83        Err(e) => return Err(format!("Failed to hash password: {}", e).into()),
84    };
85
86    let parsed_hash = match PasswordHash::new(&hash) {
87        Ok(parsed_hash) => parsed_hash,
88        Err(e) => return Err(format!("Failed parse hash: {}", e).into()),
89    };
90    match argon2.verify_password(password.as_bytes(), &parsed_hash) {
91        Ok(_) => (),
92        Err(e) => {
93            return Err(format!("Failed to verify hashed password: {}", e).into());
94        }
95    }
96    Ok(hash)
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    const PASSWORD: &str = "samplePass123";
104
105    #[test]
106    fn argon_same_password_not_matching_due_to_salt() {
107        let (first_hash, first_salt) =
108            create_hash_and_salt(PASSWORD).expect("Failed to create hashed password first time");
109        let (second_hash, second_salt) =
110            create_hash_and_salt(PASSWORD).expect("Failed to create hashed password second time");
111        assert_ne!(first_hash, second_hash);
112        assert_ne!(first_salt, second_salt);
113    }
114
115    #[test]
116    fn argon_same_password_does_match() {
117        let (hash, salt) =
118            create_hash_and_salt(PASSWORD).expect("Failed to create hashed password");
119        let check_hash = check_password_matches_hash(PASSWORD, &hash, &salt)
120            .expect("Failed to check password hash");
121        assert!(check_hash);
122    }
123
124    #[test]
125    fn argon_different_password_does_not_match() {
126        let (hash, salt) =
127            create_hash_and_salt(PASSWORD).expect("Failed to create hashed password");
128        let check_hash = check_password_matches_hash("aDifferentPassword123", &hash, &salt)
129            .expect("Failed to check password hash");
130        assert!(!check_hash);
131    }
132}