use bytes::BytesMut;
use postgres_types::{FromSql, IsNull, ToSql, Type};
#[cfg(feature = "auth-argon2")]
use crate::auth::AuthError;
#[derive(Debug, Clone, Default)]
pub struct PasswordHash(String);
impl crate::descriptor::DjogiSqlType for PasswordHash {
const SQL_TYPE: &'static str = "TEXT";
}
impl PasswordHash {
#[cfg(feature = "auth-argon2")]
pub fn hash(plaintext: &str) -> Result<Self, AuthError> {
use argon2::password_hash::SaltString;
use argon2::password_hash::rand_core::OsRng;
use argon2::{Argon2, PasswordHasher};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let phc = argon2
.hash_password(plaintext.as_bytes(), &salt)
.map_err(|e| AuthError::Provider(Box::new(e)))?
.to_string();
Ok(Self(phc))
}
pub fn verify(&self, plaintext: &str) -> bool {
#[cfg(feature = "auth-argon2")]
{
use argon2::password_hash::PasswordHash as Phc;
use argon2::{Argon2, PasswordVerifier};
if let Ok(parsed) = Phc::new(&self.0) {
return Argon2::default()
.verify_password(plaintext.as_bytes(), &parsed)
.is_ok();
}
false
}
#[cfg(not(feature = "auth-argon2"))]
{
let _ = plaintext;
false
}
}
pub fn as_phc(&self) -> &str {
&self.0
}
pub fn from_phc(phc: impl Into<String>) -> Self {
Self(phc.into())
}
}
impl ToSql for PasswordHash {
fn to_sql(
&self,
ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
self.0.to_sql(ty, out)
}
fn accepts(ty: &Type) -> bool {
<String as ToSql>::accepts(ty)
}
postgres_types::to_sql_checked!();
}
impl<'a> FromSql<'a> for PasswordHash {
fn from_sql(
ty: &Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
<String as FromSql>::from_sql(ty, raw).map(Self)
}
fn accepts(ty: &Type) -> bool {
<String as FromSql>::accepts(ty)
}
}
#[cfg(all(test, feature = "auth-argon2"))]
mod tests {
use super::*;
#[test]
fn password_hash_round_trips() {
let h = PasswordHash::hash("s3cret").unwrap();
assert!(h.verify("s3cret"));
assert!(!h.verify("wrong"));
assert!(h.as_phc().starts_with("$argon2id$"));
}
#[test]
fn password_hash_verify_returns_false_on_malformed_phc() {
let h = PasswordHash::from_phc("not-a-real-phc-string");
assert!(!h.verify("anything"));
}
#[test]
fn password_hash_is_clone_debug() {
let h = PasswordHash::hash("s3cret").unwrap();
let h2 = h.clone();
assert_eq!(h.as_phc(), h2.as_phc());
assert!(format!("{h:?}").contains("PasswordHash"));
}
}