auth/
hashing.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, Params, password_hash::SaltString};
use rand_core::OsRng; 
use crate::config::PasswordHashConfig;

/// Hashes a plain-text password using the Argon2id algorithm and the provided configuration.
///
/// A cryptographically secure random salt is generated automatically and included in the final hash.
/// The output hash is encoded in the [PHC string format](https://github.com/P-H-C/phc-string-format)
/// and can be safely stored in a database for later verification.
///
/// # Arguments
///
/// * `password` - The plain-text password to hash.
/// * `cfg` - A reference to a `PasswordHashConfig` struct containing Argon2 parameters.
///
/// # Returns
///
/// A `Result` containing the encoded password hash on success, or a `argon2::password_hash::Error` on failure.
///
/// # Example
///
/// ```rust
/// use auth::hashing::hash_password;
/// use auth::config::PasswordHashConfig;
///
/// let cfg = PasswordHashConfig {
///     mem_cost: 65536,
///     time_cost: 3,
///     lanes: 4,
///     hash_length: 32,
/// };
///
/// let password = "my_secure_password";
/// let hash = hash_password(password, &cfg).expect("Failed to hash password");
///
/// println!("Password hash: {}", hash);
/// assert!(hash.starts_with("$argon2id$"));
/// ```
pub fn hash_password(password: &str, cfg: &PasswordHashConfig) -> Result<String, argon2::password_hash::Error> {
    let salt = SaltString::generate(&mut OsRng);

    let params = Params::new(
        cfg.mem_cost,
        cfg.time_cost,
        cfg.lanes,
        Some(cfg.hash_length),
    )?;

    let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);

    let password_hash = argon2.hash_password(password.as_bytes(), &salt)?;
    Ok(password_hash.to_string()) // includes salt in the output
}
/// Verifies that a plain-text password matches a previously generated Argon2 hash.
///
/// The hash must be in [PHC string format](https://github.com/P-H-C/phc-string-format), 
/// which includes information about the algorithm, salt, and parameters used. This is the format
/// produced by [`hash_password`](fn.hash_password.html).
///
/// # Arguments
///
/// * `hash` - A PHC-encoded Argon2 hash string (e.g., from your database).
/// * `password` - The plain-text password to verify.
///
/// # Returns
///
/// A `Result` containing `true` if the password matches the hash, or `false` if it does not.
/// Returns an error if the hash cannot be parsed or if verification fails unexpectedly.
///
/// # Example
///
/// ```rust
/// use auth::hashing::{hash_password, verify_password};
/// use auth::config::PasswordHashConfig;
///
/// let cfg = PasswordHashConfig {
///     mem_cost: 65536,
///     time_cost: 3,
///     lanes: 4,
///     hash_length: 32,
/// };
///
/// let password = "my_secure_password";
/// let hash = hash_password(password, &cfg).expect("Hashing failed");
///
/// let is_valid = verify_password(&hash, password).expect("Verification failed");
/// assert!(is_valid);
///
/// let wrong = "wrong_password";
/// let is_valid = verify_password(&hash, wrong).expect("Verification failed");
/// assert!(!is_valid);
/// ```
pub fn verify_password(hash: &str, password: &str) -> Result<bool, argon2::password_hash::Error> {
    let parsed_hash = PasswordHash::new(hash)?;
    let argon2 = Argon2::default();

    Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}

#[cfg(test)]
mod tests {
    use super::*;
    
    fn get_hash_config() -> PasswordHashConfig {
        PasswordHashConfig {
            mem_cost: 65536,
            time_cost: 3,
            lanes: 4,
            hash_length: 32,
        }
    }    

    #[test]
    fn hash_password_should_succeed() {
        let password = "super_secure_password";
        let result = hash_password(password, &get_hash_config());
        assert!(result.is_ok(), "Password hashing should succeed");

        let hash = result.unwrap();
        assert!(
            hash.starts_with("$argon2id$"),
            "Hash should use argon2id and be correctly formatted"
        );
    }

    #[test]
    fn verify_password_should_succeed() {
        let password = "super_secure_password";
        let hash = hash_password(password, &get_hash_config()).expect("Password hashing did not succeed");

        let is_valid = verify_password(&hash, password).expect("Password verification generated an unexpected error");
        assert!(is_valid, "Password verification failed (but success was expected");
    }

    #[test]
    fn verify_password_should_fail() {
        let good_password = "super_secure_password";
        let hash = hash_password(good_password, &get_hash_config()).expect("Password hashing did not succeed");
        let bad_password = "wrong_password";
        let is_valid = verify_password(&hash, bad_password).expect("Password verification generated an unexpected error");
        assert!(!is_valid, "Password verification succeeded (but failure was expected");
    }

}