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
44pub 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#[allow(unreachable_code)]
57fn generate_phc_hash<'a>(
58 password: &[u8],
59 salt: &'a SaltString,
60) -> password_hash::Result<PasswordHash<'a>> {
61 #[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
74pub 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#[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 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 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 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}