use argon2::{Algorithm, Argon2, Params, Version};
use crate::contract::{Identified, PasswordBasedKdf};
use crate::error::{Error, Result};
const MIN_SALT_LEN: usize = 8;
const MAX_OUTPUT_LEN: usize = u32::MAX as usize;
#[derive(Debug, Clone)]
pub struct Argon2Kdf {
inner: Argon2<'static>,
}
impl Argon2Kdf {
pub const IDENTIFIER: &'static str = "argon2";
pub fn new() -> Self {
Self {
inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT),
}
}
pub fn with_params(m_cost: u32, t_cost: u32, p_cost: u32) -> Result<Self> {
let params = Params::new(m_cost, t_cost, p_cost, None)
.map_err(|_| Error::InvalidKdfParams { label: "Argon2" })?;
Ok(Self {
inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, params),
})
}
}
impl Default for Argon2Kdf {
fn default() -> Self {
Self::new()
}
}
impl Identified for Argon2Kdf {
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
impl PasswordBasedKdf for Argon2Kdf {
fn derive_from_password(&self, password: &[u8], salt: &[u8], length: usize) -> Result<Vec<u8>> {
if salt.len() < MIN_SALT_LEN {
return Err(Error::InvalidKeyLength {
label: "argon2 salt",
expected: MIN_SALT_LEN,
got: salt.len(),
});
}
if length == 0 || length > MAX_OUTPUT_LEN {
return Err(Error::InvalidOutputLength {
requested: length,
max: MAX_OUTPUT_LEN,
});
}
let mut output = vec![0u8; length];
self.inner
.hash_password_into(password, salt, &mut output)
.map_err(|_| Error::InvalidOutputLength {
requested: length,
max: MAX_OUTPUT_LEN,
})?;
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use argon2::{AssociatedData, ParamsBuilder};
#[test]
fn matches_rfc9106_test_vector() {
let params = ParamsBuilder::new()
.m_cost(32)
.t_cost(3)
.p_cost(4)
.output_len(32)
.data(AssociatedData::new(&[0x04u8; 12]).unwrap())
.build()
.unwrap();
let secret = [0x03u8; 8];
let ctx =
Argon2::new_with_secret(&secret, Algorithm::Argon2id, Version::V0x13, params).unwrap();
let mut tag = [0u8; 32];
ctx.hash_password_into(&[0x01u8; 32], &[0x02u8; 16], &mut tag)
.unwrap();
assert_eq!(
crate::internal::hex_encode(&tag),
concat!(
"0d640df58d78766c08c037a34a8b53c9",
"d01ef0452d75b65eb52520e96b01e659"
)
);
}
#[test]
fn with_params_matches_reference_context() {
let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
let reference = Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
Params::new(32, 3, 4, None).unwrap(),
);
let mut expected = [0u8; 32];
reference
.hash_password_into(b"password", b"0123456789abcdef", &mut expected)
.unwrap();
assert_eq!(
kdf.derive_from_password(b"password", b"0123456789abcdef", 32)
.unwrap(),
expected
);
}
#[test]
fn default_matches_reference_context() {
let reference = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::DEFAULT);
let mut expected = [0u8; 32];
reference
.hash_password_into(b"pw", b"12345678", &mut expected)
.unwrap();
assert_eq!(
Argon2Kdf::default()
.derive_from_password(b"pw", b"12345678", 32)
.unwrap(),
expected
);
}
#[test]
fn same_inputs_same_output_and_length_validation() {
let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
let first = kdf.derive_from_password(b"pw", b"salt1234", 16).unwrap();
let second = kdf.derive_from_password(b"pw", b"salt1234", 16).unwrap();
assert_eq!(first, second);
assert_eq!(first.len(), 16);
let long = kdf.derive_from_password(b"pw", b"salt1234", 32).unwrap();
assert_eq!(long.len(), 32);
assert_ne!(first, long[..16]);
assert_eq!(
kdf.derive_from_password(b"pw", b"salt1234", 0).unwrap_err(),
Error::InvalidOutputLength {
requested: 0,
max: MAX_OUTPUT_LEN
}
);
}
#[test]
fn password_and_salt_both_affect_output() {
let kdf = Argon2Kdf::with_params(32, 3, 4).unwrap();
let base = kdf.derive_from_password(b"pw", b"salt1234", 32).unwrap();
assert_ne!(
base,
kdf.derive_from_password(b"pw2", b"salt1234", 32).unwrap()
);
assert_ne!(
base,
kdf.derive_from_password(b"pw", b"salt5678", 32).unwrap()
);
}
#[test]
fn short_salt_is_rejected() {
let kdf = Argon2Kdf::new();
assert_eq!(
kdf.derive_from_password(b"pw", b"1234567", 32).unwrap_err(),
Error::InvalidKeyLength {
label: "argon2 salt",
expected: 8,
got: 7
}
);
assert_eq!(
kdf.derive_from_password(b"pw", b"12345678", 32)
.unwrap()
.len(),
32
);
}
#[test]
fn invalid_params_are_rejected() {
assert_eq!(
Argon2Kdf::with_params(4, 3, 4).unwrap_err(),
Error::InvalidKdfParams { label: "Argon2" }
);
assert_eq!(
Argon2Kdf::with_params(32, 0, 4).unwrap_err(),
Error::InvalidKdfParams { label: "Argon2" }
);
assert_eq!(
Argon2Kdf::with_params(32, 3, 0).unwrap_err(),
Error::InvalidKdfParams { label: "Argon2" }
);
}
}