1#[cfg(feature = "serde")]
45use serde::{Deserialize, Serialize};
46#[cfg(feature = "base64")]
47use subtle::ConstantTimeEq;
48use zeroize::Zeroize;
49
50#[cfg(feature = "base64")]
51use crate::argon2::ARGON2_VERSION_NUMBER;
52use crate::argon2::{self, argon2_hash};
53use crate::constants::*;
54use crate::error::Error;
55
56pub(crate) const STR_HASHBYTES: usize = 32;
57
58#[cfg_attr(
59 feature = "serde",
60 derive(Zeroize, Clone, Debug, Serialize, Deserialize)
61)]
62#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
63pub enum PasswordHashAlgorithm {
65 Argon2i13 = 1,
67 Argon2id13 = 2,
69}
70
71impl From<u32> for PasswordHashAlgorithm {
72 fn from(num: u32) -> Self {
73 match num {
75 num if num == PasswordHashAlgorithm::Argon2i13 as u32 => {
76 PasswordHashAlgorithm::Argon2i13
77 }
78 num if num == PasswordHashAlgorithm::Argon2id13 as u32 => {
79 PasswordHashAlgorithm::Argon2id13
80 }
81 _ => panic!("invalid password hash algorithm type: {}", num),
82 }
83 }
84}
85
86impl From<PasswordHashAlgorithm> for argon2::Argon2Type {
87 fn from(algo: PasswordHashAlgorithm) -> Self {
88 match algo {
89 PasswordHashAlgorithm::Argon2i13 => argon2::Argon2Type::Argon2i,
90 PasswordHashAlgorithm::Argon2id13 => argon2::Argon2Type::Argon2id,
91 }
92 }
93}
94
95pub fn crypto_pwhash(
116 output: &mut [u8],
117 password: &[u8],
118 salt: &[u8],
119 opslimit: u64,
120 memlimit: usize,
121 algorithm: PasswordHashAlgorithm,
122) -> Result<(), Error> {
123 validate!(
124 CRYPTO_PWHASH_OPSLIMIT_MIN,
125 CRYPTO_PWHASH_OPSLIMIT_MAX,
126 opslimit,
127 "opslimit"
128 );
129 validate!(
130 CRYPTO_PWHASH_MEMLIMIT_MIN,
131 CRYPTO_PWHASH_MEMLIMIT_MAX,
132 memlimit,
133 "memlimit"
134 );
135
136 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
137
138 argon2_hash(
139 t_cost,
140 m_cost,
141 1,
142 password,
143 salt,
144 None,
145 None,
146 output,
147 algorithm.into(),
148 )
149}
150
151#[cfg(any(feature = "base64", all(doc, not(doctest))))]
152#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
153pub(crate) fn pwhash_to_string(t_cost: u32, m_cost: u32, salt: &[u8], hash: &[u8]) -> String {
154 use base64::Engine as _;
155 use base64::engine::general_purpose;
156
157 format!(
158 "$argon2id$v={}$m={},t={},p=1${}${}",
159 argon2::ARGON2_VERSION_NUMBER,
160 m_cost,
161 t_cost,
162 general_purpose::STANDARD_NO_PAD.encode(salt),
163 general_purpose::STANDARD_NO_PAD.encode(hash),
164 )
165}
166
167pub(crate) fn convert_costs(opslimit: u64, memlimit: usize) -> (u32, u32) {
168 (opslimit as u32, (memlimit / 1024) as u32)
169}
170
171#[cfg(any(feature = "base64", all(doc, not(doctest))))]
180#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
181pub fn crypto_pwhash_str(password: &[u8], opslimit: u64, memlimit: usize) -> Result<String, Error> {
182 validate!(
183 CRYPTO_PWHASH_OPSLIMIT_MIN,
184 CRYPTO_PWHASH_OPSLIMIT_MAX,
185 opslimit,
186 "opslimit"
187 );
188 validate!(
189 CRYPTO_PWHASH_MEMLIMIT_MIN,
190 CRYPTO_PWHASH_MEMLIMIT_MAX,
191 memlimit,
192 "memlimit"
193 );
194
195 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
196 let mut hash = [0u8; STR_HASHBYTES];
197 crate::rng::copy_randombytes(&mut salt);
198
199 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
200
201 argon2_hash(
202 t_cost,
203 m_cost,
204 1,
205 password,
206 &salt,
207 None,
208 None,
209 &mut hash,
210 argon2::Argon2Type::Argon2id,
211 )?;
212
213 let pw = pwhash_to_string(t_cost, m_cost, &salt, &hash);
214
215 Ok(pw)
216}
217
218#[cfg(feature = "base64")]
219#[derive(Default)]
220pub(crate) struct Pwhash {
221 pub(crate) pwhash: Option<Vec<u8>>,
222 pub(crate) salt: Option<Vec<u8>>,
223 pub(crate) type_: Option<PasswordHashAlgorithm>,
224 pub(crate) t_cost: Option<u32>,
225 pub(crate) m_cost: Option<u32>,
226 pub(crate) parallelism: Option<u32>,
227 pub(crate) version: Option<u32>,
228}
229
230#[cfg(feature = "base64")]
231impl Pwhash {
232 pub(crate) fn parse_encoded_pwhash(hashed_password: &str) -> Result<Self, Error> {
233 use base64::Engine;
234 let mut pwhash = Pwhash::default();
235 let base64_engine = base64::engine::general_purpose::GeneralPurpose::new(
236 &base64::alphabet::STANDARD,
237 base64::engine::general_purpose::NO_PAD,
238 );
239
240 for s in hashed_password.split('$') {
241 if s.is_empty() {
242 } else if s.starts_with("argon2") {
244 match s {
245 "argon2i" => pwhash.type_ = Some(PasswordHashAlgorithm::Argon2i13),
246 "argon2id" => pwhash.type_ = Some(PasswordHashAlgorithm::Argon2id13),
247 _ => return Err(dryoc_error!(format!("invalid type: {}", s))),
248 }
249 } else if let Some(stripped) = s.strip_prefix("v=") {
250 pwhash.version = Some(
251 stripped
252 .parse::<u32>()
253 .map_err(|_| dryoc_error!("unable to decode password hash version"))?,
254 );
255 } else if s.contains("m=") && s.contains("t=") && s.contains("p=") {
256 for p in s.split(',') {
257 if let Some(m_cost) = p.strip_prefix("m=") {
258 pwhash.m_cost = Some(m_cost.parse::<u32>().map_err(|_| {
259 dryoc_error!("unable to decode password hash parameter m_cost")
260 })?);
261 } else if let Some(t_cost) = p.strip_prefix("t=") {
262 pwhash.t_cost = Some(t_cost.parse::<u32>().map_err(|_| {
263 dryoc_error!("unable to decode password hash parameter t_cost")
264 })?);
265 } else if let Some(parallelism) = p.strip_prefix("p=") {
266 pwhash.parallelism = Some(parallelism.parse::<u32>().map_err(|_| {
267 dryoc_error!("unable to decode password hash parameter t_cost")
268 })?);
269 }
270 }
271 } else if pwhash.salt.is_none() {
272 pwhash.salt = base64_engine.decode(s).ok();
273 } else if pwhash.pwhash.is_none() {
274 pwhash.pwhash = base64_engine.decode(s).ok();
275 }
276 }
277
278 if pwhash.version.is_none() || pwhash.version.unwrap() != ARGON2_VERSION_NUMBER {
280 Err(dryoc_error!("unsupported password hash"))
281 } else if pwhash.parallelism.is_none() || pwhash.parallelism.unwrap() != 1 {
283 Err(dryoc_error!("parallelism missing or invalid"))
284 } else if pwhash.pwhash.is_none() || pwhash.pwhash.as_ref().unwrap().is_empty() {
286 Err(dryoc_error!("password hash missing"))
287 } else if pwhash.salt.is_none() || pwhash.salt.as_ref().unwrap().is_empty() {
288 Err(dryoc_error!("password salt missing"))
289 } else if pwhash.type_.is_none() {
290 Err(dryoc_error!("algorithm type missing"))
291 } else if pwhash.m_cost.is_none() {
292 Err(dryoc_error!("m_cost missing"))
293 } else if pwhash.t_cost.is_none() {
294 Err(dryoc_error!("t_cost missing"))
295 } else {
296 Ok(pwhash)
297 }
298 }
299}
300
301#[cfg(any(feature = "base64", all(doc, not(doctest))))]
306#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
307pub fn crypto_pwhash_str_verify(hashed_password: &str, password: &[u8]) -> Result<(), Error> {
308 let mut hash = [0u8; STR_HASHBYTES];
309
310 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
311
312 argon2_hash(
313 pwhash.t_cost.unwrap(),
314 pwhash.m_cost.unwrap(),
315 pwhash.parallelism.unwrap(),
316 password,
317 pwhash.salt.unwrap().as_ref(),
318 None,
319 None,
320 &mut hash,
321 pwhash.type_.unwrap().into(),
322 )?;
323
324 if hash.ct_eq(pwhash.pwhash.unwrap().as_ref()).unwrap_u8() == 1 {
325 Ok(())
326 } else {
327 Err(dryoc_error!("password hashes do not match"))
328 }
329}
330
331#[cfg(any(feature = "base64", all(doc, not(doctest))))]
337#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
338pub fn crypto_pwhash_str_needs_rehash(
339 hashed_password: &str,
340 opslimit: u64,
341 memlimit: usize,
342) -> Result<bool, Error> {
343 let pwhash = Pwhash::parse_encoded_pwhash(hashed_password)?;
344
345 let (t_cost, m_cost) = convert_costs(opslimit, memlimit);
346
347 if t_cost != pwhash.t_cost.unwrap() || m_cost != pwhash.m_cost.unwrap() {
348 Ok(true)
349 } else {
350 Ok(false)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_crypto_pwhash() {
360 use sodiumoxide::crypto::pwhash;
361
362 use crate::rng::copy_randombytes;
363
364 let mut hash = [0u8; 32];
365 let mut so_hash = [0u8; 32];
366 let mut salt = [0u8; CRYPTO_PWHASH_SALTBYTES];
367
368 copy_randombytes(&mut salt);
369
370 let password = b"donkey kong";
371
372 crypto_pwhash(
373 &mut hash,
374 password,
375 &salt,
376 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
377 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
378 PasswordHashAlgorithm::Argon2id13,
379 )
380 .expect("pwhash failed");
381
382 let _ = pwhash::argon2id13::derive_key(
383 &mut so_hash,
384 password,
385 &pwhash::argon2id13::Salt::from_slice(&salt).expect("salt failed"),
386 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
387 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
388 )
389 .expect("so pwhash failed");
390
391 assert_eq!(hash, so_hash);
392 }
393
394 #[cfg(feature = "base64")]
395 #[test]
396 fn test_crypto_pwhash_str() {
397 use sodiumoxide::crypto::pwhash;
398
399 let password = b"donkey kong";
400
401 let pwhash = crypto_pwhash_str(
402 password,
403 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
404 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
405 )
406 .expect("pwhash failed");
407 let pwhash2 = crypto_pwhash_str(
408 password,
409 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
410 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
411 )
412 .expect("pwhash failed");
413
414 let parsed = Pwhash::parse_encoded_pwhash(&pwhash).expect("couldn't parse pwhash");
415 let parsed2 = Pwhash::parse_encoded_pwhash(&pwhash2).expect("couldn't parse pwhash");
416
417 assert_ne!(
418 parsed.salt.as_ref().expect("missing salt"),
419 &vec![0u8; CRYPTO_PWHASH_SALTBYTES]
420 );
421 assert_ne!(parsed.salt, parsed2.salt);
422
423 let mut pwhash_bytes = [0u8; CRYPTO_PWHASH_STRBYTES];
424 pwhash_bytes[..pwhash.len()].copy_from_slice(pwhash.as_bytes());
425
426 assert!(pwhash::argon2id13::pwhash_verify(
427 &pwhash::argon2id13::HashedPassword::from_slice(&pwhash_bytes)
428 .expect("hashed password failed"),
429 password,
430 ));
431 }
432
433 #[cfg(feature = "base64")]
434 #[test]
435 fn test_crypto_pwhash_str_verify() {
436 use sodiumoxide::crypto::pwhash;
437
438 let password = b"donkey kong";
439
440 let pwhash = pwhash::argon2id13::pwhash(
441 password,
442 pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
443 pwhash::argon2id13::MEMLIMIT_INTERACTIVE,
444 )
445 .expect("so pwhash failed");
446
447 let pw_str = std::str::from_utf8(&pwhash.0)
448 .expect("from ut8 failed")
449 .trim_end_matches('\x00');
450
451 crypto_pwhash_str_verify(pw_str, password).expect("verify failed");
452 crypto_pwhash_str_verify(pw_str, b"invalid password")
453 .expect_err("verify should have failed");
454
455 assert!(
457 !crypto_pwhash_str_needs_rehash(
458 pw_str,
459 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
460 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
461 )
462 .expect("verify rehash failed")
463 );
464
465 assert!(
467 crypto_pwhash_str_needs_rehash(
468 pw_str,
469 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE + 1,
470 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
471 )
472 .expect("verify rehash failed")
473 );
474 }
475}