#![forbid(unsafe_code)]
extern crate alloc;
use alloc::string::String;
use argon2::{Algorithm, Argon2, Params, Version};
use oxicrypto_core::{CryptoError, PasswordHash as PasswordHashTrait, PasswordHashParams};
#[derive(Debug, Clone, Copy)]
pub struct Argon2Params {
pub m_cost: u32,
pub t_cost: u32,
pub p_cost: u32,
}
impl Argon2Params {
pub const TEST_PARAMS: Self = Self {
m_cost: 64,
t_cost: 1,
p_cost: 1,
};
pub fn validate(&self) -> Result<(), CryptoError> {
if self.m_cost < 19_456 {
return Err(CryptoError::BadInput);
}
if self.t_cost < 2 {
return Err(CryptoError::BadInput);
}
if self.p_cost < 1 {
return Err(CryptoError::BadInput);
}
if self.m_cost < 8 * self.p_cost {
return Err(CryptoError::BadInput);
}
Ok(())
}
#[must_use]
pub fn interactive() -> Self {
Self {
m_cost: 65_536,
t_cost: 2,
p_cost: 1,
}
}
#[must_use]
pub fn moderate() -> Self {
Self {
m_cost: 262_144,
t_cost: 3,
p_cost: 4,
}
}
#[must_use]
pub fn sensitive() -> Self {
Self {
m_cost: 1_048_576,
t_cost: 4,
p_cost: 8,
}
}
}
impl PasswordHashParams for Argon2Params {
fn memory_cost(&self) -> Option<u32> {
Some(self.m_cost)
}
fn time_cost(&self) -> Option<u32> {
Some(self.t_cost)
}
fn parallelism(&self) -> Option<u32> {
Some(self.p_cost)
}
}
#[must_use = "argon2id derive result must be checked"]
pub fn argon2id_derive(
password: &[u8],
salt: &[u8],
params: Argon2Params,
out: &mut [u8],
) -> Result<(), CryptoError> {
if out.is_empty() {
return Err(CryptoError::BadInput);
}
if salt.len() < 8 {
return Err(CryptoError::BadInput);
}
let a2_params = Params::new(params.m_cost, params.t_cost, params.p_cost, Some(out.len()))
.map_err(|_| CryptoError::BadInput)?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, a2_params);
argon2
.hash_password_into(password, salt, out)
.map_err(|_| CryptoError::Internal("Argon2id derive failed"))
}
#[must_use = "argon2d derive result must be checked"]
pub fn argon2d_derive(
password: &[u8],
salt: &[u8],
params: Argon2Params,
out: &mut [u8],
) -> Result<(), CryptoError> {
if out.is_empty() {
return Err(CryptoError::BadInput);
}
if salt.len() < 8 {
return Err(CryptoError::BadInput);
}
let a2_params = Params::new(params.m_cost, params.t_cost, params.p_cost, Some(out.len()))
.map_err(|_| CryptoError::BadInput)?;
let argon2 = Argon2::new(Algorithm::Argon2d, Version::V0x13, a2_params);
argon2
.hash_password_into(password, salt, out)
.map_err(|_| CryptoError::Internal("Argon2d derive failed"))
}
#[must_use = "argon2i derive result must be checked"]
pub fn argon2i_derive(
password: &[u8],
salt: &[u8],
params: Argon2Params,
out: &mut [u8],
) -> Result<(), CryptoError> {
if out.is_empty() {
return Err(CryptoError::BadInput);
}
if salt.len() < 8 {
return Err(CryptoError::BadInput);
}
let a2_params = Params::new(params.m_cost, params.t_cost, params.p_cost, Some(out.len()))
.map_err(|_| CryptoError::BadInput)?;
let argon2 = Argon2::new(Algorithm::Argon2i, Version::V0x13, a2_params);
argon2
.hash_password_into(password, salt, out)
.map_err(|_| CryptoError::Internal("Argon2i derive failed"))
}
#[derive(Debug, Clone, Copy)]
pub struct Argon2idHasher {
pub params: Argon2Params,
}
impl Argon2idHasher {
#[must_use]
pub fn new(params: Argon2Params) -> Self {
Self { params }
}
#[must_use]
pub fn interactive() -> Self {
Self::new(Argon2Params::interactive())
}
#[must_use]
pub fn moderate() -> Self {
Self::new(Argon2Params::moderate())
}
#[must_use]
pub fn sensitive() -> Self {
Self::new(Argon2Params::sensitive())
}
}
impl PasswordHashTrait for Argon2idHasher {
fn name(&self) -> &'static str {
"argon2id"
}
fn hash_password(
&self,
password: &[u8],
salt: &[u8],
_params: &dyn PasswordHashParams,
out: &mut [u8],
) -> Result<(), CryptoError> {
argon2id_derive(password, salt, self.params, out)
}
}
#[must_use = "PHC string result must be checked"]
pub fn argon2id_to_phc_string(
hasher: &Argon2idHasher,
salt: &[u8],
hash: &[u8],
) -> Result<String, CryptoError> {
use argon2::PasswordHash;
use password_hash::phc::{Output, ParamsString, Salt};
let a2_params = Params::new(
hasher.params.m_cost,
hasher.params.t_cost,
hasher.params.p_cost,
Some(hash.len()),
)
.map_err(|_| CryptoError::BadInput)?;
let params_str = ParamsString::try_from(&a2_params).map_err(|_| CryptoError::Encoding)?;
let salt_val = Salt::new(salt).map_err(|_| CryptoError::BadInput)?;
let output = Output::new(hash).map_err(|_| CryptoError::Encoding)?;
let ph = PasswordHash {
algorithm: argon2::ARGON2ID_IDENT,
version: Some(Version::V0x13.into()),
params: params_str,
salt: Some(salt_val),
hash: Some(output),
};
Ok(alloc::format!("{ph}"))
}
#[must_use = "PHC verification result must be checked"]
pub fn argon2id_verify_phc(phc: &str, password: &[u8]) -> Result<(), CryptoError> {
use argon2::{PasswordHash, PasswordVerifier};
let hash = PasswordHash::new(phc).map_err(|_| CryptoError::Encoding)?;
Argon2::default()
.verify_password(password, &hash)
.map_err(|_| CryptoError::InvalidTag)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_hasher() -> Argon2idHasher {
Argon2idHasher::new(Argon2Params::TEST_PARAMS)
}
const TEST_SALT: &[u8] = b"01234567890abcde";
#[test]
fn argon2id_derive_deterministic() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2id_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out1)
.expect("derive 1");
argon2id_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out2)
.expect("derive 2");
assert_eq!(out1, out2, "Argon2id must be deterministic");
assert_ne!(out1, [0u8; 32]);
}
#[test]
fn argon2id_derive_short_salt_errors() {
let mut out = [0u8; 32];
let result = argon2id_derive(b"password", b"short", Argon2Params::TEST_PARAMS, &mut out);
assert_eq!(result, Err(CryptoError::BadInput));
}
#[test]
fn argon2id_derive_empty_output_errors() {
let result = argon2id_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut []);
assert_eq!(result, Err(CryptoError::BadInput));
}
#[test]
fn password_hash_trait_hash_password_deterministic() {
let hasher = test_hasher();
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
hasher
.hash_password(b"password", TEST_SALT, &hasher.params, &mut out1)
.expect("hash 1");
hasher
.hash_password(b"password", TEST_SALT, &hasher.params, &mut out2)
.expect("hash 2");
assert_eq!(out1, out2, "PasswordHash must be deterministic");
assert_ne!(out1, [0u8; 32]);
}
#[test]
fn preset_cost_ordering() {
let interactive = Argon2Params::interactive();
let moderate = Argon2Params::moderate();
let sensitive = Argon2Params::sensitive();
assert!(sensitive.m_cost > moderate.m_cost);
assert!(moderate.m_cost > interactive.m_cost);
assert!(sensitive.t_cost >= moderate.t_cost);
assert!(moderate.t_cost >= interactive.t_cost);
}
#[test]
fn hasher_name() {
assert_eq!(test_hasher().name(), "argon2id");
}
#[test]
fn phc_round_trip() {
let hasher = test_hasher();
let mut hash = [0u8; 32];
argon2id_derive(b"password", TEST_SALT, hasher.params, &mut hash).expect("derive");
let phc = argon2id_to_phc_string(&hasher, TEST_SALT, &hash).expect("to_phc");
assert!(
phc.starts_with("$argon2id$"),
"PHC must start with $argon2id$"
);
argon2id_verify_phc(&phc, b"password").expect("verify correct");
}
#[test]
fn phc_wrong_password_rejected() {
let hasher = test_hasher();
let mut hash = [0u8; 32];
argon2id_derive(b"password", TEST_SALT, hasher.params, &mut hash).expect("derive");
let phc = argon2id_to_phc_string(&hasher, TEST_SALT, &hash).expect("to_phc");
let result = argon2id_verify_phc(&phc, b"wrongpassword");
assert_eq!(result, Err(CryptoError::InvalidTag));
}
#[test]
fn phc_malformed_string_rejected() {
let result = argon2id_verify_phc("invalid-phc-string", b"password");
assert_eq!(result, Err(CryptoError::Encoding));
}
#[test]
fn argon2d_derive_deterministic() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2d_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out1)
.expect("argon2d derive 1");
argon2d_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out2)
.expect("argon2d derive 2");
assert_eq!(out1, out2, "Argon2d must be deterministic");
assert_ne!(out1, [0u8; 32]);
}
#[test]
fn argon2i_derive_deterministic() {
let mut out1 = [0u8; 32];
let mut out2 = [0u8; 32];
argon2i_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out1)
.expect("argon2i derive 1");
argon2i_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut out2)
.expect("argon2i derive 2");
assert_eq!(out1, out2, "Argon2i must be deterministic");
assert_ne!(out1, [0u8; 32]);
}
#[test]
fn argon2_variants_produce_distinct_outputs() {
let mut out_id = [0u8; 32];
let mut out_d = [0u8; 32];
let mut out_i = [0u8; 32];
argon2id_derive(
b"password",
TEST_SALT,
Argon2Params::TEST_PARAMS,
&mut out_id,
)
.expect("argon2id");
argon2d_derive(
b"password",
TEST_SALT,
Argon2Params::TEST_PARAMS,
&mut out_d,
)
.expect("argon2d");
argon2i_derive(
b"password",
TEST_SALT,
Argon2Params::TEST_PARAMS,
&mut out_i,
)
.expect("argon2i");
assert_ne!(out_id, out_d, "Argon2id must differ from Argon2d");
assert_ne!(out_id, out_i, "Argon2id must differ from Argon2i");
assert_ne!(out_d, out_i, "Argon2d must differ from Argon2i");
}
#[test]
fn argon2d_short_salt_errors() {
let mut out = [0u8; 32];
let result = argon2d_derive(b"password", b"short", Argon2Params::TEST_PARAMS, &mut out);
assert_eq!(result, Err(CryptoError::BadInput));
}
#[test]
fn argon2d_empty_output_errors() {
let result = argon2d_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut []);
assert_eq!(result, Err(CryptoError::BadInput));
}
#[test]
fn argon2i_short_salt_errors() {
let mut out = [0u8; 32];
let result = argon2i_derive(b"password", b"short", Argon2Params::TEST_PARAMS, &mut out);
assert_eq!(result, Err(CryptoError::BadInput));
}
#[test]
fn argon2i_empty_output_errors() {
let result = argon2i_derive(b"password", TEST_SALT, Argon2Params::TEST_PARAMS, &mut []);
assert_eq!(result, Err(CryptoError::BadInput));
}
}