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
41const PASSWORD_HASH_SEPARATOR: char = '$';
43
44#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct PasswordHash {
78 pub algorithm: Ident,
83
84 pub version: Option<Decimal>,
88
89 pub params: ParamsString,
94
95 pub salt: Option<Salt>,
99
100 pub hash: Option<Output>,
104}
105
106impl PasswordHash {
107 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 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 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 #[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(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: String,
240}
241
242#[cfg(feature = "alloc")]
243#[allow(clippy::len_without_is_empty, deprecated)]
244impl PasswordHashString {
245 pub fn new(s: &str) -> Result<Self> {
247 PasswordHash::new(s).map(Into::into)
248 }
249
250 pub fn password_hash(&self) -> PasswordHash {
252 PasswordHash::new(&self.string).expect("malformed password hash")
253 }
254
255 pub fn as_str(&self) -> &str {
257 self.string.as_str()
258 }
259
260 pub fn as_bytes(&self) -> &[u8] {
262 self.as_str().as_bytes()
263 }
264
265 pub fn len(&self) -> usize {
267 self.as_str().len()
268 }
269
270 pub fn algorithm(&self) -> Ident {
272 self.password_hash().algorithm
273 }
274
275 pub fn version(&self) -> Option<Decimal> {
277 self.password_hash().version
278 }
279
280 pub fn params(&self) -> ParamsString {
282 self.password_hash().params
283 }
284
285 pub fn salt(&self) -> Option<Salt> {
287 self.password_hash().salt
288 }
289
290 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}