Skip to main content

phc/
salt.rs

1//! Salt string support.
2
3use crate::{Error, Result, StringBuf};
4use base64ct::{Base64Unpadded as B64, Encoding};
5use core::{
6    fmt,
7    ops::Deref,
8    str::{self, FromStr},
9};
10#[cfg(feature = "rand_core")]
11use rand_core::{CryptoRng, TryCryptoRng};
12
13/// Error message used with `expect` for when internal invariants are violated
14/// (i.e. the contents of a [`Salt`] should always be valid)
15const INVARIANT_VIOLATED_MSG: &str = "salt string invariant violated";
16
17/// In password hashing, a "salt" is an additional value used to
18/// personalize/tweak the output of a password hashing function for a given
19/// input password.
20///
21/// Salts help defend against attacks based on precomputed tables of hashed
22/// passwords, i.e. "[rainbow tables][1]".
23///
24/// The [`Salt`] type implements the RECOMMENDED best practices for salts
25/// described in the [PHC string format specification][2], namely:
26///
27/// > - Maximum lengths for salt, output and parameter values are meant to help
28/// >   consumer implementations, in particular written in C and using
29/// >   stack-allocated buffers. These buffers must account for the worst case,
30/// >   i.e. the maximum defined length. Therefore, keep these lengths low.
31/// > - The role of salts is to achieve uniqueness. A random salt is fine for
32/// >   that as long as its length is sufficient; a 16-byte salt would work well
33/// >   (by definition, UUID are very good salts, and they encode over exactly
34/// >   16 bytes). 16 bytes encode as 22 characters in B64. Functions should
35/// >   disallow salt values that are too small for security (4 bytes should be
36/// >   viewed as an absolute minimum).
37///
38/// # Recommended length
39/// The recommended default length for a salt string is **16-bytes** (128-bits).
40///
41/// See [`Salt::RECOMMENDED_LENGTH`] for more information.
42///
43/// # Constraints
44/// Salt strings are constrained to the following set of characters per the
45/// PHC spec:
46///
47/// > The salt consists in a sequence of characters in: `[a-zA-Z0-9/+.-]`
48/// > (lowercase letters, uppercase letters, digits, `/`, `+`, `.` and `-`).
49///
50/// Additionally, the following length restrictions are enforced based on the
51/// guidelines from the spec:
52///
53/// - Minimum length: **8**-bytes
54/// - Maximum length: **48**-bytes
55///
56/// A maximum length is enforced based on the above recommendation for
57/// supporting stack-allocated buffers (which this library uses), and the
58/// specific determination of 48-bytes is taken as a best practice from the
59/// [Argon2 Encoding][3] specification in the same document:
60///
61/// > The length in bytes of the salt is between 8 and 48 bytes<sup>†</sup>, thus
62/// > yielding a length in characters between 11 and 64 characters (and that
63/// > length is never equal to 1 modulo 4). The default byte length of the salt
64/// > is 16 bytes (22 characters in B64 encoding). An encoded UUID, or a
65/// > sequence of 16 bytes produced with a cryptographically strong PRNG, are
66/// > appropriate salt values.
67/// >
68/// > <sup>†</sup>The Argon2 specification states that the salt can be much longer, up
69/// > to 2^32-1 bytes, but this makes little sense for password hashing.
70/// > Specifying a relatively small maximum length allows for parsing with a
71/// > stack allocated buffer.
72///
73/// Based on this guidance, this type enforces an upper bound of 48-bytes
74/// as a reasonable maximum, and recommends using 16-bytes.
75///
76/// [1]: https://en.wikipedia.org/wiki/Rainbow_table
77/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
78/// [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
79#[derive(Copy, Clone, Eq, PartialEq)]
80pub struct Salt {
81    /// Length of the salt in bytes.
82    pub(super) length: u8,
83
84    /// Byte array containing an ASCII-encoded string.
85    pub(super) bytes: [u8; Self::MAX_LENGTH],
86}
87
88#[allow(clippy::len_without_is_empty)]
89impl Salt {
90    /// Minimum length of a [`Salt`] (after "B64" decoding): 8-bytes.
91    pub const MIN_LENGTH: usize = 8;
92
93    /// Maximum length of a [`Salt`] (after "B64" decoding): 48-bytes.
94    ///
95    /// See type-level documentation about [`Salt`] for more information.
96    pub const MAX_LENGTH: usize = 48;
97
98    /// Recommended length of a salt: 16-bytes.
99    ///
100    /// This recommendation comes from the [PHC string format specification]:
101    ///
102    /// > The role of salts is to achieve uniqueness. A *random* salt is fine
103    /// > for that as long as its length is sufficient; a 16-byte salt would
104    /// > work well (by definition, UUID are very good salts, and they encode
105    /// > over exactly 16 bytes). 16 bytes encode as 22 characters in B64.
106    ///
107    /// [PHC string format specification]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
108    pub const RECOMMENDED_LENGTH: usize = 16;
109
110    /// Generate a random [`Salt`] with the `RECOMMENDED_LENGTH`..
111    #[cfg(feature = "getrandom")]
112    pub fn generate() -> Self {
113        let mut bytes = [0u8; Self::RECOMMENDED_LENGTH];
114        getrandom::fill(&mut bytes).expect("RNG failure");
115        Self::new(&bytes).expect(INVARIANT_VIOLATED_MSG)
116    }
117
118    /// Generate a random [`Salt`] from the given [`CryptoRng`].
119    #[cfg(feature = "rand_core")]
120    pub fn from_rng<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
121        let Ok(out) = Self::try_from_rng(rng);
122        out
123    }
124
125    /// Generate a random [`Salt`] from the given [`TryCryptoRng`].
126    #[cfg(feature = "rand_core")]
127    pub fn try_from_rng<R: TryCryptoRng + ?Sized>(
128        rng: &mut R,
129    ) -> core::result::Result<Self, R::Error> {
130        let mut bytes = [0u8; Self::RECOMMENDED_LENGTH];
131        rng.try_fill_bytes(&mut bytes)?;
132        Ok(Self::new(&bytes).expect(INVARIANT_VIOLATED_MSG))
133    }
134
135    /// Create a new [`Salt`] from the given byte slice.
136    pub fn new(slice: &[u8]) -> Result<Self> {
137        if slice.len() < Self::MIN_LENGTH {
138            return Err(Error::SaltTooShort);
139        }
140
141        let mut bytes = [0; Self::MAX_LENGTH];
142        bytes
143            .get_mut(..slice.len())
144            .ok_or(Error::SaltTooLong)?
145            .copy_from_slice(slice);
146
147        debug_assert!(slice.len() >= Self::MIN_LENGTH);
148        debug_assert!(slice.len() <= Self::MAX_LENGTH);
149
150        Ok(Self {
151            bytes,
152            length: slice.len() as u8,
153        })
154    }
155
156    /// Create a [`Salt`] from the given B64-encoded input string, validating
157    /// [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions.
158    pub fn from_b64(b64: &str) -> Result<Self> {
159        if b64.len() < SaltString::MIN_LENGTH {
160            return Err(Error::SaltTooShort);
161        }
162
163        if b64.len() > SaltString::MAX_LENGTH {
164            return Err(Error::SaltTooLong);
165        }
166
167        let mut bytes = [0; Self::MAX_LENGTH];
168        let length = B64::decode(b64, &mut bytes)?.len();
169        debug_assert!(length <= Self::MAX_LENGTH);
170
171        Ok(Self {
172            bytes,
173            length: length as u8,
174        })
175    }
176
177    /// Encode this [`Salt`] as a "B64" [`SaltString`].
178    pub fn to_salt_string(&self) -> SaltString {
179        self.into()
180    }
181}
182
183impl AsRef<[u8]> for Salt {
184    fn as_ref(&self) -> &[u8] {
185        &self.bytes[..(self.length as usize)]
186    }
187}
188
189impl Deref for Salt {
190    type Target = [u8];
191
192    fn deref(&self) -> &[u8] {
193        self.as_ref()
194    }
195}
196
197impl FromStr for Salt {
198    type Err = Error;
199
200    fn from_str(b64: &str) -> Result<Self> {
201        Self::from_b64(b64)
202    }
203}
204
205impl TryFrom<&[u8]> for Salt {
206    type Error = Error;
207
208    fn try_from(slice: &[u8]) -> Result<Self> {
209        Self::new(slice)
210    }
211}
212
213impl TryFrom<&str> for Salt {
214    type Error = Error;
215
216    fn try_from(b64: &str) -> Result<Self> {
217        Self::from_b64(b64)
218    }
219}
220
221impl fmt::Display for Salt {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        self.to_salt_string().fmt(f)
224    }
225}
226
227impl fmt::Debug for Salt {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_tuple("Salt").field(&self.as_ref()).finish()
230    }
231}
232
233/// "B64"-encoded [`Salt`] stored as a stack-allocated string.
234///
235/// This is what is ultimately encoded into the password hash string.
236///
237/// Can be infallibly converted to/from [`Salt`] using the [`From`] trait.
238#[derive(Clone, Eq)]
239pub struct SaltString(StringBuf<{ SaltString::MAX_LENGTH }>);
240
241#[allow(clippy::len_without_is_empty)]
242impl SaltString {
243    /// Minimum length of "B64"-encoded [`SaltString`] string: 11-bytes (4-bytes encoded as "B64")
244    pub const MIN_LENGTH: usize = 11;
245
246    /// Maximum length of a "B64"-encoded [`SaltString`]: 64-bytes (48-bytes encoded as "B64")
247    ///
248    /// See type-level documentation about [`Salt`] for more information.
249    pub const MAX_LENGTH: usize = 64;
250
251    /// Generate a random B64-encoded [`SaltString`].
252    #[cfg(feature = "getrandom")]
253    pub fn generate() -> Self {
254        Salt::generate().into()
255    }
256
257    /// Generate a random B64-encoded [`SaltString`] from [`CryptoRng`].
258    #[cfg(feature = "rand_core")]
259    pub fn from_rng<R: CryptoRng + ?Sized>(rng: &mut R) -> Self {
260        let Ok(out) = Self::try_from_rng(rng);
261        out
262    }
263
264    /// Generate a random B64-encoded [`SaltString`] from [`TryCryptoRng`].
265    #[cfg(feature = "rand_core")]
266    pub fn try_from_rng<R: TryCryptoRng + ?Sized>(
267        rng: &mut R,
268    ) -> core::result::Result<Self, R::Error> {
269        Ok(Salt::try_from_rng(rng)?.to_salt_string())
270    }
271
272    /// Create a new [`SaltString`] from the given B64-encoded input string,
273    /// validating [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions.
274    pub fn from_b64(s: &str) -> Result<Self> {
275        // Assert `s` parses successfully as a `Salt`
276        Salt::from_b64(s)?;
277        Ok(Self(s.parse()?))
278    }
279
280    /// Decode this "B64" string, returning a [`Salt`] containing the decoded bytes.
281    pub fn to_salt(&self) -> Salt {
282        self.into()
283    }
284}
285
286impl AsRef<str> for SaltString {
287    fn as_ref(&self) -> &str {
288        &self.0
289    }
290}
291
292impl Deref for SaltString {
293    type Target = str;
294
295    fn deref(&self) -> &str {
296        &self.0
297    }
298}
299
300impl From<Salt> for SaltString {
301    fn from(salt: Salt) -> Self {
302        SaltString::from(&salt)
303    }
304}
305
306impl From<&Salt> for SaltString {
307    fn from(salt: &Salt) -> Self {
308        let mut buf = [0; SaltString::MAX_LENGTH];
309        let b64 = B64::encode(salt, &mut buf).expect(INVARIANT_VIOLATED_MSG);
310        SaltString(b64.parse().expect(INVARIANT_VIOLATED_MSG))
311    }
312}
313
314impl From<SaltString> for Salt {
315    fn from(salt: SaltString) -> Self {
316        Salt::from(&salt)
317    }
318}
319
320impl From<&SaltString> for Salt {
321    fn from(salt: &SaltString) -> Self {
322        Salt::from_b64(salt.as_ref()).expect(INVARIANT_VIOLATED_MSG)
323    }
324}
325
326impl FromStr for SaltString {
327    type Err = Error;
328
329    fn from_str(s: &str) -> Result<Self> {
330        Self::from_b64(s)
331    }
332}
333
334impl PartialEq for SaltString {
335    fn eq(&self, other: &Self) -> bool {
336        // Ensure comparisons always honor the initialized portion of the buffer
337        self.as_ref().eq(other.as_ref())
338    }
339}
340
341impl fmt::Display for SaltString {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        f.write_str(self.as_ref())
344    }
345}
346
347impl fmt::Debug for SaltString {
348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349        write!(f, "SaltString({:?})", self.as_ref())
350    }
351}
352
353#[cfg(test)]
354#[allow(clippy::unwrap_used)]
355mod tests {
356    use super::{Error, Salt};
357
358    #[test]
359    fn new_with_valid_min_length_input() {
360        let s = "abcdabcdabc";
361        let salt = Salt::from_b64(s).unwrap();
362        assert_eq!(
363            salt.as_ref(),
364            &[0x69, 0xb7, 0x1d, 0x69, 0xb7, 0x1d, 0x69, 0xb7]
365        );
366    }
367
368    #[test]
369    fn new_with_valid_max_length_input() {
370        let s = "012345678911234567892123456789312345678941234567";
371        let salt = Salt::from_b64(s).unwrap();
372        assert_eq!(
373            salt.as_ref(),
374            &[
375                0xd3, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb, 0xf3, 0xdd, 0x75, 0xdb, 0x7e, 0x39, 0xeb, 0xbf,
376                0x3d, 0xdb, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb, 0xf3, 0xdd, 0xf5, 0xdb, 0x7e, 0x39, 0xeb,
377                0xbf, 0x3d, 0xe3, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb
378            ]
379        );
380    }
381
382    #[test]
383    fn reject_new_too_short() {
384        for &too_short in &["", "a", "ab", "abc"] {
385            let err = Salt::from_b64(too_short).err().unwrap();
386            assert_eq!(err, Error::SaltTooShort);
387        }
388    }
389
390    #[test]
391    fn reject_new_too_long() {
392        let s = "01234567891123456789212345678931234567894123456785234567896234567";
393        let err = Salt::from_b64(s).err().unwrap();
394        assert_eq!(err, Error::SaltTooLong);
395    }
396
397    #[test]
398    fn reject_new_invalid_char() {
399        let s = "01234_abcde";
400        let err = Salt::from_b64(s).err().unwrap();
401        assert_eq!(err, Error::Base64(base64ct::Error::InvalidEncoding));
402    }
403}