pub trait PasswordEncoder: Send + Sync {
fn encode(&self, raw: &str) -> String;
fn matches(&self, raw: &str, encoded: &str) -> bool;
fn upgrade_encoding(&self, _encoded: &str) -> bool {
false
}
}
pub struct BcryptPasswordEncoder {
cost: u32,
}
impl BcryptPasswordEncoder {
pub fn new() -> Self {
Self { cost: 10 }
}
pub fn with_cost(cost: u32) -> Self {
assert!((4..=31).contains(&cost), "BCrypt cost must be between 4 and 31");
Self { cost }
}
}
impl Default for BcryptPasswordEncoder {
fn default() -> Self {
Self::new()
}
}
impl PasswordEncoder for BcryptPasswordEncoder {
fn encode(&self, raw: &str) -> String {
bcrypt::hash(raw, self.cost)
.expect("BCrypt encoding failed — this is a fatal error, not a condition for fallback")
}
fn matches(&self, raw: &str, encoded: &str) -> bool {
bcrypt::verify(raw, encoded).unwrap_or(false)
}
fn upgrade_encoding(&self, encoded: &str) -> bool {
if let Some(prefix) = encoded.split('$').nth(2)
&& let Ok(cost) = prefix.parse::<u32>()
{
return cost != self.cost;
}
true
}
}
pub struct NoOpPasswordEncoder;
impl PasswordEncoder for NoOpPasswordEncoder {
fn encode(&self, raw: &str) -> String {
raw.to_string()
}
fn matches(&self, raw: &str, encoded: &str) -> bool {
raw == encoded
}
}
pub struct StandardPasswordEncoder {
encoder: Box<dyn PasswordEncoder + Send + Sync>,
}
impl Clone for StandardPasswordEncoder {
fn clone(&self) -> Self {
Self {
encoder: Box::new(BcryptPasswordEncoder::new()),
}
}
}
impl std::fmt::Debug for StandardPasswordEncoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StandardPasswordEncoder").finish()
}
}
impl StandardPasswordEncoder {
pub fn new() -> Self {
Self {
encoder: Box::new(BcryptPasswordEncoder::new()),
}
}
pub fn bcrypt() -> Self {
Self {
encoder: Box::new(BcryptPasswordEncoder::new()),
}
}
pub fn custom(encoder: Box<dyn PasswordEncoder + Send + Sync>) -> Self {
Self { encoder }
}
}
impl Default for StandardPasswordEncoder {
fn default() -> Self {
Self::new()
}
}
impl PasswordEncoder for StandardPasswordEncoder {
fn encode(&self, raw: &str) -> String {
self.encoder.encode(raw)
}
fn matches(&self, raw: &str, encoded: &str) -> bool {
self.encoder.matches(raw, encoded)
}
}
pub struct Pbkdf2PasswordEncoder {
iterations: u32,
key_length: usize,
salt_length: usize,
}
impl Pbkdf2PasswordEncoder {
pub fn new() -> Self {
Self {
iterations: 100_000,
key_length: 32,
salt_length: 16,
}
}
pub fn with_iterations(iterations: u32) -> Self {
Self {
iterations,
..Default::default()
}
}
}
impl Default for Pbkdf2PasswordEncoder {
fn default() -> Self {
Self::new()
}
}
impl PasswordEncoder for Pbkdf2PasswordEncoder {
fn encode(&self, raw: &str) -> String {
use hmac::Hmac;
use hmac::Mac;
use rand::Rng;
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let salt: Vec<u8> = (0..self.salt_length)
.map(|_| rand::rng().random())
.collect();
let hash_len = 32; let blocks_needed = (self.key_length + hash_len - 1) / hash_len;
let mut dk = Vec::with_capacity(blocks_needed * hash_len);
for block_idx in 1..=blocks_needed {
let mut mac =
HmacSha256::new_from_slice(raw.as_bytes()).expect("HMAC accepts any key length");
mac.update(&salt);
mac.update(&(block_idx as u32).to_be_bytes());
let mut u = mac.finalize().into_bytes();
let mut result = u.clone();
for _ in 1..self.iterations {
let mut mac = HmacSha256::new_from_slice(raw.as_bytes())
.expect("HMAC accepts any key length");
mac.update(&u);
u = mac.finalize().into_bytes();
for (r, u_byte) in result.iter_mut().zip(u.iter()) {
*r ^= u_byte;
}
}
dk.extend_from_slice(&result);
}
dk.truncate(self.key_length);
format!("{}${}${}", self.iterations, hex::encode(&salt), hex::encode(&dk))
}
fn matches(&self, raw: &str, encoded: &str) -> bool {
let parts: Vec<&str> = encoded.split('$').collect();
if parts.len() != 3 {
return false;
}
let iterations: u32 = match parts[0].parse() {
Ok(i) => i,
Err(_) => return false,
};
let salt = match hex::decode(parts[1]) {
Ok(s) => s,
Err(_) => return false,
};
let expected_key = match hex::decode(parts[2]) {
Ok(k) => k,
Err(_) => return false,
};
use hmac::Hmac;
use hmac::Mac;
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let hash_len = 32;
let blocks_needed = (expected_key.len() + hash_len - 1) / hash_len;
let mut dk = Vec::with_capacity(blocks_needed * hash_len);
for block_idx in 1..=blocks_needed {
let mut mac =
HmacSha256::new_from_slice(raw.as_bytes()).expect("HMAC accepts any key length");
mac.update(&salt);
mac.update(&(block_idx as u32).to_be_bytes());
let mut u = mac.finalize().into_bytes();
let mut result = u.clone();
for _ in 1..iterations {
let mut mac = HmacSha256::new_from_slice(raw.as_bytes())
.expect("HMAC accepts any key length");
mac.update(&u);
u = mac.finalize().into_bytes();
for (r, u_byte) in result.iter_mut().zip(u.iter()) {
*r ^= u_byte;
}
}
dk.extend_from_slice(&result);
}
dk.truncate(expected_key.len());
use subtle::ConstantTimeEq;
dk.ct_eq(&expected_key).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bcrypt_encoder() {
let encoder = BcryptPasswordEncoder::new();
let hash = encoder.encode("password");
assert!(encoder.matches("password", &hash));
assert!(!encoder.matches("wrong", &hash));
}
#[test]
fn test_noop_encoder() {
let encoder = NoOpPasswordEncoder;
assert_eq!(encoder.encode("password"), "password");
assert!(encoder.matches("password", "password"));
}
}