use crate::algorithms::bcrypt::{Bcrypt, PrehashAlgorithm};
use crate::algorithms::pbkdf2::{Pbkdf2, Pbkdf2Params, Prf};
use crate::backend::Backend;
use crate::error::{Error, HashingErrorKind, Result};
use crate::outcome::Outcome;
use crate::policy::{Policy, PrimaryAlgorithm};
use argon2::password_hash::{
PasswordHash, PasswordHasher, PasswordVerifier, SaltString,
};
use argon2::{Algorithm, Argon2, Version};
use base64::{engine::general_purpose, Engine as _};
use rand_core::OsRng;
use scrypt::Scrypt as ScryptHasher;
use subtle::ConstantTimeEq;
#[cfg(feature = "pepper")]
const PEPPER_PREFIX: &str = "hsh-pepper:";
const BCRYPT_PREHASH_SHA256_PREFIX: &str = "hsh-bcrypt-sha256:";
pub fn hash(
policy: &Policy,
password: impl AsRef<[u8]>,
) -> Result<String> {
let password = password.as_ref();
#[cfg(feature = "pepper")]
if let Some(pepper) = policy.pepper.as_ref() {
let version = pepper.current();
let tag = pepper.apply(version, password)?;
let peppered = general_purpose::STANDARD_NO_PAD.encode(tag);
let inner = hash_unpeppered(policy, peppered.as_bytes())?;
return Ok(format!("{PEPPER_PREFIX}{}:{inner}", version.get()));
}
hash_unpeppered(policy, password)
}
fn hash_unpeppered(policy: &Policy, password: &[u8]) -> Result<String> {
if policy.backend.is_fips()
&& !matches!(policy.primary, PrimaryAlgorithm::Pbkdf2)
{
return Err(fips_primary_must_be_pbkdf2(policy.primary));
}
if policy.backend.is_fips() && !Backend::fips_available_in_build() {
return Err(fips_feature_not_built());
}
match policy.primary {
PrimaryAlgorithm::Argon2id => {
let salt = SaltString::generate(&mut OsRng);
let engine = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
policy.argon2.clone(),
);
let phc = engine
.hash_password(password, &salt)
.map_err(map_argon2_err)?;
Ok(phc.to_string())
}
PrimaryAlgorithm::Bcrypt => {
let pw_str = std::str::from_utf8(password)
.map_err(|_| bcrypt_requires_utf8())?;
let bytes = Bcrypt::hash_with(pw_str, policy.bcrypt)?;
let mcf = String::from_utf8(bytes)
.map_err(map_bcrypt_utf8_err)?;
Ok(match policy.bcrypt.prehash {
PrehashAlgorithm::None => mcf,
PrehashAlgorithm::Sha256 => {
format!("{BCRYPT_PREHASH_SHA256_PREFIX}{mcf}")
}
})
}
PrimaryAlgorithm::Scrypt => {
let salt = SaltString::generate(&mut OsRng);
let native = policy.scrypt.to_native()?;
let phc = ScryptHasher
.hash_password_customized(
password, None, None, native, &salt,
)
.map_err(map_scrypt_err)?;
Ok(phc.to_string())
}
PrimaryAlgorithm::Pbkdf2 => {
let salt = SaltString::generate(&mut OsRng);
let raw = Pbkdf2::hash_with(
password,
salt.as_str().as_bytes(),
policy.pbkdf2,
)?;
let salt_b64 = salt.as_str();
let hash_b64 =
general_purpose::STANDARD_NO_PAD.encode(&raw);
Ok(format!(
"${alg}$i={iters},l={len}${salt_b64}${hash_b64}",
alg = match policy.pbkdf2.prf {
Prf::Sha256 => "pbkdf2-sha256",
Prf::Sha512 => "pbkdf2-sha512",
},
iters = policy.pbkdf2.iterations,
len = policy.pbkdf2.dk_len,
))
}
}
}
pub fn verify_and_upgrade(
policy: &Policy,
password: impl AsRef<[u8]>,
stored: impl AsRef<str>,
) -> Result<Outcome> {
verify_dispatch(policy, password.as_ref(), stored.as_ref())
}
fn verify_dispatch(
policy: &Policy,
password: &[u8],
stored: &str,
) -> Result<Outcome> {
#[cfg(feature = "pepper")]
if let Some(rest) = stored.strip_prefix(PEPPER_PREFIX) {
let Some(pepper) = policy.pepper.as_ref() else {
return Ok(Outcome::Invalid);
};
let (ver_str, inner) =
rest.split_once(':').ok_or_else(pepper_malformed_prefix)?;
let ver_num: u32 =
ver_str.parse().map_err(|_| pepper_keyver_not_int())?;
let stored_version = hsh_kms::KeyVersion::new(ver_num);
let tag = pepper.apply(stored_version, password)?;
let peppered = general_purpose::STANDARD_NO_PAD.encode(tag);
let inner_outcome =
verify_dispatch_inner(policy, peppered.as_bytes(), inner)?;
if !inner_outcome.is_valid() {
return Ok(Outcome::Invalid);
}
let current = pepper.current();
let needs_rotate =
stored_version != current || inner_outcome.needs_rehash();
if needs_rotate {
let new_phc = hash(policy, password)?;
return Ok(Outcome::Valid {
rehashed: Some(new_phc),
});
}
return Ok(Outcome::Valid { rehashed: None });
}
#[cfg(feature = "pepper")]
if policy.pepper.is_some() && !stored.starts_with(PEPPER_PREFIX) {
let outcome = verify_dispatch_inner(policy, password, stored)?;
if outcome.is_valid() {
let new_phc = hash(policy, password)?;
return Ok(Outcome::Valid {
rehashed: Some(new_phc),
});
}
return Ok(Outcome::Invalid);
}
verify_dispatch_inner(policy, password, stored)
}
fn verify_dispatch_inner(
policy: &Policy,
password: &[u8],
stored: &str,
) -> Result<Outcome> {
if let Some(inner_mcf) =
stored.strip_prefix(BCRYPT_PREHASH_SHA256_PREFIX)
{
return verify_bcrypt(
policy,
password,
inner_mcf,
PrehashAlgorithm::Sha256,
);
}
if stored.starts_with("$2a$")
|| stored.starts_with("$2b$")
|| stored.starts_with("$2x$")
|| stored.starts_with("$2y$")
{
return verify_bcrypt(
policy,
password,
stored,
PrehashAlgorithm::None,
);
}
let parsed =
PasswordHash::new(stored).map_err(|_| phc_not_recognised())?;
let algo_id = parsed.algorithm.as_str();
let valid = match algo_id {
"argon2id" => Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
policy.argon2.clone(),
)
.verify_password(password, &parsed)
.is_ok(),
"argon2i" => Argon2::new(
Algorithm::Argon2i,
Version::V0x13,
policy.argon2.clone(),
)
.verify_password(password, &parsed)
.is_ok(),
"argon2d" => Argon2::new(
Algorithm::Argon2d,
Version::V0x13,
policy.argon2.clone(),
)
.verify_password(password, &parsed)
.is_ok(),
"scrypt" => {
ScryptHasher.verify_password(password, &parsed).is_ok()
}
"pbkdf2-sha256" | "pbkdf2-sha512" => {
verify_pbkdf2_phc(&parsed, password, algo_id)?
}
other => {
return Err(Error::UnsupportedAlgorithm(
other.to_owned().into(),
));
}
};
if !valid {
return Ok(Outcome::Invalid);
}
if needs_rehash(&parsed, algo_id, policy) {
let new_phc = hash_unpeppered(policy, password)?;
Ok(Outcome::Valid {
rehashed: Some(new_phc),
})
} else {
Ok(Outcome::Valid { rehashed: None })
}
}
fn verify_bcrypt(
policy: &Policy,
password: &[u8],
mcf: &str,
stored_prehash: PrehashAlgorithm,
) -> Result<Outcome> {
let pw_str = std::str::from_utf8(password)
.map_err(|_| bcrypt_verify_requires_utf8())?;
let valid = Bcrypt::verify(pw_str, mcf, stored_prehash)?;
if !valid {
return Ok(Outcome::Invalid);
}
let policy_is_bcrypt =
matches!(policy.primary, PrimaryAlgorithm::Bcrypt);
let cost_drift = policy_is_bcrypt
&& !parse_bcrypt_cost(mcf)
.map(|c| policy.bcrypt_satisfies(c))
.unwrap_or(false);
let prehash_drift =
policy_is_bcrypt && stored_prehash != policy.bcrypt.prehash;
if !policy_is_bcrypt || cost_drift || prehash_drift {
let new_phc = hash_unpeppered(policy, password)?;
return Ok(Outcome::Valid {
rehashed: Some(new_phc),
});
}
Ok(Outcome::Valid { rehashed: None })
}
fn verify_pbkdf2_phc(
parsed: &PasswordHash<'_>,
password: &[u8],
algo_id: &str,
) -> Result<bool> {
let salt = parsed.salt.ok_or_else(pbkdf2_missing_salt)?;
let stored = parsed.hash.ok_or_else(pbkdf2_missing_hash)?;
let (iterations, dk_len) =
parse_pbkdf2_params(parsed, stored.as_bytes().len())?;
if iterations == 0 {
return Err(Error::InvalidHashString(
"PBKDF2 PHC missing iteration count".into(),
));
}
let prf = match algo_id {
"pbkdf2-sha256" => Prf::Sha256,
"pbkdf2-sha512" => Prf::Sha512,
other => {
return Err(Error::UnsupportedAlgorithm(
other.to_owned().into(),
));
}
};
let params = Pbkdf2Params {
prf,
iterations,
dk_len,
};
let calculated =
Pbkdf2::hash_with(password, salt.as_str().as_bytes(), params)?;
Ok(bool::from(calculated.ct_eq(stored.as_bytes())))
}
fn needs_rehash(
parsed: &PasswordHash<'_>,
algo_id: &str,
policy: &Policy,
) -> bool {
let primary_matches = matches!(
(policy.primary, algo_id),
(PrimaryAlgorithm::Argon2id, "argon2id")
| (PrimaryAlgorithm::Scrypt, "scrypt")
| (
PrimaryAlgorithm::Pbkdf2,
"pbkdf2-sha256" | "pbkdf2-sha512"
)
);
if !primary_matches {
return true;
}
if algo_id == "argon2id" {
return argon2::Params::try_from(parsed)
.map(|stored| !policy.argon2_satisfies(&stored))
.unwrap_or(true);
}
if algo_id == "scrypt" {
let stored = match parse_scrypt_phc_params(parsed) {
Some(s) => s,
None => return true,
};
return !policy.scrypt_satisfies(&stored);
}
if algo_id == "pbkdf2-sha256" || algo_id == "pbkdf2-sha512" {
let policy_prf_id = match policy.pbkdf2.prf {
Prf::Sha256 => "pbkdf2-sha256",
Prf::Sha512 => "pbkdf2-sha512",
};
if algo_id != policy_prf_id {
return true;
}
let stored_iters = parsed
.params
.iter()
.find(|p| p.0.as_str() == "i")
.and_then(|p| p.1.decimal().ok())
.unwrap_or(0);
let stored_dk_len = parsed
.params
.iter()
.find(|p| p.0.as_str() == "l")
.and_then(|p| p.1.decimal().ok().map(|d| d as usize))
.or_else(|| parsed.hash.map(|h| h.as_bytes().len()))
.unwrap_or(0);
return !policy.pbkdf2_satisfies(stored_iters, stored_dk_len);
}
false
}
fn parse_bcrypt_cost(stored: &str) -> Option<u32> {
let mut parts = stored.splitn(4, '$');
let _empty = parts.next()?; let _ident = parts.next()?; let cost_str = parts.next()?;
cost_str.parse::<u32>().ok()
}
fn parse_scrypt_phc_params(
parsed: &PasswordHash<'_>,
) -> Option<crate::algorithms::scrypt::ScryptParams> {
let mut log_n: Option<u8> = None;
let mut r: Option<u32> = None;
let mut p: Option<u32> = None;
for (k, v) in parsed.params.iter() {
match k.as_str() {
"ln" => {
log_n =
v.decimal().ok().and_then(|d| u8::try_from(d).ok())
}
"r" => r = v.decimal().ok(),
"p" => p = v.decimal().ok(),
_ => {}
}
}
Some(crate::algorithms::scrypt::ScryptParams {
log_n: log_n?,
r: r?,
p: p?,
dk_len: parsed.hash.map(|h| h.as_bytes().len())?,
})
}
#[doc(hidden)]
pub fn map_argon2_err(e: password_hash::Error) -> Error {
Error::hashing(HashingErrorKind::Argon2, e.to_string())
}
#[doc(hidden)]
pub fn map_scrypt_err(e: password_hash::Error) -> Error {
Error::hashing(HashingErrorKind::Scrypt, e.to_string())
}
#[doc(hidden)]
pub fn map_bcrypt_utf8_err(_e: std::string::FromUtf8Error) -> Error {
Error::hashing(
HashingErrorKind::Bcrypt,
"bcrypt produced non-UTF-8 output",
)
}
#[doc(hidden)]
pub fn pbkdf2_missing_salt() -> Error {
Error::InvalidHashString("PBKDF2 PHC missing salt".into())
}
#[doc(hidden)]
pub fn pbkdf2_missing_hash() -> Error {
Error::InvalidHashString("PBKDF2 PHC missing hash".into())
}
#[doc(hidden)]
pub fn parse_pbkdf2_params(
parsed: &PasswordHash<'_>,
default_dk_len: usize,
) -> Result<(u32, usize)> {
let mut iterations: u32 = 0;
let mut dk_len: usize = default_dk_len;
for p in parsed.params.iter() {
match p.0.as_str() {
"i" => {
iterations =
p.1.decimal().map_err(|_| pbkdf2_bad_iter())?;
}
"l" => {
dk_len =
p.1.decimal().map_err(|_| pbkdf2_bad_dk_len())?
as usize;
}
_ => {}
}
}
Ok((iterations, dk_len))
}
#[doc(hidden)]
pub fn pbkdf2_bad_iter() -> Error {
Error::InvalidHashString("PBKDF2 PHC bad iteration count".into())
}
#[doc(hidden)]
pub fn pbkdf2_bad_dk_len() -> Error {
Error::InvalidHashString("PBKDF2 PHC bad output length".into())
}
#[doc(hidden)]
pub fn bcrypt_requires_utf8() -> Error {
Error::InvalidPassword(
"bcrypt requires UTF-8 passwords; supply pre-hash via \
PrehashAlgorithm for arbitrary bytes"
.into(),
)
}
#[doc(hidden)]
pub fn bcrypt_verify_requires_utf8() -> Error {
Error::InvalidPassword(
"bcrypt verification requires UTF-8 passwords".into(),
)
}
#[doc(hidden)]
pub fn pepper_malformed_prefix() -> Error {
Error::InvalidHashString("malformed pepper prefix".into())
}
#[doc(hidden)]
pub fn pepper_keyver_not_int() -> Error {
Error::InvalidHashString("pepper keyver must be an integer".into())
}
#[doc(hidden)]
pub fn phc_not_recognised() -> Error {
Error::InvalidHashString(
"not a recognised PHC or MCF string".into(),
)
}
#[doc(hidden)]
pub fn fips_primary_must_be_pbkdf2(primary: PrimaryAlgorithm) -> Error {
Error::InvalidParameter(
format!(
"Backend::Fips140Required cannot mint hashes with {primary:?} \
— only PBKDF2 has a FIPS 140-3 validated implementation. \
Switch policy.primary to PrimaryAlgorithm::Pbkdf2 or relax \
policy.backend."
)
.into(),
)
}
#[doc(hidden)]
pub fn fips_feature_not_built() -> Error {
Error::InvalidParameter(
"Backend::Fips140Required policy supplied but the `fips` Cargo \
feature is not enabled in this build. Rebuild with `--features \
fips` or relax policy.backend to Backend::Native."
.into(),
)
}