password_auth/
lib.rs

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