diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-08-24 01:12:06 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-08-24 04:50:49 -0400 |
| commit | fd6a74e8ca1f5ded2a760b8ac644124862d80d54 (patch) | |
| tree | 1da22665c0259c3a4ed63bb7acef4a6e81365dbe /src/password.rs | |
| parent | 97e4c1d25f6ee17959adc23cacd8361dcd42e519 (diff) | |
Hoist `password` out to the top level.
Having this buried under `crate::user` makes it hard to split up the roles `user` fulfils right now. Moving it out to its own module makes it a bit tidier to reuse it in a separate, authentication-only way.
Diffstat (limited to 'src/password.rs')
| -rw-r--r-- | src/password.rs | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/src/password.rs b/src/password.rs new file mode 100644 index 0000000..6c189fc --- /dev/null +++ b/src/password.rs @@ -0,0 +1,67 @@ +use std::fmt; + +use argon2::Argon2; +use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use rand_core::OsRng; + +use crate::normalize::nfc; + +#[derive(Clone, Eq, PartialEq, sqlx::Type)] +#[sqlx(transparent)] +pub struct StoredHash(String); + +impl StoredHash { + pub fn verify(&self, candidate: &Password) -> Result<bool, password_hash::Error> { + let hash = PasswordHash::new(&self.0)?; + let candidate = candidate.as_bytes(); + let algorithm = Argon2::default(); + + match algorithm.verify_password(candidate, &hash) { + // Successful authentication, not an error + Ok(()) => Ok(true), + // Unsuccessful authentication, also not an error + Err(password_hash::errors::Error::Password) => Ok(false), + // Password validation failed for some other reason, treat as an error + Err(err) => Err(err), + } + } +} + +impl fmt::Debug for StoredHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("StoredHash").field(&"********").finish() + } +} + +#[derive(Clone, serde::Deserialize)] +#[serde(transparent)] +pub struct Password(nfc::String); + +impl Password { + pub fn hash(&self) -> Result<StoredHash, password_hash::Error> { + let Self(password) = self; + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string(); + Ok(StoredHash(hash)) + } + + fn as_bytes(&self) -> &[u8] { + let Self(value) = self; + value.as_bytes() + } +} + +impl fmt::Debug for Password { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Password").field(&"********").finish() + } +} + +impl From<String> for Password { + fn from(password: String) -> Self { + Password(password.into()) + } +} |
