use crate::ctx::BoxFuture;
use argon2::password_hash::{PasswordHash, PasswordHasher as _, PasswordVerifier as _, SaltString};
use argon2::{Algorithm, Argon2, Params, Version};
use base64::Engine;
use ring::pbkdf2;
use std::num::NonZeroU32;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verification {
Failed,
Success,
SuccessRehashNeeded,
}
pub const MAX_PASSWORD_BYTES: usize = 1024;
pub const MAX_PBKDF2_ITERATIONS: u32 = 2_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PasswordHasher {
pub memory_kib: u32,
pub iterations: u32,
pub parallelism: u32,
}
impl Default for PasswordHasher {
fn default() -> Self {
PasswordHasher {
memory_kib: 19 * 1024,
iterations: 2,
parallelism: 1,
}
}
}
impl PasswordHasher {
fn argon(&self) -> Option<Argon2<'static>> {
let params = Params::new(self.memory_kib, self.iterations, self.parallelism, None).ok()?;
Some(Argon2::new(Algorithm::Argon2id, Version::V0x13, params))
}
pub fn hash(&self, password: &str) -> crate::Result<String> {
let salt = SaltString::encode_b64(&super::random_bytes::<16>())
.map_err(|e| crate::Error::Internal(format!("salt: {e}")))?;
let argon = self
.argon()
.ok_or_else(|| crate::Error::Internal("invalid Argon2 parameters".into()))?;
argon
.hash_password(password.as_bytes(), &salt)
.map(|h| h.to_string())
.map_err(|e| crate::Error::Internal(format!("password hash: {e}")))
}
pub fn verify(&self, stored: &str, password: &str) -> Verification {
if password.len() > MAX_PASSWORD_BYTES {
return Verification::Failed;
}
if stored.starts_with("$argon2") {
return self.verify_argon(stored, password);
}
match dotnet::verify(stored, password) {
true => Verification::SuccessRehashNeeded,
false => Verification::Failed,
}
}
fn verify_argon(&self, stored: &str, password: &str) -> Verification {
let Ok(parsed) = PasswordHash::new(stored) else {
return Verification::Failed;
};
let Ok(params) = Params::try_from(&parsed) else {
return Verification::Failed;
};
if params.m_cost() > 1024 * 1024 || params.t_cost() > 64 || params.p_cost() > 16 {
return Verification::Failed;
}
let Some(argon) = self.argon() else {
return Verification::Failed;
};
if argon
.verify_password(password.as_bytes(), &parsed)
.is_err()
{
return Verification::Failed;
}
let weaker = parsed.algorithm.as_str() != "argon2id"
|| params.m_cost() < self.memory_kib
|| params.t_cost() < self.iterations
|| params.p_cost() < self.parallelism;
if weaker {
Verification::SuccessRehashNeeded
} else {
Verification::Success
}
}
}
pub mod dotnet {
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Prf {
Sha1 = 0,
Sha256 = 1,
Sha512 = 2,
}
impl Prf {
fn algorithm(self) -> pbkdf2::Algorithm {
match self {
Prf::Sha1 => pbkdf2::PBKDF2_HMAC_SHA1,
Prf::Sha256 => pbkdf2::PBKDF2_HMAC_SHA256,
Prf::Sha512 => pbkdf2::PBKDF2_HMAC_SHA512,
}
}
}
fn read_u32(bytes: &[u8], at: usize) -> Option<u32> {
Some(u32::from_be_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
}
pub fn verify(stored: &str, password: &str) -> bool {
let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(stored.trim()) else {
return false;
};
match bytes.first() {
Some(0x00) => {
if bytes.len() != 1 + 16 + 32 {
return false;
}
let iterations = NonZeroU32::new(1000).expect("non zero");
pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA1,
iterations,
&bytes[1..17],
password.as_bytes(),
&bytes[17..],
)
.is_ok()
}
Some(0x01) => {
let (Some(prf), Some(iterations), Some(salt_len)) = (
read_u32(&bytes, 1),
read_u32(&bytes, 5),
read_u32(&bytes, 9),
) else {
return false;
};
let prf = match prf {
0 => Prf::Sha1,
1 => Prf::Sha256,
2 => Prf::Sha512,
_ => return false,
};
let salt_len = salt_len as usize;
if salt_len < 16
|| iterations > MAX_PBKDF2_ITERATIONS
|| bytes.len() < 13 + salt_len + 16
|| bytes.len() > 13 + 1024
{
return false;
}
let Some(iterations) = NonZeroU32::new(iterations) else {
return false;
};
let (salt, subkey) = bytes[13..].split_at(salt_len);
pbkdf2::verify(
prf.algorithm(),
iterations,
salt,
password.as_bytes(),
subkey,
)
.is_ok()
}
_ => false,
}
}
pub fn hash_v2(password: &str, salt: [u8; 16]) -> String {
let mut subkey = [0u8; 32];
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA1,
NonZeroU32::new(1000).expect("non zero"),
&salt,
password.as_bytes(),
&mut subkey,
);
let mut out = vec![0x00];
out.extend_from_slice(&salt);
out.extend_from_slice(&subkey);
base64::engine::general_purpose::STANDARD.encode(out)
}
pub fn hash_v3(password: &str, prf: Prf, iterations: u32, salt: &[u8]) -> String {
let mut subkey = [0u8; 32];
pbkdf2::derive(
prf.algorithm(),
NonZeroU32::new(iterations.max(1)).expect("non zero"),
salt,
password.as_bytes(),
&mut subkey,
);
let mut out = vec![0x01];
out.extend_from_slice(&(prf as u32).to_be_bytes());
out.extend_from_slice(&iterations.to_be_bytes());
out.extend_from_slice(&(salt.len() as u32).to_be_bytes());
out.extend_from_slice(salt);
out.extend_from_slice(&subkey);
base64::engine::general_purpose::STANDARD.encode(out)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PasswordOptions {
pub required_length: usize,
pub required_unique_chars: usize,
pub require_non_alphanumeric: bool,
pub require_lowercase: bool,
pub require_uppercase: bool,
pub require_digit: bool,
}
impl Default for PasswordOptions {
fn default() -> Self {
PasswordOptions {
required_length: 6,
required_unique_chars: 1,
require_non_alphanumeric: true,
require_lowercase: true,
require_uppercase: true,
require_digit: true,
}
}
}
impl PasswordOptions {
pub fn check(&self, password: &str) -> Vec<(&'static str, Option<usize>)> {
let mut errors = Vec::new();
let chars = password.chars().count();
if password.trim().is_empty() || chars < self.required_length {
errors.push(("password_too_short", Some(self.required_length)));
}
if password.len() > MAX_PASSWORD_BYTES {
errors.push(("password_too_long", Some(MAX_PASSWORD_BYTES)));
}
if self.require_non_alphanumeric
&& password
.chars()
.all(|c| c.is_ascii_alphanumeric())
{
errors.push(("password_requires_non_alphanumeric", None));
}
if self.require_digit
&& !password
.chars()
.any(|c| c.is_ascii_digit())
{
errors.push(("password_requires_digit", None));
}
if self.require_lowercase
&& !password
.chars()
.any(|c| c.is_ascii_lowercase())
{
errors.push(("password_requires_lower", None));
}
if self.require_uppercase
&& !password
.chars()
.any(|c| c.is_ascii_uppercase())
{
errors.push(("password_requires_upper", None));
}
if self.required_unique_chars >= 1 {
let mut seen: Vec<char> = password.chars().collect();
seen.sort_unstable();
seen.dedup();
if seen.len() < self.required_unique_chars {
errors.push((
"password_requires_unique_chars",
Some(self.required_unique_chars),
));
}
}
errors
}
}
pub trait BreachedPasswords: Send + Sync + 'static {
fn is_breached(&self, password: &str) -> BoxFuture<crate::Result<bool>>;
}
#[derive(Clone, Default)]
pub struct PasswordDenyList(Arc<std::collections::HashSet<String>>);
impl PasswordDenyList {
pub fn new<I: IntoIterator<Item = S>, S: Into<String>>(passwords: I) -> Self {
PasswordDenyList(Arc::new(
passwords
.into_iter()
.map(Into::into)
.collect(),
))
}
}
impl BreachedPasswords for PasswordDenyList {
fn is_breached(&self, password: &str) -> BoxFuture<crate::Result<bool>> {
let hit = self.0.contains(password);
Box::pin(async move { Ok(hit) })
}
}