Skip to main content

aster_forge_crypto/
hash.rs

1//! Password hashing and digest helpers.
2//!
3//! Passwords are hashed with Argon2 and a fresh salt for storage, while SHA-256 helpers cover the
4//! deterministic digest cases used by services. The module keeps formatting helpers centralized so
5//! hex output stays lowercase and stable across call sites.
6
7use crate::{CryptoError, Result};
8use argon2::{
9    Argon2,
10    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
11};
12use rand_core_06::OsRng;
13use sha2::{Digest, Sha256};
14use std::fmt::Write;
15
16fn password_hasher() -> Argon2<'static> {
17    Argon2::default()
18}
19
20/// Hashes a password with Argon2 and a fresh random salt.
21pub fn hash_password(password: &str) -> Result<String> {
22    let salt = SaltString::generate(&mut OsRng);
23    password_hasher()
24        .hash_password(password.as_bytes(), &salt)
25        .map(|h| h.to_string())
26        .map_err(CryptoError::password_hash)
27}
28
29/// Verifies a password against a stored Argon2 password hash.
30pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
31    let parsed = PasswordHash::new(hash).map_err(CryptoError::password_hash)?;
32    Ok(password_hasher()
33        .verify_password(password.as_bytes(), &parsed)
34        .is_ok())
35}
36
37/// Computes the SHA-256 digest of `data` and returns lowercase hex.
38pub fn sha256_hex(data: &[u8]) -> String {
39    let mut hasher = Sha256::new();
40    hasher.update(data);
41    bytes_to_hex(&hasher.finalize())
42}
43
44/// Encodes arbitrary bytes as lowercase hex.
45pub fn bytes_to_hex(bytes: &[u8]) -> String {
46    let mut hex = String::with_capacity(bytes.len() * 2);
47    for byte in bytes {
48        let _ = write!(&mut hex, "{byte:02x}");
49    }
50    hex
51}
52
53/// Encodes a SHA-256 digest as lowercase hex.
54pub fn sha256_digest_to_hex(digest: &[u8]) -> String {
55    bytes_to_hex(digest)
56}
57
58/// Creates a new incremental SHA-256 hasher.
59pub fn new_sha256() -> Sha256 {
60    Sha256::new()
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use sha2::Digest;
67
68    #[test]
69    fn sha256_hex_matches_known_vector() {
70        assert_eq!(
71            sha256_hex(b"abc"),
72            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
73        );
74    }
75
76    #[test]
77    fn bytes_to_hex_encodes_lowercase_and_preserves_leading_zeroes() {
78        assert_eq!(bytes_to_hex(&[0x00, 0x0f, 0x10, 0xab, 0xff]), "000f10abff");
79    }
80
81    #[test]
82    fn sha256_digest_to_hex_matches_incremental_hasher_output() {
83        let mut hasher = new_sha256();
84        hasher.update(b"a");
85        hasher.update(b"bc");
86        assert_eq!(sha256_digest_to_hex(&hasher.finalize()), sha256_hex(b"abc"));
87    }
88
89    #[test]
90    fn password_hash_verifies_matching_password_and_rejects_wrong_password() {
91        let hash = hash_password("correct horse battery staple").unwrap();
92
93        assert!(verify_password("correct horse battery staple", &hash).unwrap());
94        assert!(!verify_password("wrong password", &hash).unwrap());
95        assert!(hash.starts_with("$argon2"));
96    }
97
98    #[test]
99    fn password_hash_uses_fresh_salt() {
100        let first = hash_password("same password").unwrap();
101        let second = hash_password("same password").unwrap();
102
103        assert_ne!(first, second);
104        assert!(verify_password("same password", &first).unwrap());
105        assert!(verify_password("same password", &second).unwrap());
106    }
107
108    #[test]
109    fn malformed_password_hash_returns_error() {
110        let error = verify_password("password", "not-a-password-hash").unwrap_err();
111
112        assert!(matches!(error, CryptoError::PasswordHash(_)));
113    }
114}