use argon2::{Algorithm, Argon2, Params, Version};
use zeroize::Zeroize;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy)]
pub struct Argon2Params {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
}
impl Argon2Params {
pub const OWASP_MIN: Self = Self {
m_cost: 19 * 1024,
t_cost: 2,
p_cost: 1,
};
pub const RECOMMENDED: Self = Self {
m_cost: 64 * 1024,
t_cost: 3,
p_cost: 4,
};
pub const WASM_DEFAULT: Self = Self {
m_cost: 16 * 1024,
t_cost: 3,
p_cost: 1,
};
}
pub fn argon2id(password: &[u8], salt: &[u8], params: Argon2Params, out: &mut [u8]) -> Result<()> {
const SALT_MIN: usize = 8;
const OUTPUT_MAX: usize = 4096;
const M_COST_MAX: u32 = 4_194_304;
const T_COST_MAX: u32 = 256;
const P_COST_MAX: u32 = 256;
if salt.len() < SALT_MIN {
return Err(Error::InvalidLength {
expected: SALT_MIN,
got: salt.len(),
});
}
if out.is_empty() {
return Err(Error::InvalidLength {
expected: 1,
got: 0,
});
}
if out.len() > OUTPUT_MAX {
return Err(Error::InvalidLength {
expected: OUTPUT_MAX,
got: out.len(),
});
}
if params.m_cost > M_COST_MAX || params.t_cost > T_COST_MAX || params.p_cost > P_COST_MAX {
return Err(Error::InvalidData);
}
let argon2_params = Params::new(params.m_cost, params.t_cost, params.p_cost, Some(out.len()))
.map_err(|_| Error::InvalidData)?;
let result = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params)
.hash_password_into(password, salt, out);
if result.is_err() {
out.zeroize();
}
result.map_err(|_| Error::Internal)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
const FAST: Argon2Params = Argon2Params {
m_cost: 8,
t_cost: 1,
p_cost: 1,
};
#[test]
fn deterministic() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2id(b"password", b"saltsalt", FAST, &mut out1).unwrap();
argon2id(b"password", b"saltsalt", FAST, &mut out2).unwrap();
assert_eq!(out1, out2);
}
#[test]
fn sensitive_to_password() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2id(b"password1", b"saltsalt", FAST, &mut out1).unwrap();
argon2id(b"password2", b"saltsalt", FAST, &mut out2).unwrap();
assert_ne!(out1, out2);
}
#[test]
fn sensitive_to_salt() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2id(b"password", b"saltsalt", FAST, &mut out1).unwrap();
argon2id(b"password", b"saltXXXX", FAST, &mut out2).unwrap();
assert_ne!(out1, out2);
}
#[test]
fn sensitive_to_t_cost() {
let params2 = Argon2Params {
m_cost: 8,
t_cost: 2,
p_cost: 1,
};
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2id(b"password", b"saltsalt", FAST, &mut out1).unwrap();
argon2id(b"password", b"saltsalt", params2, &mut out2).unwrap();
assert_ne!(out1, out2);
}
#[test]
fn output_length_affects_result() {
let mut out32 = [0u8; 32];
let mut out64 = [0u8; 64];
argon2id(b"password", b"saltsalt", FAST, &mut out32).unwrap();
argon2id(b"password", b"saltsalt", FAST, &mut out64).unwrap();
assert_ne!(out32, out64[..32]);
}
#[test]
fn output_16_bytes() {
let mut out = [0u8; 16];
argon2id(b"password", b"saltsalt", FAST, &mut out).unwrap();
assert!(out.iter().any(|&b| b != 0));
}
#[test]
fn empty_password_accepted() {
let mut out = [0u8; 32];
argon2id(b"", b"saltsalt", FAST, &mut out).unwrap();
assert!(out.iter().any(|&b| b != 0));
}
#[test]
fn salt_too_short_rejected() {
let mut out = [0u8; 32];
let err = argon2id(b"password", b"short", FAST, &mut out).unwrap_err();
assert!(
matches!(
err,
Error::InvalidLength {
expected: 8,
got: 5
}
),
"expected InvalidLength(8, 5), got {err:?}"
);
}
#[test]
fn empty_salt_rejected() {
let mut out = [0u8; 32];
let err = argon2id(b"password", b"", FAST, &mut out).unwrap_err();
assert!(
matches!(
err,
Error::InvalidLength {
expected: 8,
got: 0
}
),
"expected InvalidLength(8, 0), got {err:?}"
);
}
#[test]
fn empty_output_rejected() {
let mut out: [u8; 0] = [];
let err = argon2id(b"password", b"saltsalt", FAST, &mut out).unwrap_err();
assert!(
matches!(
err,
Error::InvalidLength {
expected: 1,
got: 0
}
),
"expected InvalidLength(1, 0), got {err:?}"
);
}
#[test]
fn zero_m_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 0,
t_cost: 1,
p_cost: 1,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn zero_t_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 8,
t_cost: 0,
p_cost: 1,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn zero_p_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 8,
t_cost: 1,
p_cost: 0,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn excessive_m_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 4_194_305,
t_cost: 1,
p_cost: 1,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn excessive_t_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 8,
t_cost: 257,
p_cost: 1,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn excessive_p_cost_returns_invalid_data() {
let bad = Argon2Params {
m_cost: 8,
t_cost: 1,
p_cost: 257,
};
let mut out = [0u8; 32];
assert!(matches!(
argon2id(b"pw", b"saltsalt", bad, &mut out),
Err(Error::InvalidData)
));
}
#[test]
fn output_too_large_rejected() {
let mut out = vec![0u8; 4097];
let err = argon2id(b"pw", b"saltsalt", FAST, &mut out).unwrap_err();
assert!(matches!(
err,
Error::InvalidLength {
expected: 4096,
got: 4097
}
));
}
#[test]
fn boundary_t_cost_accepted() {
let params = Argon2Params {
m_cost: 8,
t_cost: 256,
p_cost: 1,
};
let mut out = [0u8; 32];
assert!(argon2id(b"pw", b"saltsalt", params, &mut out).is_ok());
}
#[test]
fn boundary_p_cost_accepted() {
let params = Argon2Params {
m_cost: 2048,
t_cost: 1,
p_cost: 256,
};
let mut out = [0u8; 32];
assert!(argon2id(b"pw", b"saltsalt", params, &mut out).is_ok());
}
#[test]
fn boundary_output_size_accepted() {
let mut out = vec![0u8; 4096];
assert!(argon2id(b"pw", b"saltsalt", FAST, &mut out).is_ok());
}
}