Coverage Report

Created: 2026-06-10 06:36

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/salt.rs
Line
Count
Source
1
//! Salt string support.
2
3
use crate::{Encoding, Error, Result, Value};
4
use core::{fmt, str};
5
6
use crate::errors::InvalidValue;
7
#[cfg(feature = "rand_core")]
8
use rand_core::{CryptoRng, RngCore};
9
10
/// Error message used with `expect` for when internal invariants are violated
11
/// (i.e. the contents of a [`Salt`] should always be valid)
12
const INVARIANT_VIOLATED_MSG: &str = "salt string invariant violated";
13
14
/// Salt string.
15
///
16
/// In password hashing, a "salt" is an additional value used to
17
/// personalize/tweak the output of a password hashing function for a given
18
/// input password.
19
///
20
/// Salts help defend against attacks based on precomputed tables of hashed
21
/// passwords, i.e. "[rainbow tables][1]".
22
///
23
/// The [`Salt`] type implements the RECOMMENDED best practices for salts
24
/// described in the [PHC string format specification][2], namely:
25
///
26
/// > - Maximum lengths for salt, output and parameter values are meant to help
27
/// >   consumer implementations, in particular written in C and using
28
/// >   stack-allocated buffers. These buffers must account for the worst case,
29
/// >   i.e. the maximum defined length. Therefore, keep these lengths low.
30
/// > - The role of salts is to achieve uniqueness. A random salt is fine for
31
/// >   that as long as its length is sufficient; a 16-byte salt would work well
32
/// >   (by definition, UUID are very good salts, and they encode over exactly
33
/// >   16 bytes). 16 bytes encode as 22 characters in B64. Functions should
34
/// >   disallow salt values that are too small for security (4 bytes should be
35
/// >   viewed as an absolute minimum).
36
///
37
/// # Recommended length
38
/// The recommended default length for a salt string is **16-bytes** (128-bits).
39
///
40
/// See [`Salt::RECOMMENDED_LENGTH`] for more information.
41
///
42
/// # Constraints
43
/// Salt strings are constrained to the following set of characters per the
44
/// PHC spec:
45
///
46
/// > The salt consists in a sequence of characters in: `[a-zA-Z0-9/+.-]`
47
/// > (lowercase letters, uppercase letters, digits, /, +, . and -).
48
///
49
/// Additionally the following length restrictions are enforced based on the
50
/// guidelines from the spec:
51
///
52
/// - Minimum length: **4**-bytes
53
/// - Maximum length: **64**-bytes
54
///
55
/// A maximum length is enforced based on the above recommendation for
56
/// supporting stack-allocated buffers (which this library uses), and the
57
/// specific determination of 64-bytes is taken as a best practice from the
58
/// [Argon2 Encoding][3] specification in the same document:
59
///
60
/// > The length in bytes of the salt is between 8 and 64 bytes<sup>†</sup>, thus
61
/// > yielding a length in characters between 11 and 64 characters (and that
62
/// > length is never equal to 1 modulo 4). The default byte length of the salt
63
/// > is 16 bytes (22 characters in B64 encoding). An encoded UUID, or a
64
/// > sequence of 16 bytes produced with a cryptographically strong PRNG, are
65
/// > appropriate salt values.
66
/// >
67
/// > <sup>†</sup>The Argon2 specification states that the salt can be much longer, up
68
/// > to 2^32-1 bytes, but this makes little sense for password hashing.
69
/// > Specifying a relatively small maximum length allows for parsing with a
70
/// > stack allocated buffer.)
71
///
72
/// Based on this guidance, this type enforces an upper bound of 64-bytes
73
/// as a reasonable maximum, and recommends using 16-bytes.
74
///
75
/// [1]: https://en.wikipedia.org/wiki/Rainbow_table
76
/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
77
/// [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
78
#[derive(Copy, Clone, Eq, PartialEq)]
79
pub struct Salt<'a>(Value<'a>);
80
81
#[allow(clippy::len_without_is_empty)]
82
impl<'a> Salt<'a> {
83
    /// Minimum length of a [`Salt`] string: 4-bytes.
84
    pub const MIN_LENGTH: usize = 4;
85
86
    /// Maximum length of a [`Salt`] string: 64-bytes.
87
    ///
88
    /// See type-level documentation about [`Salt`] for more information.
89
    pub const MAX_LENGTH: usize = 64;
90
91
    /// Recommended length of a salt: 16-bytes.
92
    ///
93
    /// This recommendation comes from the [PHC string format specification]:
94
    ///
95
    /// > The role of salts is to achieve uniqueness. A *random* salt is fine
96
    /// > for that as long as its length is sufficient; a 16-byte salt would
97
    /// > work well (by definition, UUID are very good salts, and they encode
98
    /// > over exactly 16 bytes). 16 bytes encode as 22 characters in B64.
99
    ///
100
    /// [PHC string format specification]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
101
    pub const RECOMMENDED_LENGTH: usize = 16;
102
103
    /// Create a [`Salt`] from the given `str`, validating it according to
104
    /// [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] length restrictions.
105
0
    pub fn new(input: &'a str) -> Result<Self> {
106
0
        let length = input.as_bytes().len();
107
108
0
        if length < Self::MIN_LENGTH {
109
0
            return Err(Error::SaltInvalid(InvalidValue::TooShort));
110
0
        }
111
112
0
        if length > Self::MAX_LENGTH {
113
0
            return Err(Error::SaltInvalid(InvalidValue::TooLong));
114
0
        }
115
116
0
        input.try_into().map(Self).map_err(|e| match e {
117
0
            Error::ParamValueInvalid(value_err) => Error::SaltInvalid(value_err),
118
0
            err => err,
119
0
        })
120
0
    }
121
122
    /// Attempt to decode a B64-encoded [`Salt`], writing the decoded result
123
    /// into the provided buffer, and returning a slice of the buffer
124
    /// containing the decoded result on success.
125
    ///
126
    /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
127
0
    pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> {
128
0
        self.0.b64_decode(buf)
129
0
    }
130
131
    /// Borrow this value as a `str`.
132
0
    pub fn as_str(&self) -> &'a str {
133
0
        self.0.as_str()
134
0
    }
