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#![forbid(unsafe_code)]
9#![warn(missing_docs, rust_2018_idioms, unused_lifetimes)]
10
11#[cfg(feature = "alloc")]
24extern crate alloc;
25#[cfg(feature = "std")]
26extern crate std;
27
28#[cfg(feature = "rand_core")]
29pub use rand_core;
30
31pub mod errors;
32
33mod encoding;
34mod ident;
35mod output;
36mod params;
37mod salt;
38mod traits;
39mod value;
40
41pub use crate::{
42 encoding::Encoding,
43 errors::{Error, Result},
44 ident::Ident,
45 output::Output,
46 params::ParamsString,
47 salt::{Salt, SaltString},
48 traits::{McfHasher, PasswordHasher, PasswordVerifier},
49 value::{Decimal, Value},
50};
51
52use core::fmt::{self, Debug};
53
54#[cfg(feature = "alloc")]
55use alloc::{
56 str::FromStr,
57 string::{String, ToString},
58};
59
60const PASSWORD_HASH_SEPARATOR: char = '$';
62
63#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct PasswordHash<'a> {
97 pub algorithm: Ident<'a>,
102
103 pub version: Option<Decimal>,
107
108 pub params: ParamsString,
113
114 pub salt: Option<Salt<'a>>,
118
119 pub hash: Option<Output>,
123}
124
125impl<'a> PasswordHash<'a> {
126 pub fn new(s: &'a str) -> Result<Self> {
128 Self::parse(s, Encoding::default())
129 }
130
131 pub fn parse(s: &'a str, encoding: Encoding) -> Result<Self> {
133 if s.is_empty() {
134 return Err(Error::PhcStringField);
135 }
136
137 let mut fields = s.split(PASSWORD_HASH_SEPARATOR);
138 let beginning = fields.next().expect("no first field");
139
140 if beginning.chars().next().is_some() {
141 return Err(Error::PhcStringField);
142 }
143
144 let algorithm = fields
145 .next()
146 .ok_or(Error::PhcStringField)
147 .and_then(Ident::try_from)?;
148
149 let mut version = None;
150 let mut params = ParamsString::new();
151 let mut salt = None;
152 let mut hash = None;
153
154 let mut next_field = fields.next();
155
156 if let Some(field) = next_field {
157 if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) {
159 version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?);
160 next_field = None;
161 }
162 }
163
164 if next_field.is_none() {
165 next_field = fields.next();
166 }
167
168 if let Some(field) = next_field {
169 if field.contains(params::PAIR_DELIMITER) {
171 params = field.parse()?;
172 next_field = None;
173 }
174 }
175
176 if next_field.is_none() {
177 next_field = fields.next();
178 }
179
180 if let Some(s) = next_field {
181 salt = Some(s.try_into()?);
182 }
183
184 if let Some(field) = fields.next() {
185 hash = Some(Output::decode(field, encoding)?);
186 }
187
188 if fields.next().is_some() {
189 return Err(Error::PhcStringTrailingData);
190 }
191
192 Ok(Self {
193 algorithm,
194 version,
195 params,
196 salt,
197 hash,
198 })
199 }
200
201 pub fn generate(
203 phf: impl PasswordHasher,
204 password: impl AsRef<[u8]>,
205 salt: impl Into<Salt<'a>>,
206 ) -> Result<Self> {
207 phf.hash_password(password.as_ref(), salt)
208 }
209
210 pub fn verify_password(
213 &self,
214 phfs: &[&dyn PasswordVerifier],
215 password: impl AsRef<[u8]>,
216 ) -> Result<()> {
217 for &phf in phfs {
218 if phf.verify_password(password.as_ref(), self).is_ok() {
219 return Ok(());
220 }
221 }
222
223 Err(Error::Password)
224 }
225
226 pub fn encoding(&self) -> Encoding {
228 self.hash.map(|h| h.encoding()).unwrap_or_default()
229 }
230
231 #[cfg(feature = "alloc")]
233 pub fn serialize(&self) -> PasswordHashString {
234 self.into()
235 }
236}
237
238impl<'a> TryFrom<&'a str> for PasswordHash<'a> {
241 type Error = Error;
242
243 fn try_from(s: &'a str) -> Result<Self> {
244 Self::new(s)
245 }
246}
247
248impl<'a> fmt::Display for PasswordHash<'a> {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?;
251
252 if let Some(version) = self.version {
253 write!(f, "{}v={}", PASSWORD_HASH_SEPARATOR, version)?;
254 }
255
256 if !self.params.is_empty() {
257 write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?;
258 }
259
260 if let Some(salt) = &self.salt {
261 write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, salt)?;
262
263 if let Some(hash) = &self.hash {
264 write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, hash)?;
265 }
266 }
267
268 Ok(())
269 }
270}
271
272#[cfg(feature = "alloc")]
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct PasswordHashString {
280 string: String,
282
283 encoding: Encoding,
285}
286
287#[cfg(feature = "alloc")]
288#[allow(clippy::len_without_is_empty)]
289impl PasswordHashString {
290 pub fn new(s: &str) -> Result<Self> {
292 Self::parse(s, Encoding::default())
293 }
294
295 pub fn parse(s: &str, encoding: Encoding) -> Result<Self> {
297 Ok(PasswordHash::parse(s, encoding)?.into())
298 }
299
300 pub fn password_hash(&self) -> PasswordHash<'_> {
302 PasswordHash::parse(&self.string, self.encoding).expect("malformed password hash")
303 }
304
305 pub fn encoding(&self) -> Encoding {
307 self.encoding
308 }
309
310 pub fn as_str(&self) -> &str {
312 self.string.as_str()
313 }
314
315 pub fn as_bytes(&self) -> &[u8] {
317 self.as_str().as_bytes()
318 }
319
320 pub fn len(&self) -> usize {
322 self.as_str().len()
323 }
324
325 pub fn algorithm(&self) -> Ident<'_> {
327 self.password_hash().algorithm
328 }
329
330 pub fn version(&self) -> Option<Decimal> {
332 self.password_hash().version
333 }
334
335 pub fn params(&self) -> ParamsString {
337 self.password_hash().params
338 }
339
340 pub fn salt(&self) -> Option<Salt<'_>> {
342 self.password_hash().salt
343 }
344
345 pub fn hash(&self) -> Option<Output> {
347 self.password_hash().hash
348 }
349}
350
351#[cfg(feature = "alloc")]
352impl AsRef<str> for PasswordHashString {
353 fn as_ref(&self) -> &str {
354 self.as_str()
355 }
356}
357
358#[cfg(feature = "alloc")]
359impl From<PasswordHash<'_>> for PasswordHashString {
360 fn from(hash: PasswordHash<'_>) -> PasswordHashString {
361 PasswordHashString::from(&hash)
362 }
363}
364
365#[cfg(feature = "alloc")]
366impl From<&PasswordHash<'_>> for PasswordHashString {
367 fn from(hash: &PasswordHash<'_>) -> PasswordHashString {
368 PasswordHashString {
369 string: hash.to_string(),
370 encoding: hash.encoding(),
371 }
372 }
373}
374
375#[cfg(feature = "alloc")]
376impl FromStr for PasswordHashString {
377 type Err = Error;
378
379 fn from_str(s: &str) -> Result<Self> {
380 Self::new(s)
381 }
382}
383
384#[cfg(feature = "alloc")]
385impl fmt::Display for PasswordHashString {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 f.write_str(self.as_str())
388 }
389}