password_auth/
lib.rs

1#![no_std]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
7)]
8#![warn(
9    clippy::checked_conversions,
10    clippy::arithmetic_side_effects,
11    clippy::panic,
12    clippy::panic_in_result_fn,
13    clippy::unwrap_used,
14    missing_docs,
15    rust_2018_idioms,
16    unused_lifetimes,
17    unused_qualifications
18)]
19
20extern crate alloc;
21#[cfg(feature = "std")]
22extern crate std;
23
24mod errors;
25
26pub use crate::errors::{ParseError, VerifyError};
27
28use alloc::string::{String, ToString};
29use password_hash::{
30    PasswordHasher, PasswordVerifier,
31    phc::{ParamsString, PasswordHash, Salt},
32};
33
34#[cfg(not(any(feature = "argon2", feature = "pbkdf2", feature = "scrypt")))]
35compile_error!(
36    "please enable at least one password hash crate feature, e.g. argon2, pbkdf2, scrypt"
37);
38
39#[cfg(feature = "argon2")]
40use argon2::Argon2;
41#[cfg(feature = "pbkdf2")]
42use pbkdf2::Pbkdf2;
43#[cfg(feature = "scrypt")]
44use scrypt::Scrypt;
45
46/// Generate a password hash for the given password.
47///
48/// Uses the best available password hashing algorithm given the enabled
49/// crate features (typically Argon2 unless explicitly disabled).
50pub fn generate_hash(password: impl AsRef<[u8]>) -> String {
51    let salt = Salt::generate();
52    generate_phc_hash(password.as_ref(), &salt)
53        .map(|hash| hash.to_string())
54        .expect("password hashing error")
55}
56
57/// Generate a PHC hash using the preferred algorithm.
58#[allow(unreachable_code)]
59fn generate_phc_hash(password: &[u8], salt: &[u8]) -> password_hash::Result<PasswordHash> {
60    //
61    // Algorithms below are in order of preference
62    //
63    #[cfg(feature = "argon2")]
64    return Argon2::default().hash_password(password, salt);
65
66    #[cfg(feature = "scrypt")]
67    return Scrypt.hash_password(password, salt);
68
69    #[cfg(feature = "pbkdf2")]
70    return Pbkdf2.hash_password(password, salt);
71}
72
73/// Verify the provided password against the provided password hash.
74///
75/// # Returns
76///
77/// - `Ok(())` if the password hash verified successfully
78/// - `Err(VerifyError)` if the hash didn't parse successfully or the password
79///   failed to verify against the hash.
80pub fn verify_password(password: impl AsRef<[u8]>, hash: &str) -> Result<(), VerifyError> {
81    let hash = PasswordHash::new(hash).map_err(ParseError::new)?;
82
83    let algs: &[&dyn PasswordVerifier<PasswordHash>] = &[
84        #[cfg(feature = "argon2")]
85        &Argon2::default(),
86        #[cfg(feature = "pbkdf2")]
87        &Pbkdf2,
88        #[cfg(feature = "scrypt")]
89        &Scrypt,
90    ];
91
92    for &alg in algs {
93        if alg.verify_password(password.as_ref(), &hash).is_ok() {
94            return Ok(());
95        }
96    }
97
98    Err(VerifyError::PasswordInvalid)
99}
100
101/// Determine if the given password hash is using the recommended algorithm and
102/// parameters.
103///
104/// This can be used by implementations which wish to lazily update their
105/// password hashes (i.e. by rehashing the password with [`generate_hash`])
106/// to determine if such an update should be applied.
107///
108/// # Returns
109/// - `Ok(true)` if the hash *isn't* using the latest recommended algorithm/parameters.
110/// - `Ok(false)` if the hash *is* using the latest recommended algorithm/parameters.
111/// - `Err(ParseError)` if the hash could not be parsed.
112#[allow(unreachable_code)]
113pub fn is_hash_obsolete(hash: &str) -> Result<bool, ParseError> {
114    let hash = PasswordHash::new(hash).map_err(ParseError::new)?;
115
116    #[cfg(feature = "argon2")]
117    return Ok(hash.algorithm != argon2::Algorithm::default().ident()
118        || hash.params != default_params_string::<argon2::Params>());
119
120    #[cfg(feature = "scrypt")]
121    return Ok(hash.algorithm != scrypt::ALG_ID
122        || hash.params != default_params_string::<scrypt::Params>());
123
124    #[cfg(feature = "pbkdf2")]
125    return Ok(hash.algorithm != *pbkdf2::Algorithm::default().ident()
126        || hash.params != default_params_string::<pbkdf2::Params>());
127
128    Ok(true)
129}
130
131fn default_params_string<T>() -> ParamsString
132where
133    T: Default + TryInto<ParamsString, Error = password_hash::Error>,
134{
135    T::default().try_into().expect("invalid default params")
136}
137
138#[cfg(test)]
139mod tests {
140    use super::{generate_hash, is_hash_obsolete, verify_password};
141
142    const EXAMPLE_PASSWORD: &str = "password";
143
144    #[test]
145    fn happy_path() {
146        let hash = generate_hash(EXAMPLE_PASSWORD);
147        assert!(verify_password(EXAMPLE_PASSWORD, &hash).is_ok());
148        assert!(verify_password("bogus", &hash).is_err());
149        assert!(!is_hash_obsolete(&hash).expect("hash can be parsed"));
150    }
151
152    #[cfg(feature = "argon2")]
153    mod argon2 {
154        use super::{EXAMPLE_PASSWORD, verify_password};
155
156        /// Argon2 hash for the string "password".
157        const EXAMPLE_HASH: &str = "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo";
158
159        #[test]
160        fn verify() {
161            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
162            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
163        }
164    }
165
166    #[cfg(feature = "pbkdf2")]
167    mod pdkdf2 {
168        use super::verify_password;
169
170        /// PBKDF2 password test vector from the `pbkdf2` crate
171        const EXAMPLE_PASSWORD: &[u8] = b"passwordPASSWORDpassword";
172
173        /// PBKDF2 hash test vector from the `pbkdf2` crate
174        const EXAMPLE_HASH: &str = "$pbkdf2-sha256$i=4096,l=40\
175            $c2FsdFNBTFRzYWx0U0FMVHNhbHRTQUxUc2FsdFNBTFRzYWx0\
176            $NIyJ28vTKy8y2BS4EW6EzysXNH68GAAYHE4qH7jdU+HGNVGMfaxH6Q";
177
178        #[test]
179        fn verify() {
180            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
181            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
182        }
183    }
184
185    #[cfg(feature = "scrypt")]
186    mod scrypt {
187        use super::{EXAMPLE_PASSWORD, verify_password};
188
189        /// scrypt hash for the string "password".
190        const EXAMPLE_HASH: &str = "$scrypt$ln=16,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E";
191
192        #[test]
193        fn verify() {
194            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
195            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
196        }
197    }
198}