Skip to content

Commit

Permalink
password-auth: add is_hash_obsolete (#428)
Browse files Browse the repository at this point in the history
Adds a method for interrogating whether a given password hash is using
the recommended algorithm and parameters.

This can be used by password hash upgrade systems to determine whether a
particular hash should be upgraded/recomputed.

It additionally adds a `ParseError` type which wraps
`password_hash::Error`. This is only used by `is_hash_obsolete` for now
but it might be good for future versions to use this error type to
capture parse errors in `verify_password`.

However, that would be a breaking change, which is avoided for now.
  • Loading branch information
tarcieri committed Jun 24, 2023
1 parent 765ed9b commit bb416f9
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 18 deletions.
4 changes: 2 additions & 2 deletions password-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ with support for [Argon2], [PBKDF2], and [scrypt] password hashing algorithms.
## About

`password-auth` is a high-level password authentication library with a simple
API which eliminates as much complexity and user choice as possible. It only
has two functions:
interface which eliminates as much complexity and user choice as possible.
The core API consists of two functions:

- [`generate_hash`]: generates a password hash from the provided password. The
- [`verify_password`]: verifies the provided password against a password hash,
Expand Down
36 changes: 36 additions & 0 deletions password-auth/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Error types.

use core::fmt;

/// Password hash parse errors.
#[derive(Clone, Copy, Debug)]
pub struct ParseError(password_hash::Error);

impl ParseError {
/// Create a new parse error.
pub(crate) fn new(err: password_hash::Error) -> Self {
Self(err)
}
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &self.0)
}
}

#[cfg(feature = "std")]
impl std::error::Error for ParseError {}

/// Password hash verification errors.
#[derive(Clone, Copy, Debug)]
pub struct VerifyError;

impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("password verification error")
}
}

#[cfg(feature = "std")]
impl std::error::Error for VerifyError {}
60 changes: 44 additions & 16 deletions password-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ extern crate alloc;
#[cfg(feature = "std")]
extern crate std;

mod errors;

pub use crate::errors::{ParseError, VerifyError};

use alloc::string::{String, ToString};
use core::fmt;
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use password_hash::{ParamsString, PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use rand_core::OsRng;

#[cfg(not(any(feature = "argon2", feature = "pbkdf2", feature = "scrypt")))]
Expand All @@ -38,19 +41,6 @@ use pbkdf2::Pbkdf2;
#[cfg(feature = "scrypt")]
use scrypt::Scrypt;

/// Opaque error type.
#[derive(Clone, Copy, Debug)]
pub struct VerifyError;

impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("password verification error")
}
}

#[cfg(feature = "std")]
impl std::error::Error for VerifyError {}

/// Generate a password hash for the given password.
///
/// Uses the best available password hashing algorithm given the enabled
Expand Down Expand Up @@ -104,9 +94,46 @@ pub fn verify_password(password: impl AsRef<[u8]>, hash: &str) -> Result<(), Ver
.map_err(|_| VerifyError)
}

/// Determine if the given password hash is using the recommended algorithm and
/// parameters.
///
/// This can be used by implementations which wish to lazily update their
/// password hashes (i.e. by rehashing the password with [`generate_hash`])
/// to determine if such an update should be applied.
///
/// # Returns
/// - `true` if the hash can't be parsed or isn't using the latest
/// recommended parameters
/// - `false` if the hash is using the latest recommended parameters
#[allow(unreachable_code)]
pub fn is_hash_obsolete(hash: &str) -> Result<bool, ParseError> {
let hash = PasswordHash::new(hash).map_err(ParseError::new)?;

#[cfg(feature = "argon2")]
return Ok(hash.algorithm != argon2::Algorithm::default().ident()
|| hash.params != default_params_string::<argon2::Params>());

#[cfg(feature = "scrypt")]
return Ok(hash.algorithm != scrypt::ALG_ID
|| hash.params != default_params_string::<scrypt::Params>());

#[cfg(feature = "pbkdf2")]
return Ok(hash.algorithm != pbkdf2::Algorithm::default().ident()
|| hash.params != default_params_string::<pbkdf2::Params>());

Ok(true)
}

fn default_params_string<T>() -> ParamsString
where
T: Default + TryInto<ParamsString, Error = password_hash::Error>,
{
T::default().try_into().expect("invalid default params")
}

#[cfg(test)]
mod tests {
use super::{generate_hash, verify_password};
use super::{generate_hash, is_hash_obsolete, verify_password};

const EXAMPLE_PASSWORD: &str = "password";

Expand All @@ -115,6 +142,7 @@ mod tests {
let hash = generate_hash(EXAMPLE_PASSWORD);
assert!(verify_password(EXAMPLE_PASSWORD, &hash).is_ok());
assert!(verify_password("bogus", &hash).is_err());
assert!(!is_hash_obsolete(&hash).unwrap());
}

#[cfg(feature = "argon2")]
Expand Down

0 comments on commit bb416f9

Please sign in to comment.