Coverage Report

Created: 2025-02-22 06:51

/rust/registry/src/index.crates.io-6f17d22bba15001f/password-hash-0.6.0-rc.0/src/output.rs
Line
Count
Source (jump to first uncovered line)
1
//! Outputs from password hashing functions.
2
3
use crate::{Encoding, Error, Result};
4
use core::{cmp::Ordering, fmt, str::FromStr};
5
use subtle::{Choice, ConstantTimeEq};
6
7
/// Output from password hashing functions, i.e. the "hash" or "digest"
8
/// as raw bytes.
9
///
10
/// The [`Output`] type implements the RECOMMENDED best practices described in
11
/// the [PHC string format specification][1], namely:
12
///
13
/// > The hash output, for a verification, must be long enough to make preimage
14
/// > attacks at least as hard as password guessing. To promote wide acceptance,
15
/// > a default output size of 256 bits (32 bytes, encoded as 43 characters) is
16
/// > recommended. Function implementations SHOULD NOT allow outputs of less
17
/// > than 80 bits to be used for password verification.
18
///
19
/// # Recommended length
20
/// Per the description above, the recommended default length for an [`Output`]
21
/// of a password hashing function is **32-bytes** (256-bits).
22
///
23
/// # Constraints
24
/// The above guidelines are interpreted into the following constraints:
25
///
26
/// - Minimum length: **10**-bytes (80-bits)
27
/// - Maximum length: **64**-bytes (512-bits)
28
///
29
/// The specific recommendation of a 64-byte maximum length is taken as a best
30
/// practice from the hash output guidelines for [Argon2 Encoding][2] given in
31
/// the same document:
32
///
33
/// > The hash output...length shall be between 12 and 64 bytes (16 and 86
34
/// > characters, respectively). The default output length is 32 bytes
35
/// > (43 characters).
36
///
37
/// Based on this guidance, this type enforces an upper bound of 64-bytes
38
/// as a reasonable maximum, and recommends using 32-bytes.
39
///
40
/// # Constant-time comparisons
41
/// The [`Output`] type impls the [`ConstantTimeEq`] trait from the [`subtle`]
42
/// crate and uses it to perform constant-time comparisons.
43
///
44
/// Additionally the [`PartialEq`] and [`Eq`] trait impls for [`Output`] use
45
/// [`ConstantTimeEq`] when performing comparisons.
46
///
47
/// ## Attacks on non-constant-time password hash comparisons
48
/// Comparing password hashes in constant-time is known to mitigate at least
49
/// one [poorly understood attack][3] involving an adversary with the following
50
/// knowledge/capabilities:
51
///
52
/// - full knowledge of what password hashing algorithm is being used
53
///   including any relevant configurable parameters
54
/// - knowledge of the salt for a particular victim
55
/// - ability to accurately measure a timing side-channel on comparisons
56
///   of the password hash over the network
57
///
58
/// An attacker with the above is able to perform an offline computation of
59
/// the hash for any chosen password in such a way that it will match the
60
/// hash computed by the server.
61
///
62
/// As noted above, they also measure timing variability in the server's
63
/// comparison of the hash it computes for a given password and a target hash
64
/// the attacker is trying to learn.
65
///
66
/// When the attacker observes a hash comparison that takes longer than their
67
/// previous attempts, they learn that they guessed another byte in the
68
/// password hash correctly. They can leverage repeated measurements and
69
/// observations with different candidate passwords to learn the password
70
/// hash a byte-at-a-time in a manner similar to other such timing side-channel
71
/// attacks.
72
///
73
/// The attack may seem somewhat counterintuitive since learning prefixes of a
74
/// password hash does not reveal any additional information about the password
75
/// itself. However, the above can be combined with an offline dictionary
76
/// attack where the attacker is able to determine candidate passwords to send
77
/// to the server by performing a brute force search offline and selecting
78
/// candidate passwords whose hashes match the portion of the prefix they have
79
/// learned so far.
80
///
81
/// As the attacker learns a longer and longer prefix of the password hash,
82
/// they are able to more effectively eliminate candidate passwords offline as
83
/// part of a dictionary attack, until they eventually guess the correct
84
/// password or exhaust their set of candidate passwords.
85
///
86
/// ## Mitigations
87
/// While we have taken care to ensure password hashes are compared in constant
88
/// time, we would also suggest preventing such attacks by using randomly
89
/// generated salts and keeping those salts secret.
90
///
91
/// The [`SaltString::generate`][`crate::SaltString::generate`] function can be
92
/// used to generate random high-entropy salt values.
93
///
94
/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties
95
/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding
96
/// [3]: https://web.archive.org/web/20130208100210/http://security-assessment.com/files/documents/presentations/TimingAttackPresentation2012.pdf
97
#[derive(Copy, Clone, Eq)]
98
pub struct Output {
99
    /// Byte array containing a password hashing function output.
100
    bytes: [u8; Self::MAX_LENGTH],
101
102
    /// Length of the password hashing function output in bytes.
103
    length: u8,
104
105
    /// Encoding which output should be serialized with.
106
    encoding: Encoding,
107
}
108
109
#[allow(clippy::len_without_is_empty)]
110
impl Output {
111
    /// Minimum length of a [`Output`] string: 10-bytes.
112
    pub const MIN_LENGTH: usize = 10;
113
114
    /// Maximum length of [`Output`] string: 64-bytes.
115
    ///
116
    /// See type-level documentation about [`Output`] for more information.
117
    pub const MAX_LENGTH: usize = 64;
118
119
    /// Maximum length of [`Output`] when encoded as B64 string: 86-bytes
120
    /// (i.e. 86 ASCII characters)
121
    pub const B64_MAX_LENGTH: usize = ((Self::MAX_LENGTH * 4) / 3) + 1;
122
123
    /// Create a [`Output`] from the given byte slice, validating it according
124
    /// to [`Output::MIN_LENGTH`] and [`Output::MAX_LENGTH`] restrictions.
125
529
    pub fn new(input: &[u8]) -> Result<Self> {
126
529
        Self::init_with(input.len(), |bytes| {
127
529
            bytes.copy_from_slice(input);
128
529
            Ok(())
129
529
        })
130
529
    }
131
132
    /// Create a [`Output`] from the given byte slice and [`Encoding`],
133
    /// validating it according to [`Output::MIN_LENGTH`] and
134
    /// [`Output::MAX_LENGTH`] restrictions.
135
529
    pub fn new_with_encoding(input: &[u8], encoding: Encoding) -> Result<Self> {
136
529
        let mut result = Self::new(input)?;
137
529
        result.encoding = encoding;
138
529
        Ok(result)
139
529
    }
140
141
    /// Initialize an [`Output`] using the provided method, which is given
142
    /// a mutable byte slice into which it should write the output.
143
    ///
144
    /// The `output_size` (in bytes) must be known in advance, as well as at
145
    /// least [`Output::MIN_LENGTH`] bytes and at most [`Output::MAX_LENGTH`]
146
    /// bytes.
147
1.58k
    pub fn init_with<F>(output_size: usize, f: F) -> Result<Self>
148
1.58k
    where
149
1.58k
        F: FnOnce(&mut [u8]) -> Result<()>,
150
1.58k
    {
151
1.58k
        if output_size < Self::MIN_LENGTH {
152
0
            return Err(Error::OutputSize {
153
0
                provided: Ordering::Less,
154
0
                expected: Self::MIN_LENGTH,
155
0
            });
156
1.58k
        }
157
1.58k
158
1.58k
        if output_size > Self::MAX_LENGTH {
159
0
            return Err(Error::OutputSize {
160
0
                provided: Ordering::Greater,
161
0
                expected: Self::MAX_LENGTH,
162
0
            });
163
1.58k
        }
164
1.58k
165
1.58k
        let mut bytes = [0u8; Self::MAX_LENGTH];
166
1.58k
        f(&mut bytes[..output_size])?;
167
168
1.58k
        Ok(Self {
169
1.58k
            bytes,
170
1.58k
            length: output_size as u8,
171
1.58k
            encoding: Encoding::default(),
172
1.58k
        })
173
1.58k
    }
<password_hash::output::Output>::init_with::<<scrypt::simple::Scrypt as password_hash::traits::PasswordHasher>::hash_password_customized<password_hash::salt::Salt>::{closure#0}>
Line
Count
Source
147
529
    pub fn init_with<F>(output_size: usize, f: F) -> Result<Self>
148
529
    where
149
529
        F: FnOnce(&mut [u8]) -> Result<()>,
150
529
    {
151
529
        if output_size < Self::MIN_LENGTH {
152
0
            return Err(Error::OutputSize {
153
0
                provided: Ordering::Less,
154
0
                expected: Self::MIN_LENGTH,
155
0
            });
156
529
        }
157
529
158
529
        if output_size > Self::MAX_LENGTH {
159
0
            return Err(Error::OutputSize {
160
0
                provided: Ordering::Greater,
161
0
                expected: Self::MAX_LENGTH,
162
0
            });
163
529
        }
164
529
165
529
        let mut bytes = [0u8; Self::MAX_LENGTH];
166
529
        f(&mut bytes[..output_size])?;
167
168
529
        Ok(Self {
169
529
            bytes,
170
529
            length: output_size as u8,
171
529
            encoding: Encoding::default(),
172
529
        })
173
529
    }
<password_hash::output::Output>::init_with::<<scrypt::simple::Scrypt as password_hash::traits::PasswordHasher>::hash_password_customized<&password_hash::salt::SaltString>::{closure#0}>
Line
Count
Source
147
529
    pub fn init_with<F>(output_size: usize, f: F) -> Result<Self>
148
529
    where
149
529
        F: FnOnce(&mut [u8]) -> Result<()>,
150
529
    {
151
529
        if output_size < Self::MIN_LENGTH {
152
0
            return Err(Error::OutputSize {
153
0
                provided: Ordering::Less,
154
0
                expected: Self::MIN_LENGTH,
155
0
            });
156
529
        }
157
529
158
529
        if output_size > Self::MAX_LENGTH {
159
0
            return Err(Error::OutputSize {
160
0
                provided: Ordering::Greater,
161
0
                expected: Self::MAX_LENGTH,
162
0
            });
163
529
        }
164
529
165
529
        let mut bytes = [0u8; Self::MAX_LENGTH];
166
529
        f(&mut bytes[..output_size])?;
167
168
529
        Ok(Self {
169
529
            bytes,
170
529
            length: output_size as u8,
171
529
            encoding: Encoding::default(),
172
529
        })
173
529
    }
<password_hash::output::Output>::init_with::<<password_hash::output::Output>::new::{closure#0}>
Line
Count
Source
147
529
    pub fn init_with<F>(output_size: usize, f: F) -> Result<Self>
148
529
    where
149
529
        F: FnOnce(&mut [u8]) -> Result<()>,
150
529
    {
151
529
        if output_size < Self::MIN_LENGTH {
152
0
            return Err(Error::OutputSize {
153
0
                provided: Ordering::Less,
154
0
                expected: Self::MIN_LENGTH,
155
0
            });
156
529
        }
157
529
158
529
        if output_size > Self::MAX_LENGTH {
159
0
            return Err(Error::OutputSize {
160
0
                provided: Ordering::Greater,
161
0
                expected: Self::MAX_LENGTH,
162
0
            });
163
529
        }
164
529
165
529
        let mut bytes = [0u8; Self::MAX_LENGTH];
166
529
        f(&mut bytes[..output_size])?;
167
168
529
        Ok(Self {
169
529
            bytes,
170
529
            length: output_size as u8,
171
529
            encoding: Encoding::default(),
172
529
        })
173
529
    }
174
175
    /// Borrow the output value as a byte slice.
176
1.58k
    pub fn as_bytes(&self) -> &[u8] {
177
1.58k
        &self.bytes[..self.len()]
178
1.58k
    }
179
180
    /// Get the [`Encoding`] that this [`Output`] is serialized with.
181
0
    pub fn encoding(&self) -> Encoding {
182
0
        self.encoding
183
0
    }
184
185
    /// Get the length of the output value as a byte slice.
186
2.11k
    pub fn len(&self) -> usize {
187
2.11k
        usize::from(self.length)
188
2.11k
    }
189
190
    /// Parse B64-encoded [`Output`], i.e. using the PHC string
191
    /// specification's restricted interpretation of Base64.
192
0
    pub fn b64_decode(input: &str) -> Result<Self> {
193
0
        Self::decode(input, Encoding::B64)
194
0
    }
195
196
    /// Write B64-encoded [`Output`] to the provided buffer, returning
197
    /// a sub-slice containing the encoded data.
198
    ///
199
    /// Returns an error if the buffer is too short to contain the output.
200
0
    pub fn b64_encode<'a>(&self, out: &'a mut [u8]) -> Result<&'a str> {
201
0
        self.encode(out, Encoding::B64)
202
0
    }
203
204
    /// Decode the given input string using the specified [`Encoding`].
205
529
    pub fn decode(input: &str, encoding: Encoding) -> Result<Self> {
206
529
        let mut bytes = [0u8; Self::MAX_LENGTH];
207
529
        encoding
208
529
            .decode(input, &mut bytes)
209
529
            .map_err(Into::into)
210
529
            .and_then(|decoded| Self::new_with_encoding(decoded, encoding))
211
529
    }
212
213
    /// Encode this [`Output`] using the specified [`Encoding`].
214
529
    pub fn encode<'a>(&self, out: &'a mut [u8], encoding: Encoding) -> Result<&'a str> {
215
529
        Ok(encoding.encode(self.as_ref(), out)?)
216
529
    }
