Coverage Report

Created: 2025-12-14 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/password-hash-0.4.2/src/lib.rs
Line
Count
Source
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
#![forbid(unsafe_code)]
9
#![warn(missing_docs, rust_2018_idioms, unused_lifetimes)]
10
11
//!
12
//! # Usage
13
//!
14
//! This crate represents password hashes using the [`PasswordHash`] type, which
15
//! represents a parsed "PHC string" with the following format:
16
//!
17
//! ```text
18
//! $<id>[$v=<version>][$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
19
//! ```
20
//!
21
//! For more information, please see the documentation for [`PasswordHash`].
22
23
#[cfg(feature = "alloc")]
24
extern crate alloc;
25
#[cfg(feature = "std")]
26
extern crate std;
27
28
#[cfg(feature = "rand_core")]
29
#[cfg_attr(docsrs, doc(cfg(feature = "rand_core")))]
30
pub use rand_core;
31
32
pub mod errors;
33
34
mod encoding;
35
mod ident;
36
mod output;
37
mod params;
38
mod salt;
39
mod traits;
40
mod value;
41
42
pub use crate::{
43
    encoding::Encoding,
44
    errors::{Error, Result},
45
    ident::Ident,
46
    output::Output,
47
    params::ParamsString,
48
    salt::{Salt, SaltString},
49
    traits::{McfHasher, PasswordHasher, PasswordVerifier},
50
    value::{Decimal, Value},
51
};
52
53
use core::fmt::{self, Debug};
54
55
#[cfg(feature = "alloc")]
56
use alloc::{
57
    str::FromStr,
58
    string::{String, ToString},
59
};
60
61
/// Separator character used in password hashes (e.g. `$6$...`).
62
const PASSWORD_HASH_SEPARATOR: char = '$';
63
64
/// Password hash.
65
///
66
/// This type corresponds to the parsed representation of a PHC string as
67
/// described in the [PHC string format specification][1].
68
///
69
/// PHC strings have the following format:
70
///
71
/// ```text
72
/// $<id>[$v=<version>][$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
73
/// ```
74
///
75
/// where:
76
///
77
/// - `<id>` is the symbolic name for the function
78
/// - `<version>` is the algorithm version
79
/// - `<param>` is a parameter name
80
/// - `<value>` is a parameter value
81
/// - `<salt>` is an encoding of the salt
82
/// - `<hash>` is an encoding of the hash output
83
///
84
/// The string is then the concatenation, in that order, of:
85
///
86
/// - a `$` sign;
87
/// - the function symbolic name;
88
/// - optionally, a `$` sign followed by the algorithm version with a `v=version` format;
89
/// - optionally, a `$` sign followed by one or several parameters, each with a `name=value` format;
90
///   the parameters are separated by commas;
91
/// - optionally, a `$` sign followed by the (encoded) salt value;
92
/// - optionally, a `$` sign followed by the (encoded) hash output (the hash output may be present
93
///   only if the salt is present).
94
///
95
/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification
96
#[derive(Clone, Debug, Eq, PartialEq)]
97
pub struct PasswordHash<'a> {
98
    /// Password hashing algorithm identifier.
99
    ///
100
    /// This corresponds to the `<id>` field in a PHC string, a.k.a. the
101
    /// symbolic name for the function.
102
    pub algorithm: Ident<'a>,
103
104
    /// Optional version field.
105
    ///
106
    /// This corresponds to the `<version>` field in a PHC string.
107
    pub version: Option<Decimal>,
108
109
    /// Algorithm-specific parameters.
110
    ///
111
    /// This corresponds to the set of `$<param>=<value>(,<param>=<value>)*`
112
    /// name/value pairs in a PHC string.
113
    pub params: ParamsString,
114
115
    /// [`Salt`] string for personalizing a password hash output.
116
    ///
117
    /// This corresponds to the `<salt>` value in a PHC string.
118
    pub salt: Option<Salt<'a>>,
119
120
    /// Password hashing function [`Output`], a.k.a. hash/digest.
121
    ///
122
    /// This corresponds to the `<hash>` output in a PHC string.
123
    pub hash: Option<Output>,