135
136
    /// Borrow this value as bytes.
137
0
    pub fn as_bytes(&self) -> &'a [u8] {
138
0
        self.as_str().as_bytes()
139
0
    }
140
141
    /// Get the length of this value in ASCII characters.
142
0
    pub fn len(&self) -> usize {
143
0
        self.as_str().len()
144
0
    }
145
}
146
147
impl<'a> AsRef<str> for Salt<'a> {
148
0
    fn as_ref(&self) -> &str {
149
0
        self.as_str()
150
0
    }
151
}
152
153
impl<'a> TryFrom<&'a str> for Salt<'a> {
154
    type Error = Error;
155
156
0
    fn try_from(input: &'a str) -> Result<Self> {
157
0
        Self::new(input)
158
0
    }
159
}
160
161
impl<'a> fmt::Display for Salt<'a> {
162
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163
0
        f.write_str(self.as_str())
164
0
    }
165
}
166
167
impl<'a> fmt::Debug for Salt<'a> {
168
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169
0
        write!(f, "Salt({:?})", self.as_str())
170
0
    }
171
}
172
173
/// Owned stack-allocated equivalent of [`Salt`].
174
#[derive(Clone, Eq)]
175
pub struct SaltString {
176
    /// Byte array containing an ASCiI-encoded string.
177
    bytes: [u8; Salt::MAX_LENGTH],
178
179
    /// Length of the string in ASCII characters (i.e. bytes).
180
    length: u8,
181
}
182
183
#[allow(clippy::len_without_is_empty)]
184
impl SaltString {
185
    /// Generate a random B64-encoded [`SaltString`].
186
    #[cfg(feature = "rand_core")]
187
    #[cfg_attr(docsrs, doc(cfg(feature = "rand_core")))]
188
0
    pub fn generate(mut rng: impl CryptoRng + RngCore) -> Self {
189
0
        let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
190
0
        rng.fill_bytes(&mut bytes);
191
0
        Self::b64_encode(&bytes).expect(INVARIANT_VIOLATED_MSG)
192
0
    }
193
194
    /// Create a new [`SaltString`].
195
0
    pub fn new(s: &str) -> Result<Self> {
196
        // Assert `s` parses successfully as a `Salt`
197
0
        Salt::new(s)?;
198
199
0
        let length = s.as_bytes().len();
200
201
0
        if length < Salt::MAX_LENGTH {
202
0
            let mut bytes = [0u8; Salt::MAX_LENGTH];
203
0
            bytes[..length].copy_from_slice(s.as_bytes());
204
0
            Ok(SaltString {
205
0
                bytes,
206
0
                length: length as u8,
207
0
            })
208
        } else {
209
0
            Err(Error::SaltInvalid(InvalidValue::TooLong))
210
        }
211
0
    }
212
213
    /// Encode the given byte slice as B64 into a new [`SaltString`].
214
    ///
215
    /// Returns `None` if the slice is too long.
216
0
    pub fn b64_encode(input: &[u8]) -> Result<Self> {
217
0
        let mut bytes = [0u8; Salt::MAX_LENGTH];
218
0
        let length = Encoding::B64.encode(input, &mut bytes)?.len() as u8;
219
0
        Ok(Self { bytes, length })
220
0
    }
221
222
    /// Decode this [`SaltString`] from B64 into the provided output buffer.
223
0
    pub fn b64_decode<'a>(&self, buf: &'a mut [u8]) -> Result<&'a [u8]> {
