b453d5d
use std::time::{SystemTime, UNIX_EPOCH};
use aes_gcm::Aes256Gcm;
use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng};
use argon2::{Argon2, PasswordHasher, PasswordVerifier};
use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use config::auth::PasswordPolicy;
use rand::Rng;
use sha2::{Digest, Sha256};
use crate::errors::AuthError;
pub fn hash_password(password: &str) -> Result<String, AuthError> {
Argon2::default()
.hash_password(password.as_bytes())
.map(|hash| hash.to_string())
.map_err(|error| AuthError::Internal(format!("password hashing failed: {error}")))
}
pub fn verify_password(password: &str, hash: &str) -> bool {
Argon2::default()
.verify_password(password.as_bytes(), hash)
.is_ok()
}
pub fn validate_password(password: &str, policy: &PasswordPolicy) -> Result<(), AuthError> {
if password.len() < policy.min_length {
return Err(AuthError::BadRequest(format!(
"password must be at least {} characters",
policy.min_length
)));
}
if policy.require_upper && !password.chars().any(|c| c.is_ascii_uppercase()) {
return Err(AuthError::BadRequest(
"password must contain an uppercase letter".to_string(),
));
}
if policy.require_lower && !password.chars().any(|c| c.is_ascii_lowercase()) {
return Err(AuthError::BadRequest(
"password must contain a lowercase letter".to_string(),
));
}
if policy.require_digit && !password.chars().any(|c| c.is_ascii_digit()) {
return Err(AuthError::BadRequest(
"password must contain a digit".to_string(),
));
}
if policy.require_symbol && !password.chars().any(|c| !c.is_ascii_alphanumeric()) {
return Err(AuthError::BadRequest(
"password must contain a symbol".to_string(),
));
}
Ok(())
}
pub fn generate_numeric_code(len: usize) -> String {
let mut rng = rand::thread_rng();
(0..len)
.map(|_| rng.gen_range(0..10u32).to_string())
.collect()
}
pub fn random_bytes(len: usize) -> Vec<u8> {
let mut bytes = vec![0u8; len];
rand::thread_rng().fill(&mut bytes[..]);
bytes
}
pub fn random_token(len: usize) -> String {
URL_SAFE_NO_PAD.encode(random_bytes(len))
}
pub fn sha256_b64url(data: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(Sha256::digest(data))
}
pub fn sha256_hex(data: &[u8]) -> String {
Sha256::digest(data)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub fn aes_encrypt(key_material: &str, plaintext: &str) -> Result<String, AuthError> {
let key = aes_key(key_material);
let cipher = Aes256Gcm::new((&key).into());
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, plaintext.as_bytes())
.map_err(|error| {
tracing::error!(%error, "aes encryption failed");
AuthError::Internal("aes encryption failed".to_string())
})?;
let mut output = Vec::with_capacity(nonce.len() + ciphertext.len());
output.extend_from_slice(&nonce);
output.extend_from_slice(&ciphertext);
Ok(STANDARD.encode(output))
}
pub fn aes_decrypt(key_material: &str, stored: &str) -> Result<String, AuthError> {
let bytes = STANDARD.decode(stored).map_err(|error| {
tracing::error!(%error, "malformed encrypted value");
AuthError::Internal("malformed encrypted value".to_string())
})?;
if bytes.len() < 12 {
return Err(AuthError::Internal("malformed encrypted value".to_string()));
}
let (nonce_bytes, ciphertext) = bytes.split_at(12);
let Ok(nonce_arr): Result<&[u8; 12], _> = nonce_bytes.try_into() else {
return Err(AuthError::Internal("malformed encrypted value".to_string()));
};
let key = aes_key(key_material);
let cipher = Aes256Gcm::new((&key).into());
let plaintext = cipher
.decrypt(nonce_arr.into(), ciphertext)
.map_err(|error| {
tracing::error!(%error, "aes decryption failed");
AuthError::Internal("aes decryption failed".to_string())
})?;
String::from_utf8(plaintext).map_err(|error| {
tracing::error!(%error, "decrypted value is not utf8");
AuthError::Internal("decrypted value is not utf8".to_string())
})
}
fn aes_key(material: &str) -> [u8; 32] {
Sha256::digest(material.as_bytes()).into()
}
pub fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or(0)
}