Private
Public Access
1
0
Files
linux_patch_manager/crates/pm-auth/src/password.rs
Echo 6c72dc3ac6
Some checks failed
CI Pipeline / Rust Format Check (push) Failing after 2s
CI Pipeline / Clippy Lints (push) Failing after 1s
CI Pipeline / Rust Unit Tests (push) Failing after 2s
CI Pipeline / Security Audit (push) Failing after 2s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped
feat: populate os_family, os_name, arch, agent_version from health poller and enrollment
- health_poller: persist agent_version from HealthData.version
- health_poller: call /system/info to update os_family, os_name, arch
- enrollment: set os_family and arch from os_details during approval
- enrollment: build os_name from os+os_version when name field absent
- COALESCE in UPDATE preserves existing values when new data unavailable
- version bump 0.1.7 -> 0.1.8
2026-05-21 00:09:57 +00:00

126 lines
4.0 KiB
Rust
Executable File

//! Password hashing and verification using Argon2id.
//!
//! Parameters (calibrated per OWASP recommendations):
//! - Algorithm: Argon2id
//! - Memory cost: 65536 KiB (64 MiB)
//! - Time cost: 3 iterations
//! - Parallelism: 1
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2, Params, Version,
};
use thiserror::Error;
/// Argon2id parameters per spec.
const M_COST: u32 = 65536; // 64 MiB
const T_COST: u32 = 3; // 3 iterations
const P_COST: u32 = 1; // 1 thread
#[derive(Debug, Error)]
pub enum PasswordError {
#[error("Failed to hash password: {0}")]
HashError(String),
#[error("Failed to verify password: {0}")]
VerifyError(String),
#[error("Invalid password hash format")]
InvalidHash,
}
/// Build an Argon2id instance with calibrated parameters.
fn argon2() -> Result<Argon2<'static>, PasswordError> {
let params = Params::new(M_COST, T_COST, P_COST, None)
.map_err(|e| PasswordError::HashError(e.to_string()))?;
Ok(Argon2::new(
argon2::Algorithm::Argon2id,
Version::V0x13,
params,
))
}
/// Hash a plaintext password using Argon2id with a random salt.
///
/// Returns the PHC string format hash suitable for storage.
pub fn hash_password(password: &str) -> Result<String, PasswordError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = argon2()?;
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| PasswordError::HashError(e.to_string()))?;
Ok(hash.to_string())
}
/// Verify a plaintext password against a stored Argon2id PHC hash.
///
/// Returns `Ok(true)` if the password matches, `Ok(false)` if not.
pub fn verify_password(password: &str, hash: &str) -> Result<bool, PasswordError> {
let parsed_hash = PasswordHash::new(hash).map_err(|_| PasswordError::InvalidHash)?;
let argon2 = argon2()?;
match argon2.verify_password(password.as_bytes(), &parsed_hash) {
Ok(()) => Ok(true),
Err(argon2::password_hash::Error::Password) => Ok(false),
Err(e) => Err(PasswordError::VerifyError(e.to_string())),
}
}
/// Validate password strength against minimum requirements.
///
/// Requirements:
/// - Minimum 8 characters
/// - At least one uppercase letter
/// - At least one lowercase letter
/// - At least one digit
/// - At least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?)
pub fn validate_password_strength(password: &str) -> Result<(), String> {
if password.len() < 8 {
return Err("Password must be at least 8 characters".to_string());
}
if !password.chars().any(|c| c.is_ascii_uppercase()) {
return Err("Password must contain at least one uppercase letter".to_string());
}
if !password.chars().any(|c| c.is_ascii_lowercase()) {
return Err("Password must contain at least one lowercase letter".to_string());
}
if !password.chars().any(|c| c.is_ascii_digit()) {
return Err("Password must contain at least one digit".to_string());
}
let special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
if !password.chars().any(|c| special_chars.contains(c)) {
return Err(
"Password must contain at least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?)"
.to_string(),
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_and_verify_roundtrip() {
let password = "super-secret-password-123!";
let hash = hash_password(password).unwrap();
assert!(hash.starts_with("$argon2id$"));
assert!(verify_password(password, &hash).unwrap());
}
#[test]
fn wrong_password_fails() {
let hash = hash_password("correct-horse").unwrap();
assert!(!verify_password("wrong-password", &hash).unwrap());
}
#[test]
fn different_salts_produce_different_hashes() {
let hash1 = hash_password("same-password").unwrap();
let hash2 = hash_password("same-password").unwrap();
assert_ne!(hash1, hash2); // different salts
}
}