#[cfg(feature = "argon2")]
use argon2::password_hash::rand_core::OsRng;
#[cfg(feature = "argon2")]
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
#[cfg(feature = "argon2")]
use argon2::Argon2;
pub trait PasswordEncoder: Send + Sync {
fn encode(&self, raw_password: &str) -> String;
fn matches(&self, raw_password: &str, encoded_password: &str) -> bool;
fn upgrade_encoding(&self, _encoded_password: &str) -> bool {
false
}
}
#[cfg(feature = "argon2")]
#[derive(Clone)]
pub struct Argon2PasswordEncoder {
argon2: Argon2<'static>,
}
#[cfg(feature = "argon2")]
impl Argon2PasswordEncoder {
pub fn new() -> Self {
Argon2PasswordEncoder {
argon2: Argon2::default(),
}
}
}
#[cfg(feature = "argon2")]
impl Default for Argon2PasswordEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "argon2")]
impl PasswordEncoder for Argon2PasswordEncoder {
fn encode(&self, raw_password: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
self.argon2
.hash_password(raw_password.as_bytes(), &salt)
.expect("Failed to hash password")
.to_string()
}
fn matches(&self, raw_password: &str, encoded_password: &str) -> bool {
match PasswordHash::new(encoded_password) {
Ok(parsed_hash) => self
.argon2
.verify_password(raw_password.as_bytes(), &parsed_hash)
.is_ok(),
Err(_) => false,
}
}
}
#[cfg(feature = "bcrypt")]
#[derive(Clone)]
pub struct BCryptPasswordEncoder {
cost: u32,
}
#[cfg(feature = "bcrypt")]
impl BCryptPasswordEncoder {
pub fn new() -> Self {
Self { cost: 12 }
}
pub fn with_cost(cost: u32) -> Self {
let cost = cost.clamp(4, 31);
Self { cost }
}
pub fn with_strength(strength: &str) -> Self {
let cost = match strength {
"weak" => 10,
"strong" => 14,
_ => 12,
};
Self { cost }
}
}
#[cfg(feature = "bcrypt")]
impl Default for BCryptPasswordEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "bcrypt")]
impl PasswordEncoder for BCryptPasswordEncoder {
fn encode(&self, raw_password: &str) -> String {
bcrypt::hash(raw_password, self.cost).expect("Failed to hash password with bcrypt")
}
fn matches(&self, raw_password: &str, encoded_password: &str) -> bool {
bcrypt::verify(raw_password, encoded_password).unwrap_or(false)
}
fn upgrade_encoding(&self, encoded_password: &str) -> bool {
if encoded_password.starts_with("$2") && encoded_password.len() > 7 {
if let Some(cost_str) = encoded_password.get(4..6) {
if let Ok(hash_cost) = cost_str.parse::<u32>() {
return hash_cost < self.cost;
}
}
}
true }
}
#[derive(Clone, Copy, Default)]
pub struct NoOpPasswordEncoder;
impl PasswordEncoder for NoOpPasswordEncoder {
fn encode(&self, raw_password: &str) -> String {
raw_password.to_string()
}
fn matches(&self, raw_password: &str, encoded_password: &str) -> bool {
raw_password == encoded_password
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum DefaultEncoder {
#[default]
Argon2,
BCrypt,
}
#[cfg(feature = "argon2")]
#[derive(Clone)]
pub struct DelegatingPasswordEncoder {
argon2: Argon2PasswordEncoder,
#[cfg(feature = "bcrypt")]
bcrypt: BCryptPasswordEncoder,
default_encoder: DefaultEncoder,
}
#[cfg(feature = "argon2")]
impl DelegatingPasswordEncoder {
pub fn new() -> Self {
DelegatingPasswordEncoder {
argon2: Argon2PasswordEncoder::new(),
#[cfg(feature = "bcrypt")]
bcrypt: BCryptPasswordEncoder::new(),
default_encoder: DefaultEncoder::Argon2,
}
}
pub fn default_encoder(mut self, encoder: DefaultEncoder) -> Self {
self.default_encoder = encoder;
self
}
#[cfg(feature = "bcrypt")]
pub fn use_bcrypt(self) -> Self {
self.default_encoder(DefaultEncoder::BCrypt)
}
}
#[cfg(feature = "argon2")]
impl Default for DelegatingPasswordEncoder {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "argon2")]
impl PasswordEncoder for DelegatingPasswordEncoder {
fn encode(&self, raw_password: &str) -> String {
match self.default_encoder {
DefaultEncoder::Argon2 => {
format!("{{argon2}}{}", self.argon2.encode(raw_password))
}
#[cfg(feature = "bcrypt")]
DefaultEncoder::BCrypt => {
format!("{{bcrypt}}{}", self.bcrypt.encode(raw_password))
}
#[cfg(not(feature = "bcrypt"))]
DefaultEncoder::BCrypt => {
format!("{{argon2}}{}", self.argon2.encode(raw_password))
}
}
}
fn matches(&self, raw_password: &str, encoded_password: &str) -> bool {
if let Some(hash) = encoded_password.strip_prefix("{argon2}") {
self.argon2.matches(raw_password, hash)
} else if let Some(plain) = encoded_password.strip_prefix("{noop}") {
raw_password == plain
} else {
#[cfg(feature = "bcrypt")]
if let Some(hash) = encoded_password.strip_prefix("{bcrypt}") {
return self.bcrypt.matches(raw_password, hash);
}
#[cfg(feature = "bcrypt")]
if encoded_password.starts_with("$2") {
return self.bcrypt.matches(raw_password, encoded_password);
}
raw_password == encoded_password
}
}
fn upgrade_encoding(&self, encoded_password: &str) -> bool {
match self.default_encoder {
DefaultEncoder::Argon2 => !encoded_password.starts_with("{argon2}"),
DefaultEncoder::BCrypt => !encoded_password.starts_with("{bcrypt}"),
}
}
}
#[cfg(all(test, feature = "argon2"))]
mod tests {
use super::*;
#[test]
fn test_argon2_encoder() {
let encoder = Argon2PasswordEncoder::new();
let password = "test_password_123";
let hash = encoder.encode(password);
assert_ne!(hash, password);
assert!(encoder.matches(password, &hash));
assert!(!encoder.matches("wrong_password", &hash));
}
#[test]
fn test_noop_encoder() {
let encoder = NoOpPasswordEncoder;
let password = "plain_password";
let encoded = encoder.encode(password);
assert_eq!(encoded, password);
assert!(encoder.matches(password, &encoded));
}
#[test]
fn test_delegating_encoder() {
let encoder = DelegatingPasswordEncoder::new();
let hash = encoder.encode("password");
assert!(hash.starts_with("{argon2}"));
assert!(encoder.matches("password", &hash));
assert!(encoder.matches("plain", "{noop}plain"));
assert!(encoder.upgrade_encoding("{noop}plain"));
assert!(!encoder.upgrade_encoding(&hash));
}
}