use std::sync::Arc;
pub trait LegacyVerifier: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn recognises(&self, hashed: &str) -> bool;
fn verify(&self, plain: &str, hashed: &str) -> bool;
}
#[derive(Clone, Default)]
pub(crate) struct LegacySchemes {
verifiers: Vec<Arc<dyn LegacyVerifier>>,
}
impl LegacySchemes {
pub(crate) fn push(&mut self, verifier: Arc<dyn LegacyVerifier>) {
self.verifiers.push(verifier);
}
pub(crate) fn matching(&self, hashed: &str) -> Option<&Arc<dyn LegacyVerifier>> {
self.verifiers.iter().find(|verifier| verifier.recognises(hashed))
}
pub(crate) fn names(&self) -> Vec<&'static str> {
self.verifiers.iter().map(|verifier| verifier.name()).collect()
}
}
impl std::fmt::Debug for LegacySchemes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.names()).finish()
}
}
#[cfg(feature = "bcrypt")]
#[derive(Debug, Clone, Copy, Default)]
pub struct BcryptVerifier;
#[cfg(feature = "bcrypt")]
impl LegacyVerifier for BcryptVerifier {
fn name(&self) -> &'static str {
"bcrypt"
}
fn recognises(&self, hashed: &str) -> bool {
hashed.starts_with("$2a$") || hashed.starts_with("$2b$") || hashed.starts_with("$2y$")
}
fn verify(&self, plain: &str, hashed: &str) -> bool {
bcrypt::verify(plain, hashed).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Reversed;
impl LegacyVerifier for Reversed {
fn name(&self) -> &'static str {
"reversed"
}
fn recognises(&self, hashed: &str) -> bool {
hashed.starts_with("rev$")
}
fn verify(&self, plain: &str, hashed: &str) -> bool {
hashed
.strip_prefix("rev$")
.is_some_and(|stored| stored.chars().rev().collect::<String>() == plain)
}
}
#[test]
fn the_first_scheme_that_recognises_it_wins() {
let mut schemes = LegacySchemes::default();
schemes.push(Arc::new(Reversed));
assert!(schemes.matching("rev$drowssap").is_some());
assert!(schemes.matching("$argon2id$v=19$...").is_none());
assert_eq!(schemes.names(), vec!["reversed"]);
}
#[test]
fn an_empty_set_recognises_nothing() {
let schemes = LegacySchemes::default();
assert!(schemes.names().is_empty());
assert!(schemes.matching("rev$drowssap").is_none());
}
#[cfg(feature = "bcrypt")]
#[test]
fn bcrypt_reads_all_three_prefixes_and_nothing_else() {
let verifier = BcryptVerifier;
for prefix in ["$2a$", "$2b$", "$2y$"] {
assert!(verifier.recognises(&format!("{prefix}10$abcdefg")), "{prefix}");
}
assert!(!verifier.recognises("$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA"));
assert!(!verifier.recognises(""));
assert!(!verifier.recognises("$2$10$old"), "the 1999 prefix is not one of the three");
}
#[cfg(feature = "bcrypt")]
#[test]
fn bcrypt_verifies_a_hash_php_would_have_written() {
let hashed = bcrypt::hash("correct horse", 4).unwrap();
let php_style = hashed.replacen("$2b$", "$2y$", 1);
assert!(BcryptVerifier.verify("correct horse", &php_style));
assert!(!BcryptVerifier.verify("wrong horse", &php_style));
}
#[cfg(feature = "bcrypt")]
#[test]
fn a_malformed_bcrypt_hash_is_false_rather_than_a_panic() {
assert!(!BcryptVerifier.verify("anything", "$2y$not-a-hash"));
assert!(!BcryptVerifier.verify("anything", "$2y$"));
}
}