Skip to main content

phc/
lib.rs

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/6ee8e381/logo.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/6ee8e381/logo.svg"
7)]
8#![forbid(unsafe_code)]
9#![warn(
10    clippy::mod_module_files,
11    clippy::unwrap_used,
12    missing_docs,
13    unused_qualifications
14)]
15
16#[cfg(feature = "alloc")]
17extern crate alloc;
18
19mod error;
20mod ident;
21mod output;
22mod params;
23mod salt;
24mod string_buf;
25mod value;
26
27pub use error::{Error, Result};
28pub use ident::Ident;
29pub use output::Output;
30pub use params::ParamsString;
31pub use salt::{Salt, SaltString};
32pub use value::{Decimal, Value};
33
34use base64ct::Base64Unpadded as B64;
35use core::{fmt, str::FromStr};
36use string_buf::StringBuf;
37
38#[cfg(feature = "alloc")]
39use alloc::string::{String, ToString};
40
41/// Separator character used in password hashes (e.g. `$6$...`).
42const PASSWORD_HASH_SEPARATOR: char = '$';
43
44/// Password hash.
45///
46/// This type corresponds to the parsed representation of a PHC string as
47/// described in the [PHC string format specification][1].
48///
49/// PHC strings have the following format:
50///
51/// ```text
52/// $<id>[$v=<version>][$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
53/// ```
54///
55/// where:
56///
57/// - `<id>` is the symbolic name for the function
58/// - `<version>` is the algorithm version
59/// - `<param>` is a parameter name
60/// - `<value>` is a parameter value
61/// - `<salt>` is an encoding of the salt
62/// - `<hash>` is an encoding of the hash output
63///
64/// The string is then the concatenation, in that order, of:
65///
66/// - a `$` sign;
67/// - the function symbolic name;
68/// - optionally, a `$` sign followed by the algorithm version with a `v=version` format;
69/// - optionally, a `$` sign followed by one or several parameters, each with a `name=value` format;
70///   the parameters are separated by commas;
71/// - optionally, a `$` sign followed by the (encoded) salt value;
72/// - optionally, a `$` sign followed by the (encoded) hash output (the hash output may be present
73///   only if the salt is present).
74///
75/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct PasswordHash {
78    /// Password hashing algorithm identifier.
79    ///
80    /// This corresponds to the `<id>` field in a PHC string, a.k.a. the
81    /// symbolic name for the function.
82    pub algorithm: Ident,
83
84    /// Optional version field.
85    ///
86    /// This corresponds to the `<version>` field in a PHC string.
87    pub version: Option<Decimal>,
88
89    /// Algorithm-specific parameters.
90    ///
91    /// This corresponds to the set of `$<param>=<value>(,<param>=<value>)*`
92    /// name/value pairs in a PHC string.
93    pub params: ParamsString,
94
95    /// [`Salt`] string for personalizing a password hash output.
96    ///
97    /// This corresponds to the `<salt>` value in a PHC string.
98    pub salt: Option<Salt>,
99
100    /// Password hashing function [`Output`], a.k.a. hash/digest.
101    ///
102    /// This corresponds to the `<hash>` output in a PHC string.
103    pub hash: Option<Output>,
104}
105
106impl PasswordHash {
107    /// Parse a password hash from a string in the PHC string format.
108    pub fn new(s: &str) -> Result<Self> {
109        if s.is_empty() {
110            return Err(Error::MissingField);
111        }
112
113        let mut fields = s.split(PASSWORD_HASH_SEPARATOR);
114        let beginning = fields.next().expect("no first field");
115
116        if beginning.chars().next().is_some() {
117            return Err(Error::MissingField);
118        }
119
120        let algorithm = fields
121            .next()
122            .ok_or(Error::MissingField)
123            .and_then(Ident::from_str)?;
124
125        let mut version = None;
126        let mut params = ParamsString::new();
127        let mut salt = None;
128        let mut hash = None;
129
130        let mut next_field = fields.next();
131
132        if let Some(field) = next_field {
133            // v=<version>
134            if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) {
135                version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?);
136                next_field = None;
137            }
138        }
139
140        if next_field.is_none() {
141            next_field = fields.next();
142        }
143
144        if let Some(field) = next_field {
145            // <param>=<value>
146            if field.contains(params::PAIR_DELIMITER) {
147                params = field.parse()?;
148                next_field = None;
149            }
150        }
151
152        if next_field.is_none() {
153            next_field = fields.next();
154        }
155
156        if let Some(s) = next_field {
157            salt = Some(s.parse()?);
158        }
159
160        if let Some(field) = fields.next() {
161            hash = Some(Output::decode(field)?);
162        }
163
164        if fields.next().is_some() {
165            return Err(Error::TrailingData);
166        }
167
168        Ok(Self {
169            algorithm,
170            version,
171            params,
172            salt,
173            hash,
174        })
175    }
176
177    /// DEPRECATED: serialize this [`PasswordHash`] as a [`PasswordHashString`].
178    #[allow(deprecated)]
179    #[cfg(feature = "alloc")]
180    #[deprecated(since = "0.3.0", note = "Use `PasswordHash` or `String` instead")]
181    pub fn serialize(&self) -> PasswordHashString {
182        self.into()
183    }
184}
185
186impl FromStr for PasswordHash {
187    type Err = Error;
188
189    fn from_str(s: &str) -> Result<Self> {
190        Self::new(s)
191    }
192}
193
194impl TryFrom<&str> for PasswordHash {
195    type Error = Error;
196
197    fn try_from(s: &str) -> Result<Self> {
198        Self::new(s)
199    }
200}
201
202impl fmt::Display for PasswordHash {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?;
205
206        if let Some(version) = self.version {
207            write!(f, "{PASSWORD_HASH_SEPARATOR}v={version}")?;
208        }
209
210        if !self.params.is_empty() {
211            write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?;
212        }
213
214        if let Some(salt) = &self.salt {
215            write!(f, "{PASSWORD_HASH_SEPARATOR}{salt}")?;
216
217            if let Some(hash) = &self.hash {
218                write!(f, "{PASSWORD_HASH_SEPARATOR}{hash}")?;
219            }
220        }
221
222        Ok(())
223    }
224}
225
226/// DEPRECATED: serialized [`PasswordHash`].
227///
228/// This type contains a serialized password hash string which is ensured to
229/// parse successfully.
230///
231/// This was originally available to provide an owned representation for a password hash, but now
232/// that [`PasswordHash`] is fully owned, it can be used instead, or `String` to store a
233/// serialized PHC string.
234#[deprecated(since = "0.3.0", note = "Use `PasswordHash` or `String` instead")]
235#[cfg(feature = "alloc")]
236#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct PasswordHashString {
238    /// String value
239    string: String,
240}
241
242#[cfg(feature = "alloc")]
243#[allow(clippy::len_without_is_empty, deprecated)]
244impl PasswordHashString {
245    /// Parse a password hash from a string in the PHC string format.
246    pub fn new(s: &str) -> Result<Self> {
247        PasswordHash::new(s).map(Into::into)
248    }
249
250    /// Parse this owned string as a [`PasswordHash`].
251    pub fn password_hash(&self) -> PasswordHash {
252        PasswordHash::new(&self.string).expect("malformed password hash")
253    }
254
255    /// Borrow this value as a `str`.
256    pub fn as_str(&self) -> &str {
257        self.string.as_str()
258    }
259
260    /// Borrow this value as bytes.
261    pub fn as_bytes(&self) -> &[u8] {
262        self.as_str().as_bytes()
263    }
264
265    /// Get the length of this value in ASCII characters.
266    pub fn len(&self) -> usize {
267        self.as_str().len()
268    }
269
270    /// Password hashing algorithm identifier.
271    pub fn algorithm(&self) -> Ident {
272        self.password_hash().algorithm
273    }
274
275    /// Optional version field.
276    pub fn version(&self) -> Option<Decimal> {
277        self.password_hash().version
278    }
279
280    /// Algorithm-specific parameters.
281    pub fn params(&self) -> ParamsString {
282        self.password_hash().params
283    }
284
285    /// [`Salt`] string for personalizing a password hash output.
286    pub fn salt(&self) -> Option<Salt> {
287        self.password_hash().salt
288    }
289
290    /// Password hashing function [`Output`], a.k.a. hash/digest.
291    pub fn hash(&self) -> Option<Output> {
292        self.password_hash().hash
293    }
294}
295
296#[allow(deprecated)]
297#[cfg(feature = "alloc")]
298impl AsRef<str> for PasswordHashString {
299    fn as_ref(&self) -> &str {
300        self.as_str()
301    }
302}
303
304#[allow(deprecated)]
305#[cfg(feature = "alloc")]
306impl From<PasswordHash> for PasswordHashString {
307    fn from(hash: PasswordHash) -> PasswordHashString {
308        PasswordHashString::from(&hash)
309    }
310}
311
312#[allow(deprecated)]
313#[cfg(feature = "alloc")]
314impl From<&PasswordHash> for PasswordHashString {
315    fn from(hash: &PasswordHash) -> PasswordHashString {
316        PasswordHashString {
317            string: hash.to_string(),
318        }
319    }
320}
321
322#[allow(deprecated)]
323#[cfg(feature = "alloc")]
324impl FromStr for PasswordHashString {
325    type Err = Error;
326
327    fn from_str(s: &str) -> Result<Self> {
328        Self::new(s)
329    }
330}
331
332#[allow(deprecated)]
333#[cfg(feature = "alloc")]
334impl fmt::Display for PasswordHashString {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        f.write_str(self.as_str())
337    }
338}