217
218
    /// Get the length of this [`Output`] when encoded as B64.
219
0
    pub fn b64_len(&self) -> usize {
220
0
        Encoding::B64.encoded_len(self.as_ref())
221
0
    }
222
}
223
224
impl AsRef<[u8]> for Output {
225
1.58k
    fn as_ref(&self) -> &[u8] {
226
1.58k
        self.as_bytes()
227
1.58k
    }
228
}
229
230
impl ConstantTimeEq for Output {
231
529
    fn ct_eq(&self, other: &Self) -> Choice {
232
529
        self.as_ref().ct_eq(other.as_ref())
233
529
    }
234
}
235
236
impl FromStr for Output {
237
    type Err = Error;
238
239
0
    fn from_str(s: &str) -> Result<Self> {
240
0
        Self::b64_decode(s)
241
0
    }
242
}
243
244
impl PartialEq for Output {
245
529
    fn eq(&self, other: &Self) -> bool {
246
529
        self.ct_eq(other).into()
247
529
    }
248
}
249
250
impl TryFrom<&[u8]> for Output {
251
    type Error = Error;
252
253
0
    fn try_from(input: &[u8]) -> Result<Output> {
254
0
        Self::new(input)
255
0
    }
256
}
257
258
impl fmt::Display for Output {
259
529
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260
529
        let mut buffer = [0u8; Self::B64_MAX_LENGTH];
261
529
        self.encode(&mut buffer, self.encoding)
262
529
            .map_err(|_| fmt::Error)
263
529
            .and_then(|encoded| f.write_str(encoded))
264
529
    }
