1use std::str::FromStr;
4
5use argon2::{Algorithm as Argon2Algorithm, Argon2, Params as Argon2Params, Version};
6use base64::{engine::general_purpose::STANDARD, engine::general_purpose::STANDARD_NO_PAD, Engine as _};
7use pbkdf2::pbkdf2_hmac;
8use rand::rngs::OsRng;
9use rand::RngCore;
10use scrypt::{scrypt, Params as ScryptParams};
11use sha2::Sha256;
12use subtle::ConstantTimeEq;
13
14use crate::error::{Error, Result};
15
16#[derive(Clone, Debug)]
18pub struct HashOptions {
19 pub iterations: u32,
21 pub time_cost: u32,
23 pub memory_cost: u32,
25 pub parallelism: u32,
27 pub output_length: usize,
29 pub log_n: u8,
31 pub r: u32,
33 pub p: u32,
35 pub cost: u32,
37}
38
39impl Default for HashOptions {
40 fn default() -> Self {
41 Self {
42 iterations: 100_000,
43 time_cost: 3,
44 memory_cost: 64 * 1024,
45 parallelism: 4,
46 output_length: 32,
47 log_n: 14,
48 r: 8,
49 p: 1,
50 cost: 12,
51 }
52 }
53}
54
55impl HashOptions {
56 pub fn validate(&self) -> Result<()> {
60 if self.iterations == 0 {
61 return Err(Error::InvalidParameter(
62 "iterations must be at least 1".to_string(),
63 ));
64 }
65 if self.time_cost == 0 {
66 return Err(Error::InvalidParameter(
67 "argon2 time_cost must be at least 1".to_string(),
68 ));
69 }
70 if self.parallelism == 0 {
71 return Err(Error::InvalidParameter(
72 "argon2 parallelism must be at least 1".to_string(),
73 ));
74 }
75 if u64::from(self.memory_cost) < 8 * u64::from(self.parallelism) {
77 return Err(Error::InvalidParameter(
78 "argon2 memory_cost is too low for the requested parallelism".to_string(),
79 ));
80 }
81 if self.log_n == 0 || self.log_n >= 64 {
83 return Err(Error::InvalidParameter(
84 "scrypt log_n must be between 1 and 63".to_string(),
85 ));
86 }
87 if self.r == 0 {
88 return Err(Error::InvalidParameter(
89 "scrypt r must be at least 1".to_string(),
90 ));
91 }
92 if self.p == 0 {
93 return Err(Error::InvalidParameter(
94 "scrypt p must be at least 1".to_string(),
95 ));
96 }
97 if self.output_length == 0 {
98 return Err(Error::InvalidParameter(
99 "output_length must be at least 1".to_string(),
100 ));
101 }
102 if !(4..=31).contains(&self.cost) {
104 return Err(Error::InvalidParameter(
105 "bcrypt cost must be between 4 and 31".to_string(),
106 ));
107 }
108 Ok(())
109 }
110}
111
112fn opts_or_default(options: Option<&HashOptions>) -> HashOptions {
113 let mut defaults = HashOptions::default();
114 if let Some(o) = options {
115 if o.iterations != 0 {
116 defaults.iterations = o.iterations;
117 }
118 if o.time_cost != 0 {
119 defaults.time_cost = o.time_cost;
120 }
121 if o.memory_cost != 0 {
122 defaults.memory_cost = o.memory_cost;
123 }
124 if o.parallelism != 0 {
125 defaults.parallelism = o.parallelism;
126 }
127 if o.output_length != 0 {
128 defaults.output_length = o.output_length;
129 }
130 if o.log_n != 0 {
131 defaults.log_n = o.log_n;
132 }
133 if o.r != 0 {
134 defaults.r = o.r;
135 }
136 if o.p != 0 {
137 defaults.p = o.p;
138 }
139 if o.cost != 0 {
140 defaults.cost = o.cost;
141 }
142 }
143 defaults
144}
145
146fn normalize_algorithm(name: &str) -> String {
147 name.to_lowercase().replace('_', "-")
148}
149
150pub fn hash(password: &str) -> Result<String> {
152 hash_with(password, "argon2id", None)
153}
154
155pub fn hash_with(password: &str, algorithm: &str, options: Option<&HashOptions>) -> Result<String> {
157 let algorithm = normalize_algorithm(algorithm);
158 let opts = opts_or_default(options);
159 opts.validate()?;
160 match algorithm.as_str() {
161 "argon2id" => hash_argon2id(password, &opts),
162 "scrypt" => hash_scrypt(password, &opts),
163 "pbkdf2-sha256" => hash_pbkdf2(password, &opts),
164 "bcrypt" => hash_bcrypt(password, &opts),
165 _ => Err(Error::UnsupportedAlgorithm(algorithm)),
166 }
167}
168
169fn random_salt(len: usize) -> Vec<u8> {
170 let mut salt = vec![0u8; len];
171 OsRng.fill_bytes(&mut salt);
172 salt
173}
174
175fn phc_b64(data: &[u8]) -> String {
176 STANDARD_NO_PAD.encode(data)
177}
178
179fn phc_b64_decode(text: &str) -> Result<Vec<u8>> {
180 STANDARD_NO_PAD
181 .decode(text)
182 .map_err(|e| Error::Base64(e.to_string()))
183}
184
185fn ab64_encode(data: &[u8]) -> String {
186 STANDARD
187 .encode(data)
188 .replace('+', ".")
189 .trim_end_matches('=')
190 .to_string()
191}
192
193fn ab64_decode(text: &str) -> Result<Vec<u8>> {
194 let mut s = text.replace('.', "+");
195 let pad = (4 - (s.len() % 4)) % 4;
196 s.push_str(&"=".repeat(pad));
197 STANDARD
198 .decode(s)
199 .map_err(|e| Error::Base64(e.to_string()))
200}
201
202fn hash_argon2id(password: &str, opts: &HashOptions) -> Result<String> {
203 let salt = random_salt(16);
204 let params = Argon2Params::new(
205 opts.memory_cost,
206 opts.time_cost,
207 opts.parallelism,
208 Some(opts.output_length),
209 )
210 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
211 let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
212 let mut out = vec![0u8; opts.output_length];
213 argon2
214 .hash_password_into(password.as_bytes(), &salt, &mut out)
215 .map_err(|e| Error::Internal(e.to_string()))?;
216 Ok(format!(
217 "$argon2id$v=19$m={},t={},p={}${}${}",
218 opts.memory_cost,
219 opts.time_cost,
220 opts.parallelism,
221 phc_b64(&salt),
222 phc_b64(&out)
223 ))
224}
225
226fn hash_scrypt(password: &str, opts: &HashOptions) -> Result<String> {
227 let salt = random_salt(32);
228 let params = ScryptParams::new(opts.log_n, opts.r, opts.p, opts.output_length)
229 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
230 let mut out = vec![0u8; opts.output_length];
231 scrypt(password.as_bytes(), &salt, ¶ms, &mut out)
232 .map_err(|e| Error::Internal(e.to_string()))?;
233 Ok(format!(
234 "$scrypt$ln={},r={},p={}${}${}",
235 opts.log_n,
236 opts.r,
237 opts.p,
238 phc_b64(&salt),
239 phc_b64(&out)
240 ))
241}
242
243fn hash_pbkdf2(password: &str, opts: &HashOptions) -> Result<String> {
244 let salt = random_salt(32);
245 let mut out = vec![0u8; opts.output_length];
246 pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, opts.iterations, &mut out);
247 Ok(format!(
248 "$pbkdf2-sha256${}${}${}",
249 opts.iterations,
250 ab64_encode(&salt),
251 ab64_encode(&out)
252 ))
253}
254
255fn hash_bcrypt(password: &str, opts: &HashOptions) -> Result<String> {
256 let cost = if opts.cost == 0 { 12 } else { opts.cost };
257 bcrypt::hash(password, cost).map_err(|e| Error::Internal(e.to_string()))
258}
259
260pub fn verify(password: &str, hashed: &str) -> Result<bool> {
262 if hashed.starts_with("$argon2id$") {
263 verify_argon2id(password, hashed)
264 } else if hashed.starts_with("$scrypt$") {
265 verify_scrypt(password, hashed)
266 } else if hashed.starts_with("$pbkdf2-sha256$") {
267 verify_pbkdf2(password, hashed)
268 } else if hashed.starts_with("$2a$") || hashed.starts_with("$2b$") || hashed.starts_with("$2y$") {
269 bcrypt::verify(password, hashed).map_err(|e| Error::Internal(e.to_string()))
270 } else {
271 Err(Error::InvalidHashFormat(hashed.to_string()))
272 }
273}
274
275fn verify_argon2id(password: &str, hashed: &str) -> Result<bool> {
276 let parts: Vec<&str> = hashed.split('$').collect();
277 if parts.len() != 6 {
278 return Err(Error::InvalidHashFormat(hashed.to_string()));
279 }
280 let params = parse_params(parts[3])?;
281 let memory = u32::from_str(params.get("m").ok_or_else(|| Error::InvalidHashFormat("m".to_string()))?)
282 .map_err(|_| Error::InvalidHashFormat("m".to_string()))?;
283 let time = u32::from_str(params.get("t").ok_or_else(|| Error::InvalidHashFormat("t".to_string()))?)
284 .map_err(|_| Error::InvalidHashFormat("t".to_string()))?;
285 let parallelism = u32::from_str(params.get("p").ok_or_else(|| Error::InvalidHashFormat("p".to_string()))?)
286 .map_err(|_| Error::InvalidHashFormat("p".to_string()))?;
287 let salt = phc_b64_decode(parts[4])?;
288 let stored = phc_b64_decode(parts[5])?;
289 if salt.is_empty() || stored.is_empty() {
290 return Err(Error::InvalidHashFormat(hashed.to_string()));
291 }
292
293 let params = Argon2Params::new(memory, time, parallelism, Some(stored.len()))
294 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
295 let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
296 let mut out = vec![0u8; stored.len()];
297 argon2
298 .hash_password_into(password.as_bytes(), &salt, &mut out)
299 .map_err(|e| Error::Internal(e.to_string()))?;
300 Ok(out.as_slice().ct_eq(stored.as_slice()).into())
301}
302
303fn verify_scrypt(password: &str, hashed: &str) -> Result<bool> {
304 let parts: Vec<&str> = hashed.split('$').collect();
305 if parts.len() != 5 {
306 return Err(Error::InvalidHashFormat(hashed.to_string()));
307 }
308 let params = parse_params(parts[2])?;
309 let log_n = u8::from_str(params.get("ln").ok_or_else(|| Error::InvalidHashFormat("ln".to_string()))?)
310 .map_err(|_| Error::InvalidHashFormat("ln".to_string()))?;
311 let r = u32::from_str(params.get("r").ok_or_else(|| Error::InvalidHashFormat("r".to_string()))?)
312 .map_err(|_| Error::InvalidHashFormat("r".to_string()))?;
313 let p = u32::from_str(params.get("p").ok_or_else(|| Error::InvalidHashFormat("p".to_string()))?)
314 .map_err(|_| Error::InvalidHashFormat("p".to_string()))?;
315 let salt = phc_b64_decode(parts[3])?;
316 let stored = phc_b64_decode(parts[4])?;
317 if salt.is_empty() || stored.is_empty() {
318 return Err(Error::InvalidHashFormat(hashed.to_string()));
319 }
320
321 let params = ScryptParams::new(log_n, r, p, stored.len())
322 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
323 let mut out = vec![0u8; stored.len()];
324 scrypt(password.as_bytes(), &salt, ¶ms, &mut out)
325 .map_err(|e| Error::Internal(e.to_string()))?;
326 Ok(out.as_slice().ct_eq(stored.as_slice()).into())
327}
328
329fn verify_pbkdf2(password: &str, hashed: &str) -> Result<bool> {
330 let parts: Vec<&str> = hashed.split('$').collect();
331 if parts.len() != 5 {
332 return Err(Error::InvalidHashFormat(hashed.to_string()));
333 }
334 let iterations = u32::from_str(parts[2])
335 .map_err(|_| Error::InvalidHashFormat("iterations".to_string()))?;
336 let salt = ab64_decode(parts[3])?;
337 let stored = ab64_decode(parts[4])?;
338 if salt.is_empty() || stored.is_empty() {
339 return Err(Error::InvalidHashFormat(hashed.to_string()));
340 }
341
342 let mut out = vec![0u8; stored.len()];
343 pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, iterations, &mut out);
344 Ok(out.as_slice().ct_eq(stored.as_slice()).into())
345}
346
347fn parse_params(s: &str) -> Result<std::collections::HashMap<String, String>> {
348 let mut map = std::collections::HashMap::new();
349 for pair in s.split(',') {
350 let mut kv = pair.splitn(2, '=');
351 let k = kv.next().ok_or_else(|| Error::InvalidHashFormat(s.to_string()))?;
352 let v = kv.next().ok_or_else(|| Error::InvalidHashFormat(s.to_string()))?;
353 map.insert(k.to_string(), v.to_string());
354 }
355 Ok(map)
356}
357
358pub fn derive(
360 passphrase: &str,
361 salt: &[u8],
362 length: usize,
363 algorithm: Option<&str>,
364 options: Option<&HashOptions>,
365) -> Result<Vec<u8>> {
366 if salt.is_empty() {
367 return Err(Error::InvalidParameter("salt must not be empty".to_string()));
368 }
369 if length == 0 {
370 return Err(Error::InvalidParameter("length must be positive".to_string()));
371 }
372
373 let algorithm = normalize_algorithm(algorithm.unwrap_or("pbkdf2_sha256"));
374 let opts = opts_or_default(options);
375 opts.validate()?;
376
377 match algorithm.as_str() {
378 "pbkdf2-sha256" => {
379 let mut out = vec![0u8; length];
380 pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt, opts.iterations, &mut out);
381 Ok(out)
382 }
383 "scrypt" => {
384 let params = ScryptParams::new(opts.log_n, opts.r, opts.p, length)
385 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
386 let mut out = vec![0u8; length];
387 scrypt(passphrase.as_bytes(), salt, ¶ms, &mut out)
388 .map_err(|e| Error::Internal(e.to_string()))?;
389 Ok(out)
390 }
391 "argon2id" => {
392 let params = Argon2Params::new(
393 opts.memory_cost,
394 opts.time_cost,
395 opts.parallelism,
396 Some(length),
397 )
398 .map_err(|e| Error::InvalidParameter(e.to_string()))?;
399 let argon2 = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
400 let mut out = vec![0u8; length];
401 argon2
402 .hash_password_into(passphrase.as_bytes(), salt, &mut out)
403 .map_err(|e| Error::Internal(e.to_string()))?;
404 Ok(out)
405 }
406 _ => Err(Error::UnsupportedAlgorithm(algorithm)),
407 }
408}