aster_forge_crypto/
hash.rs1use 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
20pub 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
29pub 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
37pub fn sha256_hex(data: &[u8]) -> String {
39 let mut hasher = Sha256::new();
40 hasher.update(data);
41 bytes_to_hex(&hasher.finalize())
42}
43
44pub 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
53pub fn sha256_digest_to_hex(digest: &[u8]) -> String {
55 bytes_to_hex(digest)
56}
57
58pub 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}