265
}
266
267
impl fmt::Debug for Output {
268
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269
0
        write!(f, "Output(\"{}\")", self)
270
0
    }
271
}
272
273
#[cfg(test)]
274
mod tests {
275
    use super::{Error, Ordering, Output};
276
277
    #[test]
278
    fn new_with_valid_min_length_input() {
279
        let bytes = [10u8; 10];
280
        let output = Output::new(&bytes).unwrap();
281
        assert_eq!(output.as_ref(), &bytes);
282
    }
283
284
    #[test]
285
    fn new_with_valid_max_length_input() {
286
        let bytes = [64u8; 64];
287
        let output = Output::new(&bytes).unwrap();
288
        assert_eq!(output.as_ref(), &bytes);
289
    }
290
291
    #[test]
292
    fn reject_new_too_short() {
293
        let bytes = [9u8; 9];
294
        let err = Output::new(&bytes).err().unwrap();
295
        assert_eq!(
296
            err,
297
            Error::OutputSize {
298
                provided: Ordering::Less,
299
                expected: Output::MIN_LENGTH
300
            }
301
        );
302
    }
303
304
    #[test]
305
    fn reject_new_too_long() {
306
        let bytes = [65u8; 65];
307
        let err = Output::new(&bytes).err().unwrap();
308
        assert_eq!(
309
            err,
310
            Error::OutputSize {
311
                provided: Ordering::Greater,
312
                expected: Output::MAX_LENGTH
313
            }
314
        );
315
    }
316
317
    #[test]
318
    fn partialeq_true() {
319
        let a = Output::new(&[1u8; 32]).unwrap();
320
        let b = Output::new(&[1u8; 32]).unwrap();
321
        assert_eq!(a, b);
322
    }
323
324
    #[test]
325
    fn partialeq_false() {
326
        let a = Output::new(&[1u8; 32]).unwrap();
327
        let b = Output::new(&[2u8; 32]).unwrap();
328
        assert_ne!(a, b);
329
    }
330
}