124
}
125
126
impl<'a> PasswordHash<'a> {
127
    /// Parse a password hash from a string in the PHC string format.
128
0
    pub fn new(s: &'a str) -> Result<Self> {
129
0
        Self::parse(s, Encoding::default())
130
0
    }
131
132
    /// Parse a password hash from the given [`Encoding`].
133
0
    pub fn parse(s: &'a str, encoding: Encoding) -> Result<Self> {
134
0
        if s.is_empty() {
135
0
            return Err(Error::PhcStringTooShort);
136
0
        }
137
138
0
        let mut fields = s.split(PASSWORD_HASH_SEPARATOR);
139
0
        let beginning = fields.next().expect("no first field");
140
141
0
        if beginning.chars().next().is_some() {
142
0
            return Err(Error::PhcStringInvalid);
143
0
        }
144
145
0
        let algorithm = fields
146
0
            .next()
147
0
            .ok_or(Error::PhcStringTooShort)
148
0
            .and_then(Ident::try_from)?;
149
150
0
        let mut version = None;
151
0
        let mut params = ParamsString::new();
152
0
        let mut salt = None;
153
0
        let mut hash = None;
154
155
0
        let mut next_field = fields.next();
156
157
0
        if let Some(field) = next_field {
158
            // v=<version>
159
0
            if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) {
160
0
                version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?);
161
0
                next_field = None;
162
0
            }
163
0
        }
164
165
0
        if next_field.is_none() {
166
0
            next_field = fields.next();
167
0
        }
168
169
0
        if let Some(field) = next_field {
170
            // <param>=<value>
171
0
            if field.contains(params::PAIR_DELIMITER) {
172
0
                params = field.parse()?;
173
0
                next_field = None;
174
0
            }
175
0
        }
176
177
0
        if next_field.is_none() {
178
0
            next_field = fields.next();
179
0
        }
180
181
0
        if let Some(s) = next_field {
182
0
            salt = Some(s.try_into()?);
183
0
        }
184
185
0
        if let Some(field) = fields.next() {
186
0
            hash = Some(Output::decode(field, encoding)?);
187
0
        }
188
189
0
        if fields.next().is_some() {
190
0
            return Err(Error::PhcStringTooLong);
191
0
        }
192
193
0
        Ok(Self {
194
0
            algorithm,
195
0
            version,
196
0
            params,
197
0
            salt,
198
0
            hash,
199
0
        })
200
0
    }
201
202
    /// Generate a password hash using the supplied algorithm.
203
0
    pub fn generate(
204
0
        phf: impl PasswordHasher,
205
0
        password: impl AsRef<[u8]>,
206
0
        salt: &'a str,
207
0
    ) -> Result<Self> {
208
0
        phf.hash_password(password.as_ref(), salt)
209
0
    }
210
211
    /// Verify this password hash using the specified set of supported
212
    /// [`PasswordHasher`] trait objects.
213
0
    pub fn verify_password(
214
0
        &self,
215
0
        phfs: &[&dyn PasswordVerifier],
216
0
        password: impl AsRef<[u8]>,
217
0
    ) -> Result<()> {
218
0
        for &phf in phfs {
219
0
            if phf.verify_password(password.as_ref(), self).is_ok() {
220
0
                return Ok(());
221
0
            }
222
        }
223
224
0
        Err(Error::Password)
225
0
    }
226
227
    /// Get the [`Encoding`] that this [`PasswordHash`] is serialized with.
228
0
    pub fn encoding(&self) -> Encoding {
229
0
        self.hash.map(|h| h.encoding()).unwrap_or_default()
230
0
    }
231
232
    /// Serialize this [`PasswordHash`] as a [`PasswordHashString`].
233
    #[cfg(feature = "alloc")]
234
    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
235
    pub fn serialize(&self) -> PasswordHashString {
236
        self.into()
237
    }
238
}
239
240
// Note: this uses `TryFrom` instead of `FromStr` to support a lifetime on
241
// the `str` the value is being parsed from.
242
impl<'a> TryFrom<&'a str> for PasswordHash<'a> {
243
    type Error = Error;
244
245
0
    fn try_from(s: &'a str) -> Result<Self> {
246
0
        Self::new(s)
247
0
    }
248
}
249
250
impl<'a> fmt::Display for PasswordHash<'a> {
251
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252
0
        write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?;
253
254
0
        if let Some(version) = self.version {
255
0
            write!(f, "{}v={}", PASSWORD_HASH_SEPARATOR, version)?;
256
0
        }
257
258
0
        if !self.params.is_empty() {
259
0
            write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?;
260
0
        }
261
262
0
        if let Some(salt) = &self.salt {
263
0
            write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, salt)?;
264
265
0
            if let Some(hash) = &self.hash {
266
0
                write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, hash)?;
267
0
            }
