#![allow(clippy::indexing_slicing)]
#![allow(clippy::unwrap_used)]
use alloc::vec::Vec;
use core::fmt;
use crate::{
hashes::crypto::blake2b::{Blake2b, Blake2b512},
traits::{Digest as _, VerificationError, ct},
};
#[cfg(target_arch = "aarch64")]
mod aarch64;
mod dispatch;
mod kernels;
#[cfg(target_arch = "powerpc64")]
mod power;
#[cfg(target_arch = "riscv64")]
mod riscv64;
#[cfg(target_arch = "s390x")]
mod s390x;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "x86_64")]
mod x86_64;
use dispatch::active_compress;
pub use dispatch::{ALL_KERNELS, KernelId, required_caps};
use kernels::CompressFn;
pub const BLOCK_SIZE: usize = 1024;
const BLOCK_WORDS: usize = BLOCK_SIZE / 8;
const SYNC_POINTS: u32 = 4;
pub const MIN_SALT_LEN: usize = 8;
const MAX_VAR_BYTES: u64 = u32::MAX as u64;
pub const MIN_OUTPUT_LEN: usize = 4;
const DEFAULT_MEMORY_KIB: u32 = 19 * 1024;
const DEFAULT_TIME_COST: u32 = 2;
const DEFAULT_PARALLELISM: u32 = 1;
const DEFAULT_OUTPUT_LEN: usize = 32;
const P_LANE_WORDS: usize = 16;
#[repr(align(64))]
#[derive(Clone, Copy)]
struct MemoryBlock([u64; BLOCK_WORDS]);
impl MemoryBlock {
#[inline(always)]
const fn zero() -> Self {
Self([0u64; BLOCK_WORDS])
}
}
#[inline]
fn zeroize_u64_slice_no_fence(words: &mut [u64]) {
let mut chunks = words.chunks_exact_mut(8);
for chunk in &mut chunks {
unsafe { core::ptr::write_volatile(chunk.as_mut_ptr().cast::<[u64; 8]>(), [0u64; 8]) };
}
for w in chunks.into_remainder() {
unsafe { core::ptr::write_volatile(w, 0) };
}
}
#[inline]
fn zeroize_u64_slice(words: &mut [u64]) {
zeroize_u64_slice_no_fence(words);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Argon2Variant {
Argon2d,
Argon2i,
Argon2id,
}
impl Argon2Variant {
#[inline]
const fn y(self) -> u32 {
match self {
Self::Argon2d => 0,
Self::Argon2i => 1,
Self::Argon2id => 2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Argon2Version {
V0x10,
#[default]
V0x13,
}
impl Argon2Version {
#[inline]
const fn as_u32(self) -> u32 {
match self {
Self::V0x10 => 0x10,
Self::V0x13 => 0x13,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Argon2Error {
InvalidTimeCost,
InvalidMemoryCost,
InvalidParallelism,
InvalidOutputLen,
SaltTooShort,
SaltTooLong,
PasswordTooLong,
SecretTooLong,
AssociatedDataTooLong,
EntropyUnavailable,
}
impl fmt::Display for Argon2Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidTimeCost => "Argon2 time_cost must be at least 1",
Self::InvalidMemoryCost => "Argon2 memory_cost is out of range (m >= 8 * p, m <= 2^32 - 1)",
Self::InvalidParallelism => "Argon2 parallelism must be in 1..=2^24-1",
Self::InvalidOutputLen => "Argon2 output length must be in 4..=2^32-1",
Self::SaltTooShort => "Argon2 salt must be at least 8 bytes",
Self::SaltTooLong => "Argon2 salt exceeds 2^32-1 bytes",
Self::PasswordTooLong => "Argon2 password exceeds 2^32-1 bytes",
Self::SecretTooLong => "Argon2 secret exceeds 2^32-1 bytes",
Self::AssociatedDataTooLong => "Argon2 associated data exceeds 2^32-1 bytes",
Self::EntropyUnavailable => "Argon2 entropy source unavailable",
})
}
}
impl core::error::Error for Argon2Error {}
#[derive(Clone)]
pub struct Argon2Params {
time_cost: u32,
memory_cost_kib: u32,
parallelism: u32,
output_len: u32,
version: Argon2Version,
secret: Vec<u8>,
associated_data: Vec<u8>,
}
impl fmt::Debug for Argon2Params {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Argon2Params")
.field("time_cost", &self.time_cost)
.field("memory_cost_kib", &self.memory_cost_kib)
.field("parallelism", &self.parallelism)
.field("output_len", &self.output_len)
.field("version", &self.version)
.field("secret_len", &self.secret.len())
.field("associated_data_len", &self.associated_data.len())
.finish()
}
}
impl Drop for Argon2Params {
fn drop(&mut self) {
if !self.secret.is_empty() {
ct::zeroize(&mut self.secret);
}
if !self.associated_data.is_empty() {
ct::zeroize(&mut self.associated_data);
}
}
}
impl Default for Argon2Params {
fn default() -> Self {
Self::new()
}
}
impl Argon2Params {
#[must_use]
pub fn new() -> Self {
Self {
time_cost: DEFAULT_TIME_COST,
memory_cost_kib: DEFAULT_MEMORY_KIB,
parallelism: DEFAULT_PARALLELISM,
output_len: DEFAULT_OUTPUT_LEN as u32,
version: Argon2Version::V0x13,
secret: Vec::new(),
associated_data: Vec::new(),
}
}
#[must_use]
pub const fn time_cost(mut self, t: u32) -> Self {
self.time_cost = t;
self
}
#[must_use]
pub const fn memory_cost_kib(mut self, m: u32) -> Self {
self.memory_cost_kib = m;
self
}
#[must_use]
pub const fn parallelism(mut self, p: u32) -> Self {
self.parallelism = p;
self
}
#[must_use]
pub const fn output_len(mut self, t: u32) -> Self {
self.output_len = t;
self
}
#[must_use]
pub const fn version(mut self, v: Argon2Version) -> Self {
self.version = v;
self
}
#[must_use]
pub fn secret(mut self, secret: &[u8]) -> Self {
ct::zeroize(&mut self.secret);
self.secret.clear();
self.secret.extend_from_slice(secret);
self
}
#[must_use]
pub fn associated_data(mut self, data: &[u8]) -> Self {
ct::zeroize(&mut self.associated_data);
self.associated_data.clear();
self.associated_data.extend_from_slice(data);
self
}
pub fn build(self) -> Result<Self, Argon2Error> {
self.validate()?;
Ok(self)
}
pub fn validate(&self) -> Result<(), Argon2Error> {
if self.time_cost < 1 {
return Err(Argon2Error::InvalidTimeCost);
}
if self.parallelism < 1 || self.parallelism > (1 << 24) - 1 {
return Err(Argon2Error::InvalidParallelism);
}
let min_memory = self.parallelism.checked_mul(8).ok_or(Argon2Error::InvalidMemoryCost)?;
if self.memory_cost_kib < min_memory {
return Err(Argon2Error::InvalidMemoryCost);
}
if (self.output_len as usize) < MIN_OUTPUT_LEN {
return Err(Argon2Error::InvalidOutputLen);
}
if self.secret.len() as u64 > MAX_VAR_BYTES {
return Err(Argon2Error::SecretTooLong);
}
if self.associated_data.len() as u64 > MAX_VAR_BYTES {
return Err(Argon2Error::AssociatedDataTooLong);
}
Ok(())
}
fn check_inputs(&self, password: &[u8], salt: &[u8]) -> Result<(), Argon2Error> {
if password.len() as u64 > MAX_VAR_BYTES {
return Err(Argon2Error::PasswordTooLong);
}
if salt.len() < MIN_SALT_LEN {
return Err(Argon2Error::SaltTooShort);
}
if salt.len() as u64 > MAX_VAR_BYTES {
return Err(Argon2Error::SaltTooLong);
}
Ok(())
}
#[must_use]
pub const fn get_time_cost(&self) -> u32 {
self.time_cost
}
#[must_use]
pub const fn get_memory_cost_kib(&self) -> u32 {
self.memory_cost_kib
}
#[must_use]
pub const fn get_parallelism(&self) -> u32 {
self.parallelism
}
#[must_use]
pub const fn get_output_len(&self) -> u32 {
self.output_len
}
#[must_use]
pub const fn get_version(&self) -> Argon2Version {
self.version
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Argon2VerifyPolicy {
pub max_memory_cost_kib: u32,
pub max_time_cost: u32,
pub max_parallelism: u32,
pub max_output_len: usize,
}
impl Argon2VerifyPolicy {
#[must_use]
pub const fn new(max_memory_cost_kib: u32, max_time_cost: u32, max_parallelism: u32, max_output_len: usize) -> Self {
Self {
max_memory_cost_kib,
max_time_cost,
max_parallelism,
max_output_len,
}
}
#[must_use]
pub const fn allows(&self, params: &Argon2Params, output_len: usize) -> bool {
params.memory_cost_kib <= self.max_memory_cost_kib
&& params.time_cost <= self.max_time_cost
&& params.parallelism <= self.max_parallelism
&& output_len <= self.max_output_len
}
}
impl Default for Argon2VerifyPolicy {
fn default() -> Self {
Self::new(
DEFAULT_MEMORY_KIB,
DEFAULT_TIME_COST,
DEFAULT_PARALLELISM,
DEFAULT_OUTPUT_LEN,
)
}
}
#[cfg(feature = "diag")]
#[must_use]
pub fn diag_active_kernel() -> KernelId {
dispatch::active_kernel()
}
#[cfg(feature = "diag")]
pub fn diag_hash_active(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
argon2_hash(params, password, salt, variant, out)
}
#[cfg(feature = "diag")]
pub fn diag_hash_portable(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::Portable),
)
}
#[cfg(all(feature = "diag", target_arch = "aarch64"))]
pub fn diag_hash_aarch64_neon(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::Aarch64Neon),
)
}
#[cfg(all(feature = "diag", target_arch = "x86_64"))]
pub fn diag_hash_x86_avx2(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::X86Avx2)),
"AVX2 not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::X86Avx2),
)
}
#[cfg(all(feature = "diag", target_arch = "x86_64"))]
pub fn diag_hash_x86_avx512(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::X86Avx512)),
"AVX-512F + AVX-512VL not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::X86Avx512),
)
}
#[cfg(all(feature = "diag", target_arch = "powerpc64"))]
pub fn diag_hash_power_vsx(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::PowerVsx)),
"POWER VSX not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::PowerVsx),
)
}
#[cfg(all(feature = "diag", target_arch = "s390x"))]
pub fn diag_hash_s390x_vector(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::S390xVector)),
"s390x vector facility not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::S390xVector),
)
}
#[cfg(all(feature = "diag", target_arch = "riscv64"))]
pub fn diag_hash_riscv64_v(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::Riscv64V)),
"RISC-V V extension not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::Riscv64V),
)
}
#[cfg(all(feature = "diag", target_arch = "wasm32"))]
pub fn diag_hash_wasm_simd128(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::WasmSimd128)),
"wasm simd128 not available on host"
);
argon2_hash_with_kernel(
params,
password,
salt,
variant,
out,
dispatch::compress_fn_for(KernelId::WasmSimd128),
)
}
#[cfg(feature = "diag")]
pub fn diag_compress_portable(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
unsafe { kernels::compress_portable(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "aarch64"))]
pub fn diag_compress_aarch64_neon(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
unsafe { aarch64::compress_neon(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "x86_64"))]
pub fn diag_compress_x86_avx2(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::X86Avx2)),
"AVX2 not available on host"
);
unsafe { x86_64::compress_avx2(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "x86_64"))]
pub fn diag_compress_x86_avx512(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::X86Avx512)),
"AVX-512F + AVX-512VL not available on host"
);
unsafe { x86_64::compress_avx512(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "powerpc64"))]
pub fn diag_compress_power_vsx(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::PowerVsx)),
"POWER VSX not available on host"
);
unsafe { power::compress_vsx(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "s390x"))]
pub fn diag_compress_s390x_vector(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::S390xVector)),
"s390x vector facility not available on host"
);
unsafe { s390x::compress_vector(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "riscv64"))]
pub fn diag_compress_riscv64_v(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::Riscv64V)),
"RISC-V V extension not available on host"
);
unsafe { riscv64::compress_rvv(dst, x, y, xor_into) }
}
#[cfg(all(feature = "diag", target_arch = "wasm32"))]
pub fn diag_compress_wasm_simd128(
dst: &mut [u64; BLOCK_WORDS],
x: &[u64; BLOCK_WORDS],
y: &[u64; BLOCK_WORDS],
xor_into: bool,
) {
assert!(
crate::platform::caps().has(dispatch::required_caps(KernelId::WasmSimd128)),
"wasm simd128 not available on host"
);
unsafe { wasm::compress_simd128(dst, x, y, xor_into) }
}
#[cfg(feature = "diag")]
pub const DIAG_BLOCK_WORDS: usize = BLOCK_WORDS;
fn h_prime(input_parts: &[&[u8]], out: &mut [u8]) {
let out_len = out.len();
assert!(out_len > 0, "H' output length must be positive");
let len_le = u32::try_from(out_len)
.unwrap_or_else(|_| unreachable!("Argon2 H' out_len <= u32::MAX, validated by Argon2Params::validate"))
.to_le_bytes();
if out_len <= 64 {
let mut hasher =
Blake2b::new(u8::try_from(out_len).unwrap_or_else(|_| unreachable!("guarded by `out_len <= 64` above")));
hasher.update(&len_le);
for part in input_parts {
hasher.update(part);
}
hasher.finalize_into(out);
return;
}
let r = (out_len.strict_add(31) / 32).strict_sub(2);
let mut v_prev: [u8; 64] = {
let mut hasher = Blake2b512::new();
hasher.update(&len_le);
for part in input_parts {
hasher.update(part);
}
hasher.finalize()
};
out[..32].copy_from_slice(&v_prev[..32]);
for i in 1..r {
let v_next = Blake2b512::digest(&v_prev);
out[i.strict_mul(32)..i.strict_mul(32).strict_add(32)].copy_from_slice(&v_next[..32]);
v_prev = v_next;
}
let tail_off = r.strict_mul(32);
let tail_len = out_len.strict_sub(tail_off);
let mut hasher = Blake2b::new(
u8::try_from(tail_len)
.unwrap_or_else(|_| unreachable!("tail_len ∈ (32, 64] by construction (out_len > 64, r = ⌈out_len/32⌉ - 2)")),
);
hasher.update(&v_prev);
hasher.finalize_into(&mut out[tail_off..]);
ct::zeroize(&mut v_prev);
}
fn compute_h0(params: &Argon2Params, password: &[u8], salt: &[u8], variant: Argon2Variant) -> [u8; 64] {
let len_u32 = |label: &'static str, len: usize| -> [u8; 4] {
u32::try_from(len)
.unwrap_or_else(|_| panic!("Argon2 H0: {label} length exceeded MAX_VAR_BYTES; check_inputs should have rejected"))
.to_le_bytes()
};
let mut hasher = Blake2b512::new();
hasher.update(¶ms.parallelism.to_le_bytes());
hasher.update(¶ms.output_len.to_le_bytes());
hasher.update(¶ms.memory_cost_kib.to_le_bytes());
hasher.update(¶ms.time_cost.to_le_bytes());
hasher.update(¶ms.version.as_u32().to_le_bytes());
hasher.update(&variant.y().to_le_bytes());
hasher.update(&len_u32("password", password.len()));
hasher.update(password);
hasher.update(&len_u32("salt", salt.len()));
hasher.update(salt);
hasher.update(&len_u32("secret", params.secret.len()));
hasher.update(¶ms.secret);
hasher.update(&len_u32("associated_data", params.associated_data.len()));
hasher.update(¶ms.associated_data);
hasher.finalize()
}
#[inline(always)]
fn block_from_bytes(bytes: &[u8; BLOCK_SIZE]) -> MemoryBlock {
let mut out = MemoryBlock::zero();
for i in 0..BLOCK_WORDS {
out.0[i] = u64::from_le_bytes(
bytes[i.strict_mul(8)..i.strict_mul(8).strict_add(8)]
.try_into()
.unwrap(),
);
}
out
}
#[inline(always)]
fn block_to_bytes(block: &[u64; BLOCK_WORDS]) -> [u8; BLOCK_SIZE] {
let mut out = [0u8; BLOCK_SIZE];
for i in 0..BLOCK_WORDS {
out[i.strict_mul(8)..i.strict_mul(8).strict_add(8)].copy_from_slice(&block[i].to_le_bytes());
}
out
}
#[derive(Clone)]
struct AddressBlock {
words: MemoryBlock,
}
impl AddressBlock {
fn zeros() -> Self {
Self {
words: MemoryBlock::zero(),
}
}
#[allow(clippy::too_many_arguments)] fn refresh(
&mut self,
compress: CompressFn,
pass: u32,
lane: u32,
slice: u32,
blocks: u32,
total_passes: u32,
variant_y: u32,
counter: u64,
) {
let mut input = MemoryBlock::zero();
input.0[0] = pass as u64;
input.0[1] = lane as u64;
input.0[2] = slice as u64;
input.0[3] = blocks as u64;
input.0[4] = total_passes as u64;
input.0[5] = variant_y as u64;
input.0[6] = counter;
let mut zero = MemoryBlock::zero();
let mut intermediate = MemoryBlock::zero();
unsafe {
compress(&mut intermediate.0, &zero.0, &input.0, false);
compress(&mut self.words.0, &zero.0, &intermediate.0, false);
}
zeroize_u64_slice_no_fence(&mut zero.0);
zeroize_u64_slice_no_fence(&mut intermediate.0);
zeroize_u64_slice_no_fence(&mut input.0);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
struct Matrix {
blocks: Vec<MemoryBlock>,
lane_len: u32,
lanes: u32,
segment_len: u32,
}
impl Matrix {
fn new(mem_kib: u32, lanes: u32) -> Result<Self, Argon2Error> {
let four_p = lanes.strict_mul(SYNC_POINTS);
let m_prime = mem_kib / four_p * four_p;
let lane_len = m_prime / lanes;
let segment_len = lane_len / SYNC_POINTS;
let total = m_prime as usize;
let mut blocks = Vec::new();
blocks
.try_reserve_exact(total)
.map_err(|_| Argon2Error::InvalidMemoryCost)?;
blocks.resize(total, MemoryBlock::zero());
Ok(Self {
blocks,
lane_len,
lanes,
segment_len,
})
}
#[inline(always)]
fn len(&self) -> usize {
self.blocks.len()
}
#[inline(always)]
fn index(&self, lane: u32, col: u32) -> usize {
(lane as usize)
.strict_mul(self.lane_len as usize)
.strict_add(col as usize)
}
#[inline(always)]
fn get(&self, lane: u32, col: u32) -> &[u64; BLOCK_WORDS] {
let idx = self.index(lane, col);
self.get_index(idx)
}
#[inline(always)]
fn get_index(&self, idx: usize) -> &[u64; BLOCK_WORDS] {
&self.blocks[idx].0
}
#[inline(always)]
fn set(&mut self, lane: u32, col: u32, block: MemoryBlock) {
let idx = self.index(lane, col);
self.blocks[idx] = block;
}
}
impl Drop for Matrix {
fn drop(&mut self) {
for block in &mut self.blocks {
zeroize_u64_slice_no_fence(&mut block.0);
}
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
#[derive(Clone, Copy)]
struct MatrixView {
ptr: *mut MemoryBlock,
total_len: usize,
}
#[cfg(feature = "parallel")]
unsafe impl Send for MatrixView {}
#[cfg(feature = "parallel")]
unsafe impl Sync for MatrixView {}
impl MatrixView {
#[inline(always)]
fn from_blocks(blocks: &mut [MemoryBlock]) -> Self {
Self {
ptr: blocks.as_mut_ptr(),
total_len: blocks.len(),
}
}
#[inline(always)]
unsafe fn block<'a>(self, idx: usize) -> &'a [u64; BLOCK_WORDS] {
debug_assert!(idx < self.total_len);
unsafe { &(*self.ptr.add(idx)).0 }
}
#[inline(always)]
unsafe fn block_mut<'a>(self, idx: usize) -> &'a mut [u64; BLOCK_WORDS] {
debug_assert!(idx < self.total_len);
unsafe { &mut (*self.ptr.add(idx)).0 }
}
}
#[allow(clippy::too_many_arguments)] #[inline(always)]
fn reference_index(
pass: u32,
lane: u32,
slice: u32,
col: u32,
j1: u32,
j2: u32,
lanes: u32,
segment_len: u32,
lane_len: u32,
) -> (u32, u32) {
let ref_lane = if pass == 0 && slice == 0 { lane } else { j2 % lanes };
let same_lane = ref_lane == lane;
let position_in_segment = col.wrapping_sub(slice.wrapping_mul(segment_len));
let area_size: u32 = if pass == 0 {
if same_lane {
col.wrapping_sub(1)
} else {
let completed_slices = slice.wrapping_mul(segment_len);
if position_in_segment == 0 {
completed_slices.wrapping_sub(1)
} else {
completed_slices
}
}
} else {
if same_lane {
lane_len
.wrapping_sub(segment_len)
.wrapping_add(position_in_segment)
.wrapping_sub(1)
} else {
let base = lane_len.wrapping_sub(segment_len);
if position_in_segment == 0 {
base.wrapping_sub(1)
} else {
base
}
}
};
let j1_u64 = j1 as u64;
let relative_position = {
let x = (j1_u64.wrapping_mul(j1_u64)) >> 32;
let y = (area_size as u64).wrapping_mul(x) >> 32;
(area_size as u64).wrapping_sub(1).wrapping_sub(y) as u32
};
let start_position = if pass == 0 || slice == (SYNC_POINTS - 1) {
0
} else {
(slice.wrapping_add(1)).wrapping_mul(segment_len)
};
let ref_index = (start_position.wrapping_add(relative_position)) % lane_len;
(ref_lane, ref_index)
}
#[allow(clippy::too_many_arguments)] fn fill_segment(
matrix: &mut Matrix,
compress: CompressFn,
pass: u32,
lane: u32,
slice: u32,
variant: Argon2Variant,
version: Argon2Version,
time_cost: u32,
) {
let lanes = matrix.lanes;
let segment_len = matrix.segment_len;
let lane_len = matrix.lane_len;
let total_blocks = matrix.len() as u32;
let view = MatrixView::from_blocks(&mut matrix.blocks);
unsafe {
fill_segment_inner(
view,
compress,
pass,
lane,
slice,
lanes,
segment_len,
lane_len,
total_blocks,
variant,
version,
time_cost,
);
}
}
#[allow(clippy::too_many_arguments, clippy::doc_lazy_continuation)]
unsafe fn fill_segment_inner(
view: MatrixView,
compress: CompressFn,
pass: u32,
lane: u32,
slice: u32,
lanes: u32,
segment_len: u32,
lane_len: u32,
total_blocks: u32,
variant: Argon2Variant,
version: Argon2Version,
time_cost: u32,
) {
let variant_y = variant.y();
let lane_len_usize = lane_len as usize;
let lane_base = (lane as usize).strict_mul(lane_len_usize);
let is_independent = match variant {
Argon2Variant::Argon2i => true,
Argon2Variant::Argon2id => pass == 0 && slice < 2,
Argon2Variant::Argon2d => false,
};
let mut address_block = AddressBlock::zeros();
let mut address_counter: u64 = 0;
if is_independent {
address_counter = 1; address_block.refresh(
compress,
pass,
lane,
slice,
total_blocks,
time_cost,
variant_y,
address_counter,
);
}
let starting_col: u32 = if pass == 0 && slice == 0 { 2 } else { 0 };
let segment_start = slice.strict_mul(segment_len);
let first_col = segment_start.strict_add(starting_col);
let mut prev_idx = if first_col == 0 {
lane_base.strict_add(lane_len_usize.strict_sub(1))
} else {
lane_base.strict_add(first_col.strict_sub(1) as usize)
};
for seg_col in starting_col..segment_len {
let col = segment_start.strict_add(seg_col);
let cur_idx = lane_base.strict_add(col as usize);
let (j1, j2) = if is_independent {
let addr_pos = (seg_col as usize) % BLOCK_WORDS;
if addr_pos == 0 && seg_col != 0 {
address_counter = address_counter.strict_add(1);
address_block.refresh(
compress,
pass,
lane,
slice,
total_blocks,
time_cost,
variant_y,
address_counter,
);
}
let word = address_block.words.0[addr_pos];
((word & 0xFFFF_FFFFu64) as u32, (word >> 32) as u32)
} else {
let prev_block = unsafe { view.block(prev_idx) };
let word = prev_block[0];
((word & 0xFFFF_FFFFu64) as u32, (word >> 32) as u32)
};
let (ref_lane, ref_index) = reference_index(pass, lane, slice, col, j1, j2, lanes, segment_len, lane_len);
let xor_into = pass > 0 && version == Argon2Version::V0x13;
let ref_idx = (ref_lane as usize)
.strict_mul(lane_len_usize)
.strict_add(ref_index as usize);
unsafe {
let prev_block = view.block(prev_idx);
let ref_block = view.block(ref_idx);
let dst = view.block_mut(cur_idx);
compress(dst, prev_block, ref_block, xor_into);
}
prev_idx = cur_idx;
}
}
#[inline]
fn fill_slice_sequential(
matrix: &mut Matrix,
compress: CompressFn,
pass: u32,
slice: u32,
variant: Argon2Variant,
version: Argon2Version,
time_cost: u32,
) {
let lanes = matrix.lanes;
for lane in 0..lanes {
fill_segment(matrix, compress, pass, lane, slice, variant, version, time_cost);
}
}
#[cfg(feature = "parallel")]
fn fill_slice_parallel(
matrix: &mut Matrix,
compress: CompressFn,
pass: u32,
slice: u32,
variant: Argon2Variant,
version: Argon2Version,
time_cost: u32,
) {
let lanes = matrix.lanes;
let segment_len = matrix.segment_len;
let lane_len = matrix.lane_len;
let total_blocks = matrix.len() as u32;
let view = MatrixView::from_blocks(&mut matrix.blocks);
rayon::scope(|s| {
for lane in 1..lanes {
s.spawn(move |_| {
unsafe {
fill_segment_inner(
view,
compress,
pass,
lane,
slice,
lanes,
segment_len,
lane_len,
total_blocks,
variant,
version,
time_cost,
);
}
});
}
unsafe {
fill_segment_inner(
view,
compress,
pass,
0,
slice,
lanes,
segment_len,
lane_len,
total_blocks,
variant,
version,
time_cost,
);
}
});
}
#[inline]
fn fill_slice(
matrix: &mut Matrix,
compress: CompressFn,
pass: u32,
slice: u32,
variant: Argon2Variant,
version: Argon2Version,
time_cost: u32,
) {
#[cfg(feature = "parallel")]
if matrix.lanes > 1 {
fill_slice_parallel(matrix, compress, pass, slice, variant, version, time_cost);
return;
}
fill_slice_sequential(matrix, compress, pass, slice, variant, version, time_cost);
}
fn argon2_hash(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
) -> Result<(), Argon2Error> {
argon2_hash_with_kernel(params, password, salt, variant, out, active_compress())
}
fn argon2_hash_with_kernel(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
variant: Argon2Variant,
out: &mut [u8],
compress: CompressFn,
) -> Result<(), Argon2Error> {
params.validate()?;
params.check_inputs(password, salt)?;
if out.len() < MIN_OUTPUT_LEN || out.len() as u64 > MAX_VAR_BYTES || out.len() != params.output_len as usize {
return Err(Argon2Error::InvalidOutputLen);
}
let mut h0 = compute_h0(params, password, salt, variant);
let mut matrix = Matrix::new(params.memory_cost_kib, params.parallelism)?;
let lane_len = matrix.lane_len;
let lanes = matrix.lanes;
for lane in 0..lanes {
let mut buf = [0u8; BLOCK_SIZE];
let lane_le = lane.to_le_bytes();
h_prime(&[&h0, &0u32.to_le_bytes(), &lane_le], &mut buf);
matrix.set(lane, 0, block_from_bytes(&buf));
h_prime(&[&h0, &1u32.to_le_bytes(), &lane_le], &mut buf);
matrix.set(lane, 1, block_from_bytes(&buf));
ct::zeroize(&mut buf);
}
for pass in 0..params.time_cost {
for slice in 0..SYNC_POINTS {
fill_slice(
&mut matrix,
compress,
pass,
slice,
variant,
params.version,
params.time_cost,
);
}
}
let mut acc = *matrix.get(0, lane_len - 1);
for lane in 1..lanes {
let blk = matrix.get(lane, lane_len - 1);
for i in 0..BLOCK_WORDS {
acc[i] ^= blk[i];
}
}
let mut acc_bytes = block_to_bytes(&acc);
h_prime(&[&acc_bytes], out);
ct::zeroize(&mut h0);
zeroize_u64_slice(&mut acc);
ct::zeroize(&mut acc_bytes);
Ok(())
}
macro_rules! define_argon2_variant {
(
$(#[$meta:meta])*
$name:ident { variant: $variant:expr, phc_algorithm: $phc_alg:literal }
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, Default)]
pub struct $name;
impl $name {
pub const ALGORITHM: &'static str = $phc_alg;
pub fn hash(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
out: &mut [u8],
) -> Result<(), Argon2Error> {
argon2_hash(params, password, salt, $variant, out)
}
pub fn hash_array<const N: usize>(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
) -> Result<[u8; N], Argon2Error> {
let mut out = [0u8; N];
Self::hash(params, password, salt, &mut out)?;
Ok(out)
}
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
expected: &[u8],
) -> Result<(), VerificationError> {
let actual_len = params.output_len as usize;
let mut actual = alloc::vec![0u8; actual_len];
let hash_failed = Self::hash(params, password, salt, &mut actual).is_err();
let bytes_match = ct::constant_time_eq(&actual, expected);
ct::zeroize(&mut actual);
let success = !hash_failed & bytes_match;
if core::hint::black_box(success) {
Ok(())
} else {
Err(VerificationError::new())
}
}
#[cfg(feature = "phc-strings")]
pub fn hash_string_with_salt(
params: &Argon2Params,
password: &[u8],
salt: &[u8],
) -> Result<alloc::string::String, Argon2Error> {
let mut hash = alloc::vec![0u8; params.output_len as usize];
Self::hash(params, password, salt, &mut hash)?;
let encoded = phc_integration::encode_string(Self::ALGORITHM, params, salt, &hash);
ct::zeroize(&mut hash);
Ok(encoded)
}
#[cfg(all(feature = "phc-strings", feature = "getrandom"))]
pub fn hash_string(
params: &Argon2Params,
password: &[u8],
) -> Result<alloc::string::String, Argon2Error> {
let mut salt = [0u8; 16];
getrandom::fill(&mut salt).map_err(|_| Argon2Error::EntropyUnavailable)?;
Self::hash_string_with_salt(params, password, &salt)
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string(
password: &[u8],
encoded: &str,
) -> Result<(), VerificationError> {
Self::verify_string_with_context(password, encoded, &[], &[])
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string_with_policy(
password: &[u8],
encoded: &str,
policy: &Argon2VerifyPolicy,
) -> Result<(), VerificationError> {
Self::verify_string_with_context_and_policy(password, encoded, &[], &[], policy)
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string_with_context(
password: &[u8],
encoded: &str,
secret: &[u8],
associated_data: &[u8],
) -> Result<(), VerificationError> {
Self::verify_string_with_context_and_policy(
password,
encoded,
secret,
associated_data,
&Argon2VerifyPolicy::new(u32::MAX, u32::MAX, u32::MAX, usize::MAX),
)
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string_with_context_and_policy(
password: &[u8],
encoded: &str,
secret: &[u8],
associated_data: &[u8],
policy: &Argon2VerifyPolicy,
) -> Result<(), VerificationError> {
let parsed = phc_integration::decode_string(encoded, Self::ALGORITHM)
.map_err(|_| VerificationError::new())?;
let mut params = parsed.params;
if !secret.is_empty() {
params = params.secret(secret);
}
if !associated_data.is_empty() {
params = params.associated_data(associated_data);
}
params = params.build().map_err(|_| VerificationError::new())?;
if !policy.allows(¶ms, parsed.hash.len()) {
return Err(VerificationError::new());
}
let mut actual = alloc::vec![0u8; parsed.hash.len()];
if Self::hash(¶ms, password, &parsed.salt, &mut actual).is_err() {
ct::zeroize(&mut actual);
return Err(VerificationError::new());
}
let ok = ct::constant_time_eq(&actual, &parsed.hash);
ct::zeroize(&mut actual);
if core::hint::black_box(ok) {
Ok(())
} else {
Err(VerificationError::new())
}
}
#[cfg(feature = "phc-strings")]
pub fn decode_string(
encoded: &str,
) -> Result<(Argon2Params, alloc::vec::Vec<u8>, alloc::vec::Vec<u8>), crate::auth::phc::PhcError> {
let parsed = phc_integration::decode_string(encoded, Self::ALGORITHM)?;
Ok((parsed.params, parsed.salt, parsed.hash))
}
}
};
}
define_argon2_variant! {
Argon2d { variant: Argon2Variant::Argon2d, phc_algorithm: "argon2d" }
}
define_argon2_variant! {
Argon2i { variant: Argon2Variant::Argon2i, phc_algorithm: "argon2i" }
}
define_argon2_variant! {
Argon2id { variant: Argon2Variant::Argon2id, phc_algorithm: "argon2id" }
}
#[cfg(feature = "phc-strings")]
mod phc_integration {
use alloc::{string::String, vec::Vec};
use super::{Argon2Params, Argon2Version};
use crate::auth::phc::{self, PhcError};
pub(super) struct ParsedPhc {
pub params: Argon2Params,
pub salt: Vec<u8>,
pub hash: Vec<u8>,
}
pub(super) fn encode_string(algorithm: &str, params: &Argon2Params, salt: &[u8], hash: &[u8]) -> String {
let mut out = String::new();
out.push('$');
out.push_str(algorithm);
out.push_str("$v=");
phc::push_u32_decimal(&mut out, params.get_version().as_u32());
out.push_str("$m=");
phc::push_u32_decimal(&mut out, params.get_memory_cost_kib());
out.push_str(",t=");
phc::push_u32_decimal(&mut out, params.get_time_cost());
out.push_str(",p=");
phc::push_u32_decimal(&mut out, params.get_parallelism());
out.push('$');
phc::base64_encode_into(salt, &mut out);
out.push('$');
phc::base64_encode_into(hash, &mut out);
out
}
pub(super) fn decode_string(encoded: &str, expected_algorithm: &str) -> Result<ParsedPhc, PhcError> {
let parts = phc::parse(encoded)?;
if parts.algorithm != expected_algorithm {
return Err(PhcError::AlgorithmMismatch);
}
let version = match parts.version {
Some(v) => match phc::parse_param_u32(v)? {
0x10 => Argon2Version::V0x10,
0x13 => Argon2Version::V0x13,
_ => return Err(PhcError::UnsupportedVersion),
},
None => Argon2Version::V0x13, };
let mut memory_kib: Option<u32> = None;
let mut time_cost: Option<u32> = None;
let mut parallelism: Option<u32> = None;
for pair in phc::PhcParamIter::new(parts.parameters) {
let (k, v) = pair?;
let value = phc::parse_param_u32(v)?;
match k {
"m" => {
if memory_kib.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
"t" => {
if time_cost.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
"p" => {
if parallelism.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
_ => return Err(PhcError::UnknownParam),
}
}
let m = memory_kib.ok_or(PhcError::MissingParam)?;
let t = time_cost.ok_or(PhcError::MissingParam)?;
let p = parallelism.ok_or(PhcError::MissingParam)?;
let salt = phc::decode_base64_to_vec(parts.salt_b64)?;
let hash = phc::decode_base64_to_vec(parts.hash_b64)?;
if salt.len() < super::MIN_SALT_LEN || hash.len() < super::MIN_OUTPUT_LEN {
return Err(PhcError::InvalidLength);
}
let params = Argon2Params::new()
.memory_cost_kib(m)
.time_cost(t)
.parallelism(p)
.output_len(hash.len() as u32)
.version(version);
let params = params.build().map_err(|_| PhcError::ParamOutOfRange)?;
Ok(ParsedPhc { params, salt, hash })
}
}
#[cfg(test)]
mod tests {
#[cfg(not(miri))]
use alloc::vec;
use super::*;
#[cfg(not(miri))]
const PASSWORD: &[u8] = &[0x01u8; 32];
#[cfg(not(miri))]
const SALT: &[u8] = &[0x02u8; 16];
#[cfg(not(miri))]
const SECRET: &[u8] = &[0x03u8; 8];
#[cfg(not(miri))]
const AD: &[u8] = &[0x04u8; 12];
#[cfg(not(miri))]
fn canon_params() -> Argon2Params {
Argon2Params::new()
.memory_cost_kib(32)
.time_cost(3)
.parallelism(4)
.output_len(32)
.version(Argon2Version::V0x13)
.secret(SECRET)
.associated_data(AD)
.build()
.unwrap()
}
#[test]
#[cfg(not(miri))]
fn argon2d_rfc_appendix_a_vector() {
let expected: [u8; 32] = [
0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97, 0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94, 0xf8, 0x68, 0xe3,
0xbe, 0x39, 0x84, 0xf3, 0xc1, 0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb,
];
let mut out = [0u8; 32];
Argon2d::hash(&canon_params(), PASSWORD, SALT, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
#[cfg(not(miri))]
fn argon2i_rfc_appendix_a_vector() {
let expected: [u8; 32] = [
0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa, 0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1, 0xc8, 0xde, 0x6b,
0x01, 0x6d, 0xd3, 0x88, 0xd2, 0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8,
];
let mut out = [0u8; 32];
Argon2i::hash(&canon_params(), PASSWORD, SALT, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
#[cfg(not(miri))]
fn argon2id_rfc_appendix_a_vector() {
let expected: [u8; 32] = [
0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9, 0xd0, 0x1e, 0xf0,
0x45, 0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59,
];
let mut out = [0u8; 32];
Argon2id::hash(&canon_params(), PASSWORD, SALT, &mut out).unwrap();
assert_eq!(out, expected);
}
#[test]
#[cfg(not(miri))]
fn argon2id_verify_accepts_correct() {
let params = canon_params();
let h = Argon2id::hash_array::<32>(¶ms, PASSWORD, SALT).unwrap();
assert!(Argon2id::verify(¶ms, PASSWORD, SALT, &h).is_ok());
}
#[test]
#[cfg(not(miri))]
fn argon2id_verify_rejects_wrong_password() {
let params = canon_params();
let h = Argon2id::hash_array::<32>(¶ms, PASSWORD, SALT).unwrap();
assert!(Argon2id::verify(¶ms, b"wrong_password_wrong_wrong!!wrong", SALT, &h).is_err());
}
#[test]
#[cfg(not(miri))]
fn argon2id_verify_rejects_wrong_salt() {
let params = canon_params();
let h = Argon2id::hash_array::<32>(¶ms, PASSWORD, SALT).unwrap();
let wrong_salt = [0xffu8; 16];
assert!(Argon2id::verify(¶ms, PASSWORD, &wrong_salt, &h).is_err());
}
#[cfg(all(feature = "phc-strings", feature = "getrandom"))]
#[test]
#[cfg(not(miri))]
fn hash_string_uses_random_salt_and_verifies() {
let params = Argon2Params::new()
.memory_cost_kib(32)
.time_cost(1)
.parallelism(1)
.output_len(32)
.build()
.unwrap();
let encoded = Argon2id::hash_string(¶ms, b"password").unwrap();
assert!(Argon2id::verify_string(b"password", &encoded).is_ok());
assert!(Argon2id::verify_string(b"wrong-password", &encoded).is_err());
}
#[test]
fn invalid_params_fail_build() {
assert_eq!(
Argon2Params::new().time_cost(0).build().unwrap_err(),
Argon2Error::InvalidTimeCost
);
assert_eq!(
Argon2Params::new().parallelism(0).build().unwrap_err(),
Argon2Error::InvalidParallelism
);
assert_eq!(
Argon2Params::new()
.parallelism(4)
.memory_cost_kib(16) .build()
.unwrap_err(),
Argon2Error::InvalidMemoryCost
);
assert_eq!(
Argon2Params::new().output_len(3).build().unwrap_err(),
Argon2Error::InvalidOutputLen
);
}
#[test]
fn short_salt_rejected() {
let params = Argon2Params::new().memory_cost_kib(32).parallelism(4).build().unwrap();
let mut out = [0u8; 32];
assert_eq!(
Argon2id::hash(¶ms, b"pw", &[0u8; 7], &mut out).unwrap_err(),
Argon2Error::SaltTooShort
);
}
#[test]
fn output_len_mismatch_rejected() {
let params = Argon2Params::new().memory_cost_kib(32).parallelism(4).build().unwrap();
let mut out = [0u8; 16]; assert_eq!(
Argon2id::hash(¶ms, b"pw", &[0u8; 16], &mut out).unwrap_err(),
Argon2Error::InvalidOutputLen
);
}
#[test]
fn error_traits() {
fn assert_copy<T: Copy>() {}
fn assert_err<T: core::error::Error>() {}
assert_copy::<Argon2Error>();
assert_err::<Argon2Error>();
}
#[cfg(not(miri))]
fn oracle_hash(
algo: argon2::Algorithm,
password: &[u8],
salt: &[u8],
m_kib: u32,
t: u32,
p: u32,
out_len: usize,
) -> vec::Vec<u8> {
let params = argon2::Params::new(m_kib, t, p, Some(out_len)).unwrap();
let ctx = argon2::Argon2::new(algo, argon2::Version::V0x13, params);
let mut out = alloc::vec![0u8; out_len];
ctx.hash_password_into(password, salt, &mut out).unwrap();
out
}
#[test]
#[cfg(not(miri))]
fn argon2d_matches_oracle_small_params() {
let cases: &[(u32, u32, u32, usize)] = &[(8, 1, 1, 16), (16, 2, 1, 32), (32, 3, 2, 32)];
for &(m, t, p, out_len) in cases {
let params = Argon2Params::new()
.memory_cost_kib(m)
.time_cost(t)
.parallelism(p)
.output_len(out_len as u32)
.build()
.unwrap();
let mut actual = alloc::vec![0u8; out_len];
Argon2d::hash(¶ms, b"password", &[0u8; 16], &mut actual).unwrap();
let expected = oracle_hash(argon2::Algorithm::Argon2d, b"password", &[0u8; 16], m, t, p, out_len);
assert_eq!(actual, expected, "argon2d mismatch m={m} t={t} p={p} T={out_len}");
}
}
#[test]
#[cfg(not(miri))]
fn argon2i_matches_oracle_small_params() {
let cases: &[(u32, u32, u32, usize)] = &[(8, 1, 1, 16), (16, 2, 1, 32), (32, 3, 2, 32)];
for &(m, t, p, out_len) in cases {
let params = Argon2Params::new()
.memory_cost_kib(m)
.time_cost(t)
.parallelism(p)
.output_len(out_len as u32)
.build()
.unwrap();
let mut actual = alloc::vec![0u8; out_len];
Argon2i::hash(¶ms, b"password", &[0u8; 16], &mut actual).unwrap();
let expected = oracle_hash(argon2::Algorithm::Argon2i, b"password", &[0u8; 16], m, t, p, out_len);
assert_eq!(actual, expected, "argon2i mismatch m={m} t={t} p={p} T={out_len}");
}
}
#[test]
#[cfg(not(miri))]
fn argon2id_matches_oracle_small_params() {
let cases: &[(u32, u32, u32, usize)] = &[(8, 1, 1, 16), (16, 2, 1, 32), (32, 3, 2, 32), (32, 3, 4, 64)];
for &(m, t, p, out_len) in cases {
let params = Argon2Params::new()
.memory_cost_kib(m)
.time_cost(t)
.parallelism(p)
.output_len(out_len as u32)
.build()
.unwrap();
let mut actual = alloc::vec![0u8; out_len];
Argon2id::hash(¶ms, b"password", &[0u8; 16], &mut actual).unwrap();
let expected = oracle_hash(argon2::Algorithm::Argon2id, b"password", &[0u8; 16], m, t, p, out_len);
assert_eq!(actual, expected, "argon2id mismatch m={m} t={t} p={p} T={out_len}");
}
}
#[test]
fn kernel_id_stringifies() {
assert_eq!(KernelId::Portable.as_str(), "portable");
}
#[test]
fn portable_kernel_has_no_required_caps() {
assert!(required_caps(KernelId::Portable).is_empty());
}
#[cfg(all(feature = "phc-strings", not(miri)))]
mod phc_tests {
use alloc::{string::String, vec};
use super::*;
use crate::auth::phc::PhcError;
fn small_params() -> Argon2Params {
Argon2Params::new()
.memory_cost_kib(32)
.time_cost(2)
.parallelism(1)
.output_len(32)
.build()
.unwrap()
}
#[test]
fn hash_string_with_salt_round_trip_id() {
let params = small_params();
let salt = [0xAAu8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"password", &salt).unwrap();
assert!(encoded.starts_with("$argon2id$v=19$m=32,t=2,p=1$"));
assert!(Argon2id::verify_string(b"password", &encoded).is_ok());
assert!(Argon2id::verify_string(b"wrongpassword", &encoded).is_err());
}
#[test]
fn verify_string_with_policy_enforces_argon2_bounds() {
let params = small_params();
let salt = [0xA1u8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"password", &salt).unwrap();
let allowed = Argon2VerifyPolicy::new(32, 2, 1, 32);
assert!(Argon2id::verify_string_with_policy(b"password", &encoded, &allowed).is_ok());
let low_memory = Argon2VerifyPolicy::new(31, 2, 1, 32);
assert!(Argon2id::verify_string_with_policy(b"password", &encoded, &low_memory).is_err());
let short_output = Argon2VerifyPolicy::new(32, 2, 1, 31);
assert!(Argon2id::verify_string_with_policy(b"password", &encoded, &short_output).is_err());
}
#[test]
fn verify_string_with_context_and_policy_enforces_argon2_bounds() {
let params = small_params().secret(b"pepper").build().unwrap();
let salt = [0xA2u8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"password", &salt).unwrap();
let allowed = Argon2VerifyPolicy::new(32, 2, 1, 32);
assert!(Argon2id::verify_string_with_context_and_policy(b"password", &encoded, b"pepper", &[], &allowed).is_ok());
let low_time = Argon2VerifyPolicy::new(32, 1, 1, 32);
assert!(
Argon2id::verify_string_with_context_and_policy(b"password", &encoded, b"pepper", &[], &low_time).is_err()
);
}
#[test]
fn hash_string_round_trip_d_and_i() {
let params = small_params();
let salt = [0xBBu8; 16];
let encoded_d = Argon2d::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
let encoded_i = Argon2i::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
assert!(encoded_d.starts_with("$argon2d$"));
assert!(encoded_i.starts_with("$argon2i$"));
assert!(Argon2d::verify_string(b"pw", &encoded_d).is_ok());
assert!(Argon2i::verify_string(b"pw", &encoded_i).is_ok());
}
#[test]
fn verify_string_rejects_variant_mismatch() {
let params = small_params();
let salt = [0xCCu8; 16];
let encoded_d = Argon2d::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
assert!(Argon2id::verify_string(b"pw", &encoded_d).is_err());
assert!(Argon2i::verify_string(b"pw", &encoded_d).is_err());
}
#[test]
fn decode_string_extracts_params_salt_hash() {
let params = small_params();
let salt = vec![0xDDu8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
let (decoded_params, decoded_salt, decoded_hash) = Argon2id::decode_string(&encoded).unwrap();
assert_eq!(decoded_params.get_memory_cost_kib(), 32);
assert_eq!(decoded_params.get_time_cost(), 2);
assert_eq!(decoded_params.get_parallelism(), 1);
assert_eq!(decoded_params.get_output_len(), 32);
assert_eq!(decoded_params.get_version(), Argon2Version::V0x13);
assert_eq!(decoded_salt, salt);
assert_eq!(decoded_hash.len(), 32);
let mut rehashed = [0u8; 32];
Argon2id::hash(&decoded_params, b"pw", &decoded_salt, &mut rehashed).unwrap();
assert_eq!(rehashed.as_slice(), decoded_hash.as_slice());
}
#[test]
fn decode_string_rejects_malformed() {
assert_eq!(
Argon2id::decode_string("not a phc string").unwrap_err(),
PhcError::MalformedInput
);
assert_eq!(
Argon2id::decode_string("$argon2id$m=32,t=2,p=1$YWE$aGFzaA").unwrap_err(),
PhcError::InvalidLength
);
assert_eq!(
Argon2id::decode_string("$argon2id$m=32,t=2,p=1$QUFBQUFBQUFBQUFBQUFBQQ$YWE").unwrap_err(),
PhcError::InvalidLength
);
}
#[test]
fn verify_string_rejects_tampered_hash() {
let params = small_params();
let salt = [0xEEu8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
let mut tampered: String = encoded.chars().collect();
let len = tampered.len();
tampered.replace_range(len - 1..len, "A"); let mut matched_tamper = encoded.clone();
let variants = ["A", "B", "C"];
let last_char = encoded.chars().last().unwrap();
for v in variants {
if v.chars().next().unwrap() != last_char {
let mut t = encoded.clone();
let len = t.len();
t.replace_range(len - 1..len, v);
matched_tamper = t;
break;
}
}
assert!(Argon2id::verify_string(b"pw", &matched_tamper).is_err());
}
#[test]
fn decode_string_rejects_duplicate_params() {
let params = small_params();
let encoded = Argon2id::hash_string_with_salt(¶ms, b"pw", &[0xFFu8; 16]).unwrap();
let broken = encoded.replace("t=2", "m=99");
assert_eq!(Argon2id::decode_string(&broken).unwrap_err(), PhcError::DuplicateParam);
}
#[test]
fn decode_string_accepts_missing_version() {
let params = small_params();
let salt = [0x77u8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
let no_v = encoded.replace("$v=19", "");
let (p, s, h) = Argon2id::decode_string(&no_v).unwrap();
assert_eq!(p.get_version(), Argon2Version::V0x13);
assert_eq!(s, salt);
assert_eq!(h.len(), 32);
}
#[test]
fn verify_string_with_context_handles_secret_and_associated_data() {
let params = small_params()
.secret(b"pepper")
.associated_data(b"context")
.build()
.unwrap();
let salt = [0x99u8; 16];
let encoded = Argon2id::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
assert!(Argon2id::verify_string(b"pw", &encoded).is_err());
assert!(Argon2id::verify_string_with_context(b"pw", &encoded, b"pepper", b"context").is_ok());
assert!(Argon2id::verify_string_with_context(b"pw", &encoded, b"wrong", b"context").is_err());
assert!(Argon2id::verify_string_with_context(b"pw", &encoded, b"pepper", b"wrong").is_err());
}
#[test]
fn encoded_format_exact_for_known_vector() {
let params = Argon2Params::new()
.memory_cost_kib(8)
.time_cost(1)
.parallelism(1)
.output_len(16)
.version(Argon2Version::V0x13)
.build()
.unwrap();
let salt = b"exampleSALTvalue"; let encoded = Argon2id::hash_string_with_salt(¶ms, b"password", salt).unwrap();
let segments: alloc::vec::Vec<&str> = encoded.split('$').collect();
assert_eq!(segments[0], "");
assert_eq!(segments[1], "argon2id");
assert_eq!(segments[2], "v=19");
assert_eq!(segments[3], "m=8,t=1,p=1");
assert_eq!(segments[4].len(), 22);
assert_eq!(segments[5].len(), 22);
assert_eq!(segments.len(), 6);
}
}
}