use std::fmt;
#[derive(Debug)]
pub enum HashError {
Backend(String),
InvalidHash,
}
impl fmt::Display for HashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HashError::Backend(m) => write!(f, "hash backend error: {}", m),
HashError::InvalidHash => write!(f, "stored hash is malformed"),
}
}
}
impl std::error::Error for HashError {}
pub trait PasswordHasher: Send + Sync {
fn hash(&self, plaintext: &str) -> Result<String, HashError>;
fn verify(&self, plaintext: &str, stored: &str) -> Result<bool, HashError>;
fn needs_rehash(&self, _stored: &str) -> bool {
false
}
}
use argon2::password_hash::{PasswordHash, PasswordHasher as _, PasswordVerifier, SaltString};
use argon2::password_hash::rand_core::OsRng;
use argon2::Argon2;
use crate::hasher::{HashError, PasswordHasher};
pub struct Argon2Hasher {
inner: Argon2<'static>,
}
impl Argon2Hasher {
pub fn new() -> Self {
Argon2Hasher { inner: Argon2::default() }
}
}
impl PasswordHasher for Argon2Hasher {
fn hash(&self, plaintext: &str) -> Result<String, HashError> {
let salt = SaltString::generate(&mut OsRng);
self.inner
.hash_password(plaintext.as_bytes(), &salt)
.map(|h| h.to_string())
.map_err(|e| HashError::Backend(e.to_string()))
}
fn verify(&self, plaintext: &str, stored: &str) -> Result<bool, HashError> {
let parsed = PasswordHash::new(stored).map_err(|_| HashError::InvalidHash)?;
Ok(self.inner.verify_password(plaintext.as_bytes(), &parsed).is_ok())
}
fn needs_rehash(&self, stored: &str) -> bool {
// Anything not already in Argon2 PHC form should be upgraded.
PasswordHash::new(stored)
.map(|h| h.algorithm.as_str() != "argon2id")
.unwrap_or(true)
}
}
pub struct BcryptHasher {
cost: u32,
}
impl BcryptHasher {
pub fn new(cost: u32) -> Self {
BcryptHasher { cost }
}
}
impl PasswordHasher for BcryptHasher {
fn hash(&self, plaintext: &str) -> Result<String, HashError> {
bcrypt::hash(plaintext, self.cost).map_err(|e| HashError::Backend(e.to_string()))
}
fn verify(&self, plaintext: &str, stored: &str) -> Result<bool, HashError> {
bcrypt::verify(plaintext, stored).map_err(|_| HashError::InvalidHash)
}
}
use crate::hasher::{HashError, PasswordHasher};
pub struct Credentials {
pub user_id: i64,
pub password_hash: String,
}
pub trait CredentialStore {
fn find(&self, email: &str) -> Option<Credentials>;
fn update_hash(&self, user_id: i64, new_hash: &str);
}
pub struct AuthService<S: CredentialStore> {
hasher: Box<dyn PasswordHasher>,
store: S,
}
impl<S: CredentialStore> AuthService<S> {
pub fn new(hasher: Box<dyn PasswordHasher>, store: S) -> Self {
AuthService { hasher, store }
}
pub fn register(&self, user_id: i64, plaintext: &str) -> Result<String, HashError> {
self.hasher.hash(plaintext)
}
pub fn authenticate(&self, email: &str, plaintext: &str) -> Result<Option<i64>, HashError> {
let creds = match self.store.find(email) {
Some(c) => c,
None => return Ok(None),
};
if !self.hasher.verify(plaintext, &creds.password_hash)? {
return Ok(None);
}
if self.hasher.needs_rehash(&creds.password_hash) {
let upgraded = self.hasher.hash(plaintext)?;
self.store.update_hash(creds.user_id, &upgraded);
}
Ok(Some(creds.user_id))
}
}
Password hashing algorithms change over time: bcrypt was fine for years, Argon2 is the current recommendation, and whatever replaces it will arrive eventually. Hard-coding a single algorithm into an authentication service makes migration painful, so this snippet defines a PasswordHasher trait as a stable seam between the auth logic and the concrete hashing backend.
In hasher.rs, the PasswordHasher trait declares two methods: hash produces a self-describing PHC-format string, and verify checks a plaintext candidate against a stored hash. The trait is object-safe, so it can be stored behind a Box<dyn PasswordHasher> and swapped at runtime or in tests. HashError wraps the failure modes into one type via From conversions, keeping call sites clean. Returning a bool from verify rather than a Result<()> distinguishes a genuine mismatch from an operational error like a corrupt hash string.
backends.rs provides two implementations. Argon2Hasher uses the argon2 crate with a random salt from OsRng and encodes parameters into the output, while BcryptHasher wraps the bcrypt crate with a configurable cost. Because each hash string embeds its own algorithm identifier and parameters, verify can validate hashes produced under older settings — critical for rolling password migrations where existing users keep their bcrypt hashes until their next login.
auth_service.rs shows the payoff. AuthService holds a boxed hasher and never names a concrete algorithm. authenticate looks up the stored hash and delegates to verify; on success it calls needs_rehash to detect hashes made by a weaker or outdated scheme and transparently upgrades them using the current hasher. This is the standard pattern for phasing in a stronger algorithm without forcing a password reset.
The trade-off is a small amount of dynamic dispatch and the discipline of a self-describing hash format, in exchange for testability and painless algorithm migration. A pitfall to avoid is comparing hashes with ==; verification must always go through the algorithm's constant-time check, which both backends here delegate to their crates.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
package com.example.myapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
Dependency injection with Hilt
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.