tuwunel_core/utils/hash/
argon.rs1use std::fmt::Display;
2
3use argon2::{
4 Algorithm, Argon2, Error as Argon2Error, Params, PasswordHasher, PasswordVerifier, Version,
5 password_hash::phc::Salt,
6};
7use rand::random;
8use smallstr::SmallString;
9
10use crate::{Error, Result, err, format_small_string, implement};
11
12pub type PhcString = SmallString<[u8; 112]>;
17
18#[derive(Clone, Copy, Debug)]
24pub struct Cost {
25 pub m_cost: u32,
27
28 pub t_cost: u32,
30
31 pub p_cost: u32,
33}
34
35pub fn password(password: &str, cost: Cost) -> Result<PhcString> {
41 let salt: [u8; Salt::RECOMMENDED_LENGTH] = random();
42
43 hasher(cost)
44 .map_err(map_err)?
45 .hash_password_with_salt(password.as_bytes(), &salt)
46 .map(|hash| format_small_string!("{hash}"))
47 .map_err(map_err)
48}
49
50pub fn verify_password(password: &str, password_hash: &str) -> Result {
56 Argon2::default()
57 .verify_password(password.as_bytes(), password_hash)
58 .map_err(map_err)
59}
60
61#[implement(Cost)]
67pub fn check(self) -> Result<(), Argon2Error> { hasher(self).map(|_| ()) }
68
69#[implement(Cost)]
78#[must_use]
79pub fn is_recommended(self) -> bool {
80 const MIN_M_COST: u64 = 7168;
81 const MIN_PRODUCT: u64 = MIN_M_COST * 5;
82
83 let m_cost = u64::from(self.m_cost);
84 let product = m_cost.saturating_mul(u64::from(self.t_cost));
85
86 m_cost >= MIN_M_COST && product >= MIN_PRODUCT
87}
88
89fn hasher(Cost { m_cost, t_cost, p_cost }: Cost) -> Result<Argon2<'static>, Argon2Error> {
90 let out_len: Option<usize> = None;
91
92 Params::new(m_cost, t_cost, p_cost, out_len)
93 .map(|params| Argon2::new(Algorithm::Argon2id, Version::default(), params))
94}
95
96fn map_err<E: Display>(e: E) -> Error { err!("{e}") }
97
98#[cfg(test)]
99mod tests {
100 use argon2::Params;
101
102 use super::{Cost, password, verify_password};
103
104 const COST: Cost = Cost {
106 m_cost: Params::DEFAULT_M_COST,
107 t_cost: Params::DEFAULT_T_COST,
108 p_cost: Params::DEFAULT_P_COST,
109 };
110
111 #[test]
112 fn password_hash_and_verify() {
113 let preimage = "temp123";
114 let digest = password(preimage, COST).expect("digest");
115
116 verify_password(preimage, &digest).expect("verified");
117 }
118
119 #[test]
120 fn the_owasp_pairs_are_recommended_and_weaker_costs_are_not() {
121 let recommended = [(47104, 1), (19456, 2), (12288, 3), (9216, 4), (7168, 5)];
122 let weaker = [(19456, 1), (7168, 4), (4096, 9), (64, 1)];
123 let cost = |(m_cost, t_cost)| Cost { m_cost, t_cost, p_cost: 1 };
124
125 for pair in recommended {
126 assert!(cost(pair).is_recommended(), "{pair:?}");
127 }
128
129 for pair in weaker {
130 assert!(!cost(pair).is_recommended(), "{pair:?}");
131 }
132 }
133
134 #[test]
135 #[should_panic(expected = "unverified")]
136 fn password_hash_and_verify_fail() {
137 let preimage = "temp123";
138 let fakeimage = "temp321";
139 let digest = password(preimage, COST).expect("digest");
140
141 verify_password(fakeimage, &digest).expect("unverified");
142 }
143}