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    hash.verify_password(algs, password)
93        .map_err(|_| VerifyError::PasswordInvalid)
94}
95
96/// Determine if the given password hash is using the recommended algorithm and
97/// parameters.
98///
99/// This can be used by implementations which wish to lazily update their
100/// password hashes (i.e. by rehashing the password with [`generate_hash`])
101/// to determine if such an update should be applied.
102///
103/// # Returns
104/// - `Ok(true)` if the hash *isn't* using the latest recommended algorithm/parameters.
105/// - `Ok(false)` if the hash *is* using the latest recommended algorithm/parameters.
106/// - `Err(ParseError)` if the hash could not be parsed.
107#[allow(unreachable_code)]
108pub fn is_hash_obsolete(hash: &str) -> Result<bool, ParseError> {
109    let hash = PasswordHash::new(hash).map_err(ParseError::new)?;
110
111    #[cfg(feature = "argon2")]
112    return Ok(hash.algorithm != argon2::Algorithm::default().ident()
113        || hash.params != default_params_string::<argon2::Params>());
114
115    #[cfg(feature = "scrypt")]
116    return Ok(hash.algorithm != scrypt::ALG_ID
117        || hash.params != default_params_string::<scrypt::Params>());
118
119    #[cfg(feature = "pbkdf2")]
120    return Ok(hash.algorithm != *pbkdf2::Algorithm::default().ident()
121        || hash.params != default_params_string::<pbkdf2::Params>());
122
123    Ok(true)
124}
125
126fn default_params_string<T>() -> ParamsString
127where
128    T: Default + TryInto<ParamsString, Error = password_hash::Error>,
129{
130    T::default().try_into().expect("invalid default params")
131}
132
133#[cfg(test)]
134mod tests {
135    use super::{generate_hash, is_hash_obsolete, verify_password};
136
137    const EXAMPLE_PASSWORD: &str = "password";
138
139    #[test]
140    fn happy_path() {
141        let hash = generate_hash(EXAMPLE_PASSWORD);
142        assert!(verify_password(EXAMPLE_PASSWORD, &hash).is_ok());
143        assert!(verify_password("bogus", &hash).is_err());
144        assert!(!is_hash_obsolete(&hash).expect("hash can be parsed"));
145    }
146
147    #[cfg(feature = "argon2")]
148    mod argon2 {
149        use super::{EXAMPLE_PASSWORD, verify_password};
150
151        /// Argon2 hash for the string "password".
152        const EXAMPLE_HASH: &str = "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo";
153
154        #[test]
155        fn verify() {
156            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
157            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
158        }
159    }
160
161    #[cfg(feature = "pbkdf2")]
162    mod pdkdf2 {
163        use super::verify_password;
164
165        /// PBKDF2 password test vector from the `pbkdf2` crate
166        const EXAMPLE_PASSWORD: &[u8] = b"passwordPASSWORDpassword";
167
168        /// PBKDF2 hash test vector from the `pbkdf2` crate
169        const EXAMPLE_HASH: &str = "$pbkdf2-sha256$i=4096,l=40\
170            $c2FsdFNBTFRzYWx0U0FMVHNhbHRTQUxUc2FsdFNBTFRzYWx0\
171            $NIyJ28vTKy8y2BS4EW6EzysXNH68GAAYHE4qH7jdU+HGNVGMfaxH6Q";
172
173        #[test]
174        fn verify() {
175            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
176            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
177        }
178    }
179
180    #[cfg(feature = "scrypt")]
181    mod scrypt {
182        use super::{EXAMPLE_PASSWORD, verify_password};
183
184        /// scrypt hash for the string "password".
185        const EXAMPLE_HASH: &str = "$scrypt$ln=16,r=8,p=1$aM15713r3Xsvxbi31lqr1Q$nFNh2CVHVjNldFVKDHDlm4CbdRSCdEBsjjJxD+iCs5E";
186
187        #[test]
188        fn verify() {
189            assert!(verify_password(EXAMPLE_PASSWORD, EXAMPLE_HASH).is_ok());
190            assert!(verify_password("bogus", EXAMPLE_HASH).is_err());
191        }
192    }
193}