Files
claudetools/projects/msp-tools/guru-connect/server/src/auth/password.rs
Mike Swanson 58e5d436e3 Week 1 Day 2-3: Complete remaining security fixes (SEC-6 through SEC-13)
Security Improvements:
- SEC-6: Remove password logging - write to secure file instead
- SEC-7: Add CSP headers for XSS prevention
- SEC-9: Explicitly configure Argon2id password hashing
- SEC-11: Restrict CORS to specific origins (production + localhost)
- SEC-12: Implement comprehensive security headers
- SEC-13: Explicit JWT expiration enforcement

Completed Features:
✓ Password credentials written to .admin-credentials file (600 permissions)
✓ CSP headers prevent XSS attacks
✓ Argon2id explicitly configured (Algorithm::Argon2id)
✓ CORS restricted to connect.azcomputerguru.com + localhost
✓ Security headers: X-Frame-Options, X-Content-Type-Options, etc.
✓ JWT expiration strictly enforced (validate_exp=true, leeway=0)

Files Created:
- server/src/middleware/security_headers.rs
- WEEK1_DAY2-3_SECURITY_COMPLETE.md

Files Modified:
- server/src/main.rs (password file write, CORS, security headers)
- server/src/auth/jwt.rs (explicit expiration validation)
- server/src/auth/password.rs (explicit Argon2id)
- server/src/middleware/mod.rs (added security_headers)

Week 1 Progress: 10/13 items complete (77%)
Compilation: SUCCESS (53 warnings, 0 errors)
Risk Level: CRITICAL → LOW/MEDIUM

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 19:35:59 -07:00

77 lines
2.5 KiB
Rust

//! Password hashing using Argon2id
//!
//! SEC-9: Explicitly uses Argon2id (hybrid variant) for password hashing
//! Argon2id provides resistance against both side-channel and GPU attacks
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2, Algorithm, Version, Params,
};
/// Hash a password using Argon2id
///
/// SEC-9: Explicitly configured to use Argon2id variant
/// - Algorithm: Argon2id (hybrid of Argon2i and Argon2d)
/// - Version: 0x13 (latest version)
/// - Memory: 19456 KiB (default)
/// - Iterations: 2 (default)
/// - Parallelism: 1 (default)
pub fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
// Explicitly use Argon2id (Algorithm::Argon2id)
let argon2 = Argon2::new(
Algorithm::Argon2id, // SEC-9: Explicit Argon2id variant
Version::V0x13, // Latest version
Params::default(), // Default params (19456 KiB, 2 iterations, 1 parallelism)
);
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow!("Failed to hash password: {}", e))?;
Ok(hash.to_string())
}
/// Verify a password against a stored hash
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| anyhow!("Invalid password hash format: {}", e))?;
// Argon2::default() uses Argon2id, but we verify against the hash's embedded algorithm
let argon2 = Argon2::default();
Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
/// Generate a random password (for initial admin)
pub fn generate_random_password(length: usize) -> String {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%";
let mut rng = rand::thread_rng();
(0..length)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_and_verify() {
let password = "test_password_123";
let hash = hash_password(password).unwrap();
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong_password", &hash).unwrap());
}
#[test]
fn test_random_password() {
let password = generate_random_password(16);
assert_eq!(password.len(), 16);
}
}