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