224
0
        self.as_salt().b64_decode(buf)
225
0
    }
226
227
    /// Borrow the contents of a [`SaltString`] as a [`Salt`].
228
0
    pub fn as_salt(&self) -> Salt<'_> {
229
0
        Salt::new(self.as_str()).expect(INVARIANT_VIOLATED_MSG)
230
0
    }
231
232
    /// Borrow the contents of a [`SaltString`] as a `str`.
233
0
    pub fn as_str(&self) -> &str {
234
0
        str::from_utf8(&self.bytes[..(self.length as usize)]).expect(INVARIANT_VIOLATED_MSG)
235
0
    }
236
237
    /// Borrow this value as bytes.
238
0
    pub fn as_bytes(&self) -> &[u8] {
239
0
        self.as_str().as_bytes()
240
0
    }
241
242
    /// Get the length of this value in ASCII characters.
243
0
    pub fn len(&self) -> usize {
244
0
        self.as_str().len()
245
0
    }
246
}
247
248
impl AsRef<str> for SaltString {
249
0
    fn as_ref(&self) -> &str {
250
0
        self.as_str()
251
0
    }
252
}
253
254
impl PartialEq for SaltString {
255
0
    fn eq(&self, other: &Self) -> bool {
256
        // Ensure comparisons always honor the initialized portion of the buffer
257
0
        self.as_ref().eq(other.as_ref())
258
0
    }
259
}
260
261
impl<'a> From<&'a SaltString> for Salt<'a> {
262
0
    fn from(salt_string: &'a SaltString) -> Salt<'a> {
263
0
        salt_string.as_salt()
264
0
    }
265
}
266
267
impl fmt::Display for SaltString {
268
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269
0
        f.write_str(self.as_str())
270
0
    }
271
}
272
273
impl fmt::Debug for SaltString {
274
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275
0
        write!(f, "SaltString({:?})", self.as_str())
276
0
    }
277
}
278
279
#[cfg(test)]
280
mod tests {
281
    use super::{Error, Salt};
282
    use crate::errors::InvalidValue;
283
284
    #[test]
285
    fn new_with_valid_min_length_input() {
286
        let s = "abcd";
287
        let salt = Salt::new(s).unwrap();
288
        assert_eq!(salt.as_ref(), s);
289
    }
290
291
    #[test]
292
    fn new_with_valid_max_length_input() {
293
        let s = "012345678911234567892123456789312345678941234567";
294
        let salt = Salt::new(s).unwrap();
295
        assert_eq!(salt.as_ref(), s);
296
    }
297
298
    #[test]
299
    fn reject_new_too_short() {
300
        for &too_short in &["", "a", "ab", "abc"] {
301
            let err = Salt::new(too_short).err().unwrap();
302
            assert_eq!(err, Error::SaltInvalid(InvalidValue::TooShort));
303
        }
304
    }
305
306
    #[test]
307
    fn reject_new_too_long() {
308
        let s = "01234567891123456789212345678931234567894123456785234567896234567";
309
        let err = Salt::new(s).err().unwrap();
310
        assert_eq!(err, Error::SaltInvalid(InvalidValue::TooLong));
311
    }
312
313
    #[test]
314
    fn reject_new_invalid_char() {
315
        let s = "01234_abcd";
316
        let err = Salt::new(s).err().unwrap();
317
        assert_eq!(err, Error::SaltInvalid(InvalidValue::InvalidChar('_')));
318
    }
319
}