use super::hash_algorithm::HashAlgorithm;
use crate::algorithms::{
argon2id::{self as a2, Argon2d, Argon2i, Argon2id},
bcrypt::Bcrypt,
pbkdf2::{Pbkdf2, Pbkdf2Params},
scrypt::{Scrypt, ScryptParams},
};
use crate::error::{Error, Result};
use crate::models::hash_algorithm::HashingAlgorithm;
use ::argon2::Algorithm as Argon2Algorithm;
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub type Salt = Vec<u8>;
#[non_exhaustive]
#[derive(
Clone,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
ZeroizeOnDrop,
)]
pub struct Hash {
hash: Vec<u8>,
salt: Salt,
#[zeroize(skip)]
algorithm: HashAlgorithm,
}
impl Hash {
pub fn new_argon2id(password: &str, salt: Salt) -> Result<Self> {
let salt_str = std::str::from_utf8(&salt)?;
let calculated = Argon2id::hash_password(password, salt_str)?;
Ok(Self {
hash: calculated,
salt,
algorithm: HashAlgorithm::Argon2id,
})
}
#[cfg(feature = "compat-v0_0_x")]
#[deprecated(
since = "0.0.9",
note = "Argon2i is verify-only — use Hash::new_argon2id for new hashes."
)]
pub fn new_argon2i(password: &str, salt: Salt) -> Result<Self> {
let salt_str = std::str::from_utf8(&salt)?;
let calculated = Argon2i::hash_password(password, salt_str)?;
Ok(Self {
hash: calculated,
salt,
algorithm: HashAlgorithm::Argon2i,
})
}
pub fn new_bcrypt(password: &str, cost: u32) -> Result<Self> {
use crate::algorithms::bcrypt::{
BcryptParams, PrehashAlgorithm,
};
let params = BcryptParams {
cost,
prehash: PrehashAlgorithm::None,
};
let hashed = Bcrypt::hash_with(password, params)?;
Ok(Self {
hash: hashed,
salt: Vec::new(),
algorithm: HashAlgorithm::Bcrypt,
})
}
pub fn new_scrypt(password: &str, salt: Salt) -> Result<Self> {
let salt_str = std::str::from_utf8(&salt)?;
let calculated = Scrypt::hash_password(password, salt_str)?;
Ok(Self {
hash: calculated,
salt,
algorithm: HashAlgorithm::Scrypt,
})
}
pub fn algorithm(&self) -> HashAlgorithm {
self.algorithm
}
pub fn from_hash(hash: &[u8], algo: &str) -> Result<Self> {
let algorithm = parse_algorithm_tag(algo)?;
Ok(Hash {
salt: Vec::new(),
hash: hash.to_vec(),
algorithm,
})
}
pub fn from_string(hash_str: &str) -> Result<Self> {
let parts: Vec<&str> = hash_str.split('$').collect();
if parts.len() != 6 {
return Err(Error::InvalidHashString(
"expected 6 fields separated by '$'".into(),
));
}
let algorithm = Self::parse_algorithm(hash_str)?;
let salt = format!(
"${}${}${}${}",
parts[1], parts[2], parts[3], parts[4]
);
let hash_bytes = general_purpose::STANDARD.decode(parts[5])?;
Ok(Hash {
salt: salt.into_bytes(),
hash: hash_bytes,
algorithm,
})
}
pub fn generate_hash(
password: &str,
salt: &str,
algo: &str,
) -> Result<Vec<u8>> {
match algo {
"argon2id" => Argon2id::hash_password(password, salt),
"argon2i" => Argon2i::hash_password(password, salt),
"argon2d" => Argon2d::hash_password(password, salt),
"bcrypt" => Bcrypt::hash_password(password, salt),
"scrypt" => Scrypt::hash_password(password, salt),
"pbkdf2" | "pbkdf2-sha256" => {
Pbkdf2::hash_password(password, salt)
}
other => Err(Error::UnsupportedAlgorithm(
other.to_owned().into(),
)),
}
}
pub fn generate_random_string(len: usize) -> Result<String> {
const CHARS: &[u8] =
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut raw = vec![0u8; len];
getrandom::getrandom(&mut raw).map_err(|e| {
Error::hashing(
crate::error::HashingErrorKind::Argon2,
format!("OS RNG failed: {e}"),
)
})?;
let s: String = raw
.into_iter()
.map(|b| CHARS[usize::from(b) % CHARS.len()] as char)
.collect();
Ok(s)
}
pub fn generate_salt(algo: &str) -> Result<String> {
match algo {
"argon2id" | "argon2i" | "argon2d" => {
Self::generate_random_string(16)
}
"bcrypt" => {
let mut raw = [0u8; 16];
getrandom::getrandom(&mut raw).map_err(|e| {
Error::hashing(
crate::error::HashingErrorKind::Argon2,
format!("OS RNG failed: {e}"),
)
})?;
Ok(general_purpose::STANDARD.encode(raw))
}
"scrypt" => {
let mut raw = [0u8; 32];
getrandom::getrandom(&mut raw).map_err(|e| {
Error::hashing(
crate::error::HashingErrorKind::Argon2,
format!("OS RNG failed: {e}"),
)
})?;
Ok(general_purpose::STANDARD.encode(raw))
}
other => Err(Error::UnsupportedAlgorithm(
other.to_owned().into(),
)),
}
}
pub fn hash(&self) -> &[u8] {
&self.hash
}
pub fn hash_length(&self) -> usize {
self.hash.len()
}
pub fn new(password: &str, salt: &str, algo: &str) -> Result<Self> {
if password.len() < 8 {
return Err(Error::InvalidPassword(
"must be at least 8 characters".into(),
));
}
let hash = Self::generate_hash(password, salt, algo)?;
let algorithm = parse_algorithm_tag(algo)?;
Ok(Self {
hash,
salt: salt.as_bytes().to_vec(),
algorithm,
})
}
pub fn parse(input: &str) -> Result<Self> {
Ok(serde_json::from_str(input)?)
}
pub fn parse_algorithm(hash_str: &str) -> Result<HashAlgorithm> {
let parts: Vec<&str> = hash_str.split('$').collect();
if parts.len() < 2 {
return Err(Error::InvalidHashString(
"missing algorithm marker".into(),
));
}
parse_algorithm_tag(parts[1])
}
pub fn salt(&self) -> &[u8] {
&self.salt
}
pub fn set_hash(&mut self, hash: &[u8]) {
self.hash.zeroize();
self.hash = hash.to_vec();
}
pub fn set_password(
&mut self,
password: &str,
salt: &str,
algo: &str,
) -> Result<()> {
let new_hash = Self::generate_hash(password, salt, algo)?;
self.hash.zeroize();
self.hash = new_hash;
Ok(())
}
pub fn set_salt(&mut self, salt: &[u8]) {
self.salt.zeroize();
self.salt = salt.to_vec();
}
pub fn to_string_representation(&self) -> String {
let hash_str = self
.hash
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<String>>()
.join("");
format!("{}:{}", String::from_utf8_lossy(&self.salt), hash_str)
}
pub fn verify(&self, password: &str) -> Result<bool> {
let salt = std::str::from_utf8(&self.salt)?;
match self.algorithm {
HashAlgorithm::Argon2id => a2::verify(
Argon2Algorithm::Argon2id,
a2::owasp_minimum_2025(),
password,
salt,
&self.hash,
),
HashAlgorithm::Argon2i => a2::verify(
Argon2Algorithm::Argon2i,
a2::owasp_minimum_2025(),
password,
salt,
&self.hash,
),
HashAlgorithm::Argon2d => a2::verify(
Argon2Algorithm::Argon2d,
a2::owasp_minimum_2025(),
password,
salt,
&self.hash,
),
HashAlgorithm::Bcrypt => {
use crate::algorithms::bcrypt::PrehashAlgorithm;
let stored_str = std::str::from_utf8(&self.hash)?;
Bcrypt::verify(
password,
stored_str,
PrehashAlgorithm::None,
)
}
HashAlgorithm::Scrypt => {
let calculated = Scrypt::hash_with(
password,
salt,
ScryptParams::default(),
)?;
let ok = bool::from(calculated.ct_eq(&self.hash));
let mut tmp = calculated;
tmp.zeroize();
Ok(ok)
}
HashAlgorithm::Pbkdf2 => {
let calculated = Pbkdf2::hash_with(
password.as_bytes(),
salt.as_bytes(),
Pbkdf2Params::default(),
)?;
let ok = bool::from(calculated.ct_eq(&self.hash));
let mut tmp = calculated;
tmp.zeroize();
Ok(ok)
}
}
}
}
fn parse_algorithm_tag(algo: &str) -> Result<HashAlgorithm> {
match algo {
"pbkdf2" | "pbkdf2-sha256" | "pbkdf2-sha512" => {
Ok(HashAlgorithm::Pbkdf2)
}
"argon2id" => Ok(HashAlgorithm::Argon2id),
"argon2i" => Ok(HashAlgorithm::Argon2i),
"argon2d" => Ok(HashAlgorithm::Argon2d),
"bcrypt" => Ok(HashAlgorithm::Bcrypt),
"scrypt" => Ok(HashAlgorithm::Scrypt),
other => {
Err(Error::UnsupportedAlgorithm(other.to_owned().into()))
}
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash {{ hash: {:?} }}", self.hash)
}
}
impl fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl FromStr for HashAlgorithm {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
parse_algorithm_tag(s).map_err(|_| {
Error::UnsupportedAlgorithm(s.to_owned().into())
})
}
}
#[derive(
Clone,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
pub struct HashBuilder {
hash: Option<Vec<u8>>,
salt: Option<Salt>,
algorithm: Option<HashAlgorithm>,
}
impl HashBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn hash(mut self, hash: Vec<u8>) -> Self {
self.hash = Some(hash);
self
}
pub fn salt(mut self, salt: Salt) -> Self {
self.salt = Some(salt);
self
}
pub fn algorithm(mut self, algorithm: HashAlgorithm) -> Self {
self.algorithm = Some(algorithm);
self
}
pub fn build(self) -> Result<Hash> {
match (self.hash, self.salt, self.algorithm) {
(Some(hash), Some(salt), Some(algorithm)) => Ok(Hash {
hash,
salt,
algorithm,
}),
_ => Err(Error::InvalidHashString(
"HashBuilder missing one of: hash, salt, algorithm"
.into(),
)),
}
}
}