feat: per-profile DEKs + multi-profile (Phase A2, #3) #9

Merged
alvaro merged 2 commits from feat/3-phase-a2-per-profile-deks into main 2026-07-19 09:46:54 +00:00
Showing only changes of commit 706df11f15 - Show all commits

View file

@ -1,6 +1,9 @@
use anyhow::Result;
use pbkdf2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
password_hash::{
rand_core::OsRng, Error as PasswordHashError, PasswordHash, PasswordHasher,
PasswordVerifier, SaltString,
},
Pbkdf2,
};
@ -15,14 +18,24 @@ impl PasswordService {
Ok(password_hash.to_string())
}
/// Verify a password against a stored PHC string.
///
/// Returns `Ok(true)` on a match, `Ok(false)` on a mismatch (wrong
/// password — an expected outcome, not an error), and `Err(...)` only for
/// genuine failures (unparseable hash, malformed input, etc.). Returning
/// `Ok(false)` for a wrong password lets the login handler distinguish
/// "invalid credentials" (401) from a real backend fault (500).
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| anyhow::anyhow!("Failed to parse password hash: {}", e))?;
Pbkdf2
.verify_password(password.as_bytes(), &parsed_hash)
.map(|_| true)
.map_err(|e| anyhow::anyhow!("Password verification failed: {}", e))
match Pbkdf2.verify_password(password.as_bytes(), &parsed_hash) {
Ok(()) => Ok(true),
// `Error::Password` is the password-hash crate's signal for a
// mismatch — the one expected "failure" during normal auth.
Err(PasswordHashError::Password) => Ok(false),
Err(e) => Err(anyhow::anyhow!("Password verification failed: {}", e)),
}
}
}