268
0
        }
269
270
0
        Ok(())
271
0
    }
272
}
273
274
/// Serialized [`PasswordHash`].
275
///
276
/// This type contains a serialized password hash string which is ensured to
277
/// parse successfully.
278
// TODO(tarcieri): cached parsed representations? or at least structural data
279
#[cfg(feature = "alloc")]
280
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
281
#[derive(Clone, Debug, Eq, PartialEq)]
282
pub struct PasswordHashString {
283
    /// String value
284
    string: String,
285
286
    /// String encoding
287
    encoding: Encoding,
288
}
289
290
#[cfg(feature = "alloc")]
291
#[allow(clippy::len_without_is_empty)]
292
impl PasswordHashString {
293
    /// Parse a password hash from a string in the PHC string format.
294
    pub fn new(s: &str) -> Result<Self> {
295
        Self::parse(s, Encoding::default())
296
    }
297
298
    /// Parse a password hash from the given [`Encoding`].
299
    pub fn parse(s: &str, encoding: Encoding) -> Result<Self> {
300
        Ok(PasswordHash::parse(s, encoding)?.into())
301
    }
302
303
    /// Parse this owned string as a [`PasswordHash`].
304
    pub fn password_hash(&self) -> PasswordHash<'_> {
305
        PasswordHash::parse(&self.string, self.encoding).expect("malformed password hash")
306
    }
307
308
    /// Get the [`Encoding`] that this [`PasswordHashString`] is serialized with.
309
    pub fn encoding(&self) -> Encoding {
310
        self.encoding
311
    }
312
313
    /// Borrow this value as a `str`.
314
    pub fn as_str(&self) -> &str {
315
        self.string.as_str()
316
    }
317
318
    /// Borrow this value as bytes.
319
    pub fn as_bytes(&self) -> &[u8] {
320
        self.as_str().as_bytes()
321
    }
322
323
    /// Get the length of this value in ASCII characters.
324
    pub fn len(&self) -> usize {
325
        self.as_str().len()
326
    }
327
328
    /// Password hashing algorithm identifier.
329
    pub fn algorithm(&self) -> Ident<'_> {
330
        self.password_hash().algorithm
331
    }
332
333
    /// Optional version field.
334
    pub fn version(&self) -> Option<Decimal> {
335
        self.password_hash().version
336
    }
337
338
    /// Algorithm-specific parameters.
339
    pub fn params(&self) -> ParamsString {
340
        self.password_hash().params
341
    }
342
343
    /// [`Salt`] string for personalizing a password hash output.
344
    pub fn salt(&self) -> Option<Salt<'_>> {
345
        self.password_hash().salt
346
    }
347
348
    /// Password hashing function [`Output`], a.k.a. hash/digest.
349
    pub fn hash(&self) -> Option<Output> {
350
        self.password_hash().hash
351
    }
352
}
353
354
#[cfg(feature = "alloc")]
355
impl AsRef<str> for PasswordHashString {
356
    fn as_ref(&self) -> &str {
357
        self.as_str()
358
    }
359
}
360
361
#[cfg(feature = "alloc")]
362
impl From<PasswordHash<'_>> for PasswordHashString {
363
    fn from(hash: PasswordHash<'_>) -> PasswordHashString {
364
        PasswordHashString::from(&hash)
365
    }
366
}
367
368
#[cfg(feature = "alloc")]
369
impl From<&PasswordHash<'_>> for PasswordHashString {
370
    fn from(hash: &PasswordHash<'_>) -> PasswordHashString {
371
        PasswordHashString {
372
            string: hash.to_string(),
373
            encoding: hash.encoding(),
374
        }
375
    }
376
}
377
378
#[cfg(feature = "alloc")]
379
impl FromStr for PasswordHashString {
380
    type Err = Error;
381
382
    fn from_str(s: &str) -> Result<Self> {
383
        Self::new(s)
384
    }
385
}
386
387
#[cfg(feature = "alloc")]
388
impl fmt::Display for PasswordHashString {
389
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390
        f.write_str(self.as_str())
391
    }
392
}