pbkdf2/
simple.rs

1//! Implementation of the `password-hash` crate API.
2
3use crate::pbkdf2_hmac;
4use core::{cmp::Ordering, fmt, str::FromStr};
5use password_hash::{
6    errors::InvalidValue, Decimal, Error, Ident, Output, ParamsString, PasswordHash,
7    PasswordHasher, Result, Salt,
8};
9use sha2::{Sha256, Sha512};
10
11#[cfg(feature = "sha1")]
12use sha1::Sha1;
13
14/// PBKDF2 type for use with [`PasswordHasher`].
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
17pub struct Pbkdf2;
18
19impl PasswordHasher for Pbkdf2 {
20    type Params = Params;
21
22    fn hash_password_customized<'a>(
23        &self,
24        password: &[u8],
25        alg_id: Option<Ident<'a>>,
26        version: Option<Decimal>,
27        params: Params,
28        salt: impl Into<Salt<'a>>,
29    ) -> Result<PasswordHash<'a>> {
30        let algorithm = Algorithm::try_from(alg_id.unwrap_or(Algorithm::default().ident()))?;
31
32        // Versions unsupported
33        if version.is_some() {
34            return Err(Error::Version);
35        }
36
37        let salt = salt.into();
38        let mut salt_arr = [0u8; 64];
39        let salt_bytes = salt.decode_b64(&mut salt_arr)?;
40
41        let output = Output::init_with(params.output_length, |out| {
42            let f = match algorithm {
43                #[cfg(feature = "sha1")]
44                Algorithm::Pbkdf2Sha1 => pbkdf2_hmac::<Sha1>,
45                Algorithm::Pbkdf2Sha256 => pbkdf2_hmac::<Sha256>,
46                Algorithm::Pbkdf2Sha512 => pbkdf2_hmac::<Sha512>,
47            };
48
49            f(password, salt_bytes, params.rounds, out);
50            Ok(())
51        })?;
52
53        Ok(PasswordHash {
54            algorithm: algorithm.ident(),
55            version: None,
56            params: params.try_into()?,
57            salt: Some(salt),
58            hash: Some(output),
59        })
60    }
61}
62
63/// PBKDF2 variants.
64///
65/// <https://en.wikipedia.org/wiki/PBKDF2>
66#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
67#[non_exhaustive]
68#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
69pub enum Algorithm {
70    /// PBKDF2 SHA1
71    #[cfg(feature = "sha1")]
72    #[cfg_attr(docsrs, doc(cfg(feature = "sha1")))]
73    Pbkdf2Sha1,
74
75    /// PBKDF2 SHA-256
76    Pbkdf2Sha256,
77
78    /// PBKDF2 SHA-512
79    Pbkdf2Sha512,
80}
81
82impl Default for Algorithm {
83    /// Default suggested by the [OWASP cheat sheet]:
84    ///
85    /// > Use PBKDF2 with a work factor of 600,000 or more and set with an
86    /// > internal hash function of HMAC-SHA-256.
87    ///
88    /// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
89    fn default() -> Self {
90        Self::Pbkdf2Sha256
91    }
92}
93
94impl Algorithm {
95    /// PBKDF2 (SHA-1) algorithm identifier
96    #[cfg(feature = "sha1")]
97    pub const PBKDF2_SHA1_IDENT: Ident<'static> = Ident::new_unwrap("pbkdf2");
98
99    /// PBKDF2 (SHA-256) algorithm identifier
100    pub const PBKDF2_SHA256_IDENT: Ident<'static> = Ident::new_unwrap("pbkdf2-sha256");
101
102    /// PBKDF2 (SHA-512) algorithm identifier
103    pub const PBKDF2_SHA512_IDENT: Ident<'static> = Ident::new_unwrap("pbkdf2-sha512");
104
105    /// Parse an [`Algorithm`] from the provided string.
106    pub fn new(id: impl AsRef<str>) -> Result<Self> {
107        id.as_ref().parse()
108    }
109
110    /// Get the [`Ident`] that corresponds to this PBKDF2 [`Algorithm`].
111    pub fn ident(&self) -> Ident<'static> {
112        match self {
113            #[cfg(feature = "sha1")]
114            Algorithm::Pbkdf2Sha1 => Self::PBKDF2_SHA1_IDENT,
115            Algorithm::Pbkdf2Sha256 => Self::PBKDF2_SHA256_IDENT,
116            Algorithm::Pbkdf2Sha512 => Self::PBKDF2_SHA512_IDENT,
117        }
118    }
119
120    /// Get the identifier string for this PBKDF2 [`Algorithm`].
121    pub fn as_str(&self) -> &str {
122        self.ident().as_str()
123    }
124}
125
126impl AsRef<str> for Algorithm {
127    fn as_ref(&self) -> &str {
128        self.as_str()
129    }
130}
131
132impl fmt::Display for Algorithm {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(self.as_str())
135    }
136}
137
138impl FromStr for Algorithm {
139    type Err = Error;
140
141    fn from_str(s: &str) -> Result<Algorithm> {
142        Ident::try_from(s)?.try_into()
143    }
144}
145
146impl From<Algorithm> for Ident<'static> {
147    fn from(alg: Algorithm) -> Ident<'static> {
148        alg.ident()
149    }
150}
151
152impl<'a> TryFrom<Ident<'a>> for Algorithm {
153    type Error = Error;
154
155    fn try_from(ident: Ident<'a>) -> Result<Algorithm> {
156        match ident {
157            #[cfg(feature = "sha1")]
158            Self::PBKDF2_SHA1_IDENT => Ok(Algorithm::Pbkdf2Sha1),
159            Self::PBKDF2_SHA256_IDENT => Ok(Algorithm::Pbkdf2Sha256),
160            Self::PBKDF2_SHA512_IDENT => Ok(Algorithm::Pbkdf2Sha512),
161            _ => Err(Error::Algorithm),
162        }
163    }
164}
165
166/// PBKDF2 params
167#[cfg_attr(docsrs, doc(cfg(feature = "simple")))]
168#[derive(Copy, Clone, Debug, Eq, PartialEq)]
169pub struct Params {
170    /// Number of rounds
171    pub rounds: u32,
172
173    /// Size of the output (in bytes)
174    pub output_length: usize,
175}
176
177impl Params {
178    /// Recommended number of PBKDF2 rounds (used by default).
179    ///
180    /// This number is adopted from the [OWASP cheat sheet]:
181    ///
182    /// > Use PBKDF2 with a work factor of 600,000 or more
183    ///
184    /// [OWASP cheat sheet]: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
185    pub const RECOMMENDED_ROUNDS: usize = 600_000;
186}
187
188impl Default for Params {
189    fn default() -> Params {
190        Params {
191            rounds: Self::RECOMMENDED_ROUNDS as u32,
192            output_length: 32,
193        }
194    }
195}
196
197impl<'a> TryFrom<&'a PasswordHash<'a>> for Params {
198    type Error = Error;
199
200    fn try_from(hash: &'a PasswordHash<'a>) -> Result<Self> {
201        let mut params = Params::default();
202        let mut output_length = None;
203
204        if hash.version.is_some() {
205            return Err(Error::Version);
206        }
207
208        for (ident, value) in hash.params.iter() {
209            match ident.as_str() {
210                "i" => params.rounds = value.decimal()?,
211                "l" => {
212                    output_length = Some(
213                        value
214                            .decimal()?
215                            .try_into()
216                            .map_err(|_| InvalidValue::Malformed.param_error())?,
217                    )
218                }
219                _ => return Err(Error::ParamNameInvalid),
220            }
221        }
222
223        if let Some(len) = output_length {
224            if let Some(hash) = &hash.hash {
225                match hash.len().cmp(&len) {
226                    Ordering::Less => return Err(InvalidValue::TooShort.param_error()),
227                    Ordering::Greater => return Err(InvalidValue::TooLong.param_error()),
228                    Ordering::Equal => (),
229                }
230            }
231
232            params.output_length = len;
233        }
234
235        Ok(params)
236    }
237}
238
239impl<'a> TryFrom<Params> for ParamsString {
240    type Error = Error;
241
242    fn try_from(input: Params) -> Result<ParamsString> {
243        let mut output = ParamsString::new();
244        output.add_decimal("i", input.rounds)?;
245        output.add_decimal("l", input.output_length as u32)?;
246        Ok(output)
247    }
248}