use crate::{Argon2HashSnafu, Argon2ParseSnafu, Error};
use argon2::Argon2;
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use rand_core::OsRng;
use snafu::ResultExt;
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PasswordCheck {
Matched,
MatchedNeedsRehash,
Mismatch,
}
pub fn hash_password(secret: &[u8]) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default()
.hash_password(secret, &salt)
.context(Argon2HashSnafu)?;
Ok(hash.to_string())
}
pub fn verify_password(stored: &str, secret: &[u8]) -> Result<PasswordCheck> {
if is_legacy_sha256(stored) {
return if constant_time_eq(stored.as_bytes(), secret) {
Ok(PasswordCheck::MatchedNeedsRehash)
} else {
Ok(PasswordCheck::Mismatch)
};
}
let parsed = PasswordHash::new(stored).context(Argon2ParseSnafu)?;
match Argon2::default().verify_password(secret, &parsed) {
Ok(()) => Ok(PasswordCheck::Matched),
Err(argon2::password_hash::Error::Password) => Ok(PasswordCheck::Mismatch),
Err(source) => Err(Error::Argon2Parse { source }),
}
}
fn is_legacy_sha256(stored: &str) -> bool {
stored.len() == 64 && stored.bytes().all(|b| b.is_ascii_hexdigit())
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter()
.zip(b.iter())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_then_verify_roundtrip() {
let secret = b"a3f1c9deadbeef";
let stored = hash_password(secret).unwrap();
assert!(stored.starts_with("$argon2"));
assert!(!is_legacy_sha256(&stored));
assert_eq!(
verify_password(&stored, secret).unwrap(),
PasswordCheck::Matched
);
assert_eq!(
verify_password(&stored, b"wrong").unwrap(),
PasswordCheck::Mismatch
);
}
#[test]
fn legacy_sha256_matches_and_flags_rehash() {
let legacy = "e".repeat(64);
assert!(is_legacy_sha256(&legacy));
assert_eq!(
verify_password(&legacy, legacy.as_bytes()).unwrap(),
PasswordCheck::MatchedNeedsRehash
);
assert_eq!(
verify_password(&legacy, "f".repeat(64).as_bytes()).unwrap(),
PasswordCheck::Mismatch
);
}
#[test]
fn distinct_salts_produce_distinct_hashes() {
let secret = b"same-input";
assert_ne!(
hash_password(secret).unwrap(),
hash_password(secret).unwrap()
);
}
}