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
46pub 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#[allow(unreachable_code)]
59fn generate_phc_hash(password: &[u8], salt: &[u8]) -> password_hash::Result<PasswordHash> {
60 #[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
73pub 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#[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 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 const EXAMPLE_PASSWORD: &[u8] = b"passwordPASSWORDpassword";
172
173 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 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}