Skip to content

Commit 800e0ff

Browse files
Simplify error kinds: remove dead BcryptError::Io branch, unify invalid hash branches (#93)
* Delete unused `BcrypError::Io` variant While looking at how best to handle this error, I found that it's actually not used. * Document when the errors can occur * return stringy errors for different ways hashes can be malformed
1 parent 920e169 commit 800e0ff

2 files changed

Lines changed: 27 additions & 45 deletions

File tree

src/errors.rs

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
#[cfg(any(feature = "alloc", feature = "std"))]
2-
use alloc::string::String;
31
use core::fmt;
42

53
#[cfg(feature = "std")]
64
use std::error;
7-
#[cfg(feature = "std")]
8-
use std::io;
95

106
/// Library generic result type.
117
pub type BcryptResult<T> = Result<T, BcryptError>;
@@ -14,22 +10,19 @@ pub type BcryptResult<T> = Result<T, BcryptError>;
1410
/// All the errors we can encounter while hashing/verifying
1511
/// passwords
1612
pub enum BcryptError {
17-
#[cfg(feature = "std")]
18-
Io(io::Error),
13+
/// Raised when the cost value is outside of the allowed 4-31 range.
14+
///
15+
/// Cost is provided as an argument to hashing functions, and extracted from the hash in
16+
/// verification functions.
1917
CostNotAllowed(u32),
18+
/// Raised when verifying against an incorrectly formatted hash.
2019
#[cfg(any(feature = "alloc", feature = "std"))]
21-
InvalidCost(String),
22-
#[cfg(any(feature = "alloc", feature = "std"))]
23-
InvalidPrefix(String),
24-
#[cfg(any(feature = "alloc", feature = "std"))]
25-
InvalidHash(String),
26-
InvalidSaltLen(usize),
27-
InvalidBase64(base64::DecodeError),
20+
InvalidHash(&'static str),
21+
/// Raised when an error occurs when generating a salt value.
2822
#[cfg(any(feature = "alloc", feature = "std"))]
2923
Rand(getrandom::Error),
30-
/// Return this error if the input contains more than 72 bytes. This variant contains the
31-
/// length of the input in bytes.
32-
/// Only returned when calling `non_truncating_*` functions
24+
/// Raised when the input to a `non_truncating_*` function contains more than 72 bytes.
25+
/// This variant contains the length of the input in bytes.
3326
Truncation(usize),
3427
}
3528

@@ -43,19 +36,12 @@ macro_rules! impl_from_error {
4336
};
4437
}
4538

46-
impl_from_error!(base64::DecodeError, BcryptError::InvalidBase64);
47-
#[cfg(feature = "std")]
48-
impl_from_error!(io::Error, BcryptError::Io);
4939
#[cfg(any(feature = "alloc", feature = "std"))]
5040
impl_from_error!(getrandom::Error, BcryptError::Rand);
5141

5242
impl fmt::Display for BcryptError {
5343
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5444
match *self {
55-
#[cfg(feature = "std")]
56-
BcryptError::Io(ref err) => write!(f, "IO error: {}", err),
57-
#[cfg(any(feature = "alloc", feature = "std"))]
58-
BcryptError::InvalidCost(ref cost) => write!(f, "Invalid Cost: {}", cost),
5945
BcryptError::CostNotAllowed(ref cost) => write!(
6046
f,
6147
"Cost needs to be between {} and {}, got {}",
@@ -64,13 +50,7 @@ impl fmt::Display for BcryptError {
6450
cost
6551
),
6652
#[cfg(any(feature = "alloc", feature = "std"))]
67-
BcryptError::InvalidPrefix(ref prefix) => write!(f, "Invalid Prefix: {}", prefix),
68-
#[cfg(any(feature = "alloc", feature = "std"))]
69-
BcryptError::InvalidHash(ref hash) => write!(f, "Invalid hash: {}", hash),
70-
BcryptError::InvalidBase64(ref err) => write!(f, "Base64 error: {}", err),
71-
BcryptError::InvalidSaltLen(len) => {
72-
write!(f, "Invalid salt len: expected 16, received {}", len)
73-
}
53+
BcryptError::InvalidHash(ref reason) => write!(f, "Invalid hash: {}", reason),
7454
#[cfg(any(feature = "alloc", feature = "std"))]
7555
BcryptError::Rand(ref err) => write!(f, "Rand error: {}", err),
7656
BcryptError::Truncation(len) => {
@@ -84,14 +64,9 @@ impl fmt::Display for BcryptError {
8464
impl error::Error for BcryptError {
8565
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
8666
match *self {
87-
BcryptError::Io(ref err) => Some(err),
88-
BcryptError::InvalidCost(_)
89-
| BcryptError::CostNotAllowed(_)
90-
| BcryptError::InvalidPrefix(_)
67+
BcryptError::CostNotAllowed(_)
9168
| BcryptError::InvalidHash(_)
92-
| BcryptError::InvalidSaltLen(_)
9369
| BcryptError::Truncation(_) => None,
94-
BcryptError::InvalidBase64(ref err) => Some(err),
9570
BcryptError::Rand(ref err) => Some(err),
9671
}
9772
}

src/lib.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,25 +157,27 @@ fn split_hash(hash: &str) -> BcryptResult<HashParts> {
157157
let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect();
158158

159159
if raw_parts.len() != 3 {
160-
return Err(BcryptError::InvalidHash(hash.to_string()));
160+
return Err(BcryptError::InvalidHash("the hash format is malformed"));
161161
}
162162

163163
if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x"
164164
{
165-
return Err(BcryptError::InvalidPrefix(raw_parts[0].to_string()));
165+
return Err(BcryptError::InvalidHash(
166+
"the hash prefix is not a bcrypt prefix",
167+
));
166168
}
167169

168170
if let Ok(c) = raw_parts[1].parse::<u32>() {
169171
parts.cost = c;
170172
} else {
171-
return Err(BcryptError::InvalidCost(raw_parts[1].to_string()));
173+
return Err(BcryptError::InvalidHash("the cost value is not a number"));
172174
}
173175

174176
if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) {
175177
parts.salt = raw_parts[2][..22].chars().collect();
176178
parts.hash = raw_parts[2][22..].chars().collect();
177179
} else {
178-
return Err(BcryptError::InvalidHash(hash.to_string()));
180+
return Err(BcryptError::InvalidHash("the hash format is malformed"));
179181
}
180182

181183
Ok(parts)
@@ -257,17 +259,22 @@ fn _verify<P: AsRef<[u8]>>(password: P, hash: &str, err_on_truncation: bool) ->
257259
use subtle::ConstantTimeEq;
258260

259261
let parts = split_hash(hash)?;
260-
let salt = BASE_64.decode(&parts.salt)?;
261-
let salt_len = salt.len();
262+
let salt = BASE_64
263+
.decode(&parts.salt)
264+
.map_err(|_| BcryptError::InvalidHash("the salt part is not valid base64"))?;
262265
let generated = _hash_password(
263266
password.as_ref(),
264267
parts.cost,
265268
salt.try_into()
266-
.map_err(|_| BcryptError::InvalidSaltLen(salt_len))?,
269+
.map_err(|_| BcryptError::InvalidHash("the salt length is not 16 bytes"))?,
267270
err_on_truncation,
268271
)?;
269-
let source_decoded = BASE_64.decode(parts.hash)?;
270-
let generated_decoded = BASE_64.decode(generated.hash)?;
272+
let source_decoded = BASE_64
273+
.decode(parts.hash)
274+
.map_err(|_| BcryptError::InvalidHash("the hash to verify against is not valid base64"))?;
275+
let generated_decoded = BASE_64.decode(generated.hash).map_err(|_| {
276+
BcryptError::InvalidHash("the generated hash for the password is not valid base64")
277+
})?;
271278

272279
Ok(source_decoded.ct_eq(&generated_decoded).into())
273280
}

0 commit comments

Comments
 (0)