use alloc::string::String;
use alloc::vec::Vec;
use crate::blake2b::{Blake2b, blake2b_long};
use crate::block::{Block, Instance, Position};
use crate::error::Error;
use crate::fill_block::{Backend, FillSegmentFn};
use crate::memory::{Arena, Workspace, clear_internal_memory, clear_internal_memory_u64};
use crate::params::{
Algorithm, BLOCK_SIZE, MAX_PWD_LENGTH, PREHASH_DIGEST_LENGTH, PREHASH_SEED_LENGTH, Params,
SYNC_POINTS, Version,
};
#[cfg(test)]
use crate::params::{Memory, TagLen};
pub type PassTrace<'a> = &'a mut dyn FnMut(u32, &[Block]);
#[cfg(all(test, feature = "std"))]
std::thread_local! {
static H0_COPY_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[must_use]
pub fn index_alpha(
instance: &Instance,
position: &Position,
pseudo_rand: u32,
same_lane: bool,
) -> u32 {
if instance.lane_length == 0 {
return 0;
}
let reference_area_size: u32 = if position.pass == 0 {
if position.slice == 0 {
position.index.wrapping_sub(1)
} else if same_lane {
position
.slice
.wrapping_mul(instance.segment_length)
.wrapping_add(position.index)
.wrapping_sub(1)
} else {
position
.slice
.wrapping_mul(instance.segment_length)
.wrapping_sub(u32::from(position.index == 0))
}
} else if same_lane {
instance
.lane_length
.wrapping_sub(instance.segment_length)
.wrapping_add(position.index)
.wrapping_sub(1)
} else {
instance
.lane_length
.wrapping_sub(instance.segment_length)
.wrapping_sub(u32::from(position.index == 0))
};
let mut relative_position = u64::from(pseudo_rand);
relative_position = (relative_position * relative_position) >> 32;
relative_position = u64::from(reference_area_size.wrapping_sub(1))
.wrapping_sub((u64::from(reference_area_size) * relative_position) >> 32);
let mut start_position: u32 = 0;
if position.pass != 0 {
start_position = if position.slice == SYNC_POINTS - 1 {
0
} else {
position
.slice
.wrapping_add(1)
.wrapping_mul(instance.segment_length)
};
}
((u64::from(start_position).wrapping_add(relative_position)) % u64::from(instance.lane_length))
as u32
}
pub fn initial_hash(
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<[u8; PREHASH_SEED_LENGTH], Error> {
let mut blockhash = [0u8; PREHASH_SEED_LENGTH];
initial_hash_into(
algorithm,
version,
params,
pwd,
salt,
secret,
ad,
&mut blockhash,
)?;
Ok(blockhash)
}
#[allow(clippy::too_many_arguments)]
fn initial_hash_into(
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
blockhash: &mut [u8; PREHASH_SEED_LENGTH],
) -> Result<(), Error> {
#[inline]
fn le32(len: usize) -> [u8; 4] {
(len as u32).to_le_bytes()
}
let mut state = Blake2b::new(PREHASH_DIGEST_LENGTH)?;
state.update(¶ms.lanes().to_le_bytes());
state.update(&le32(params.tag_len_bytes()));
state.update(¶ms.memory_kib().to_le_bytes());
state.update(¶ms.passes().to_le_bytes());
state.update(&version.as_u32().to_le_bytes());
state.update(&algorithm.as_u32().to_le_bytes());
state.update(&le32(pwd.len()));
state.update(pwd);
state.update(&le32(salt.len()));
state.update(salt);
state.update(&le32(secret.len()));
state.update(secret);
state.update(&le32(ad.len()));
state.update(ad);
state.finalize(&mut blockhash[..PREHASH_DIGEST_LENGTH])?;
Ok(())
}
pub fn fill_first_blocks(
blockhash: &mut [u8; PREHASH_SEED_LENGTH],
arena: &mut [Block],
lanes: u32,
lane_length: u32,
) -> Result<(), Error> {
let mut blockhash_bytes = [0u8; BLOCK_SIZE];
let result = (|| {
for lane in 0..lanes {
blockhash[PREHASH_DIGEST_LENGTH..PREHASH_DIGEST_LENGTH + 4]
.copy_from_slice(&0u32.to_le_bytes());
blockhash[PREHASH_DIGEST_LENGTH + 4..PREHASH_SEED_LENGTH]
.copy_from_slice(&lane.to_le_bytes());
blake2b_long(&mut blockhash_bytes, blockhash)?;
let base = (lane as usize)
.checked_mul(lane_length as usize)
.ok_or(Error::IncorrectParameter)?;
match arena.get_mut(base) {
Some(block) => block.load_le(&blockhash_bytes),
None => return Err(Error::IncorrectParameter),
}
blockhash[PREHASH_DIGEST_LENGTH..PREHASH_DIGEST_LENGTH + 4]
.copy_from_slice(&1u32.to_le_bytes());
blake2b_long(&mut blockhash_bytes, blockhash)?;
let second = base.checked_add(1).ok_or(Error::IncorrectParameter)?;
match arena.get_mut(second) {
Some(block) => block.load_le(&blockhash_bytes),
None => return Err(Error::IncorrectParameter),
}
}
Ok(())
})();
clear_internal_memory(&mut blockhash_bytes);
result
}
pub fn fill_memory_blocks(instance: &Instance) -> Result<(), Error> {
unsafe { fill_memory_blocks_traced(instance, crate::fill_block::backend(), None) }
}
pub unsafe fn fill_memory_blocks_traced(
instance: &Instance,
backend: Backend,
mut trace: Option<PassTrace<'_>>,
) -> Result<(), Error> {
if instance.lanes == 0 {
return Err(Error::IncorrectParameter);
}
let fill = crate::fill_block::fill_segment_fn(backend);
#[cfg(feature = "parallel")]
if instance.threads > 1 && instance.lanes > 1 {
unsafe { fill_pooled(instance, fill, trace) };
return Ok(());
}
for pass in 0..instance.passes {
for slice in 0..SYNC_POINTS {
unsafe { fill_slice_st(instance, fill, pass, slice) };
}
if let Some(callback) = trace.as_mut() {
let blocks = unsafe {
core::slice::from_raw_parts(
instance.memory_ptr().cast_const(),
instance.memory_len(),
)
};
callback(pass, blocks);
}
}
Ok(())
}
unsafe fn fill_slice_st(instance: &Instance, fill: FillSegmentFn, pass: u32, slice: u32) {
for lane in 0..instance.lanes {
let position = Position::new(pass, lane, slice, 0);
unsafe { fill(instance, position) };
}
}
#[cfg(feature = "parallel")]
#[derive(Clone, Copy)]
struct SharedInstance<'a>(&'a Instance);
#[cfg(feature = "parallel")]
unsafe impl Send for SharedInstance<'_> {}
#[cfg(feature = "parallel")]
struct FillSync {
next_lane: core::sync::atomic::AtomicU32,
arrived: core::sync::atomic::AtomicU32,
generation: core::sync::atomic::AtomicU32,
lost: core::sync::atomic::AtomicU32,
stop: core::sync::atomic::AtomicBool,
helpers: u32,
}
#[cfg(feature = "parallel")]
const SPIN_LIMIT: u32 = 1024;
#[cfg(feature = "parallel")]
impl FillSync {
#[inline]
fn park_until(mut cond: impl FnMut() -> bool) {
let mut spins = 0u32;
while !cond() {
if spins < SPIN_LIMIT {
spins += 1;
core::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
}
}
#[cfg(feature = "parallel")]
unsafe fn drain_lanes(
shared: SharedInstance<'_>,
sync: &FillSync,
fill: FillSegmentFn,
pass: u32,
slice: u32,
lanes: u32,
) {
use core::sync::atomic::Ordering;
loop {
let lane = sync.next_lane.fetch_add(1, Ordering::Relaxed);
if lane >= lanes {
return;
}
unsafe { fill(shared.0, Position::new(pass, lane, slice, 0)) };
}
}
#[cfg(feature = "parallel")]
unsafe fn fill_pooled(instance: &Instance, fill: FillSegmentFn, mut trace: Option<PassTrace<'_>>) {
use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
let lanes = instance.lanes;
let passes = instance.passes;
let workers = instance.threads.min(lanes);
let shared = SharedInstance(instance);
let sync = FillSync {
next_lane: AtomicU32::new(0),
arrived: AtomicU32::new(0),
generation: AtomicU32::new(0),
lost: AtomicU32::new(0),
stop: AtomicBool::new(false),
helpers: workers.saturating_sub(1),
};
let sync = &sync;
std::thread::scope(|scope| {
struct ReleaseHelpers<'a>(&'a FillSync);
impl Drop for ReleaseHelpers<'_> {
fn drop(&mut self) {
self.0.stop.store(true, Ordering::Relaxed);
self.0.generation.fetch_add(1, Ordering::Release);
}
}
let _release = ReleaseHelpers(sync);
let mut spawned = 0u32;
for _ in 1..workers {
let handle = std::thread::Builder::new().spawn_scoped(scope, move || {
struct Bail<'a>(&'a AtomicU32, bool);
impl Drop for Bail<'_> {
fn drop(&mut self) {
if self.1 {
self.0.fetch_add(1, Ordering::Release);
}
}
}
let mut bail = Bail(&sync.lost, true);
let mut generation = 0u32;
'outer: for pass in 0..passes {
for slice in 0..SYNC_POINTS {
unsafe { drain_lanes(shared, sync, fill, pass, slice, lanes) };
sync.arrived.fetch_add(1, Ordering::Release);
generation += 1;
FillSync::park_until(|| {
sync.generation.load(Ordering::Acquire) == generation
|| sync.stop.load(Ordering::Relaxed)
});
if sync.stop.load(Ordering::Relaxed) {
break 'outer;
}
}
}
bail.1 = false;
});
if handle.is_ok() {
spawned += 1;
}
}
sync.lost
.fetch_add(sync.helpers - spawned, Ordering::Relaxed);
let mut generation = 0u32;
'outer: for pass in 0..passes {
for slice in 0..SYNC_POINTS {
unsafe { drain_lanes(shared, sync, fill, pass, slice, lanes) };
FillSync::park_until(|| {
sync.arrived.load(Ordering::Acquire) + sync.lost.load(Ordering::Acquire)
>= sync.helpers
});
if sync.lost.load(Ordering::Relaxed) > sync.helpers - spawned {
sync.stop.store(true, Ordering::Relaxed);
sync.generation.fetch_add(1, Ordering::Release);
break 'outer;
}
if slice == SYNC_POINTS - 1
&& let Some(callback) = trace.as_mut()
{
let blocks = unsafe {
core::slice::from_raw_parts(
instance.memory_ptr().cast_const(),
instance.memory_len(),
)
};
callback(pass, blocks);
}
sync.next_lane.store(0, Ordering::Relaxed);
sync.arrived.store(0, Ordering::Relaxed);
generation += 1;
sync.generation.store(generation, Ordering::Release);
}
}
});
}
pub fn finalize(instance: &Instance, out: &mut [u8]) -> Result<(), Error> {
let lane_length = instance.lane_length as usize;
if lane_length == 0 || instance.lanes == 0 {
return Err(Error::IncorrectParameter);
}
let blocks = unsafe {
core::slice::from_raw_parts(instance.memory_ptr().cast_const(), instance.memory_len())
};
let Some(&last_of_lane_0) = blocks.get(lane_length - 1) else {
return Err(Error::IncorrectParameter);
};
let mut blockhash = last_of_lane_0;
for lane in 1..instance.lanes {
let index = (lane as usize)
.checked_mul(lane_length)
.and_then(|base| base.checked_add(lane_length - 1))
.ok_or(Error::IncorrectParameter)?;
match blocks.get(index) {
Some(block) => blockhash.xor_with(block),
None => return Err(Error::IncorrectParameter),
}
}
let mut blockhash_bytes = blockhash.to_le_bytes();
let result = blake2b_long(out, &blockhash_bytes);
clear_internal_memory_u64(&mut blockhash.0);
clear_internal_memory(&mut blockhash_bytes);
result
}
#[must_use]
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut d = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
d |= x ^ y;
}
let d = core::hint::black_box(d);
let verdict = (1i32 & ((i32::from(d) - 1) >> 8)) - 1;
verdict == 0
}
#[cfg(feature = "std")]
pub const RANDOM_SALT_LEN: usize = 16;
pub const BOUNDED_MAX_SALT_LEN: u32 = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Argon2 {
algorithm: Algorithm,
version: Version,
params: Params,
}
impl Argon2 {
#[inline]
#[must_use]
pub const fn new(algorithm: Algorithm, version: Version, params: Params) -> Argon2 {
Argon2 {
algorithm,
version,
params,
}
}
#[inline]
#[must_use]
pub const fn algorithm(&self) -> Algorithm {
self.algorithm
}
#[inline]
#[must_use]
pub const fn version(&self) -> Version {
self.version
}
#[inline]
#[must_use]
pub const fn params(&self) -> &Params {
&self.params
}
#[inline]
#[must_use]
pub fn hasher(&self) -> Hasher {
Hasher {
argon2: *self,
workspace: Workspace::new(),
}
}
pub fn hash_into(&self, pwd: &[u8], salt: &[u8], out: &mut [u8]) -> Result<(), Error> {
self.hash_into_with_ad(pwd, salt, &[], &[], out)
}
pub fn hash_into_with_ad(
&self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
unsafe {
hash_inner(
crate::fill_block::backend(),
self.algorithm,
self.version,
&self.params,
pwd,
salt,
secret,
ad,
out,
)
}
}
pub fn hash(&self, pwd: &[u8], salt: &[u8]) -> Result<Vec<u8>, Error> {
self.hash_with_ad(pwd, salt, &[], &[])
}
pub fn hash_with_ad(
&self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<Vec<u8>, Error> {
let mut out = try_zeroed_vec(self.params.tag_len_bytes())?;
self.hash_into_with_ad(pwd, salt, secret, ad, &mut out)?;
Ok(out)
}
pub fn hash_encoded(&self, pwd: &[u8], salt: &[u8]) -> Result<String, Error> {
self.hash_encoded_with_ad(pwd, salt, &[], &[])
}
pub fn hash_encoded_with_ad(
&self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<String, Error> {
let mut tag = self.hash_with_ad(pwd, salt, secret, ad)?;
let encoded = crate::encoding::encode_string_alloc(
self.algorithm,
self.version,
&self.params,
salt,
&tag,
);
clear_internal_memory(&mut tag);
encoded
}
pub fn verify(&self, pwd: &[u8], salt: &[u8], expected: &[u8]) -> Result<(), Error> {
self.verify_with_ad(pwd, salt, &[], &[], expected)
}
pub fn verify_with_ad(
&self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
expected: &[u8],
) -> Result<(), Error> {
let mut computed = try_zeroed_vec(self.params.tag_len_bytes())?;
let result = self.hash_into_with_ad(pwd, salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, expected);
clear_internal_memory(&mut computed);
result?;
if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
}
}
pub fn verify_encoded(encoded: &str, pwd: &[u8], algorithm: Algorithm) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = crate::encoding::decode_string(encoded, algorithm)?;
Argon2::new(decoded.algorithm, decoded.version, decoded.params).verify(
pwd,
&decoded.salt,
&decoded.hash,
)
}
pub fn verify_encoded_with_ad(
encoded: &str,
pwd: &[u8],
secret: &[u8],
ad: &[u8],
algorithm: Algorithm,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = crate::encoding::decode_string(encoded, algorithm)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
let mut computed = try_zeroed_vec(argon2.params.tag_len_bytes())?;
let result =
argon2.hash_into_with_ad(pwd, &decoded.salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, &decoded.hash);
clear_internal_memory(&mut computed);
result?;
if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
}
}
#[inline]
pub fn hash_password_into(&self, pwd: &[u8], salt: &[u8], out: &mut [u8]) -> Result<(), Error> {
self.hash_into(pwd, salt, out)
}
#[inline]
pub fn hash_password(&self, pwd: &[u8], salt: &[u8]) -> Result<String, Error> {
self.hash_encoded(pwd, salt)
}
#[cfg(feature = "std")]
pub fn hash_password_with_random_salt(&self, pwd: &[u8]) -> Result<String, Error> {
let mut salt = [0u8; RANDOM_SALT_LEN];
crate::random::os_random(&mut salt)?;
self.hash_encoded(pwd, &salt)
}
#[inline]
pub fn verify_password(encoded: &str, pwd: &[u8], algorithm: Algorithm) -> Result<(), Error> {
Argon2::verify_encoded(encoded, pwd, algorithm)
}
pub fn verify_encoded_bounded(
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
ceiling: &Params,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = decode_bounded(encoded, algorithm, ceiling)?;
Argon2::new(decoded.algorithm, decoded.version, decoded.params).verify(
pwd,
&decoded.salt,
&decoded.hash,
)
}
pub fn verify_encoded_bounded_with_ad(
encoded: &str,
pwd: &[u8],
secret: &[u8],
ad: &[u8],
algorithm: Algorithm,
ceiling: &Params,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = decode_bounded(encoded, algorithm, ceiling)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
let mut computed = try_zeroed_vec(argon2.params.tag_len_bytes())?;
let result = argon2.hash_into_with_ad(pwd, &decoded.salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, &decoded.hash);
clear_internal_memory(&mut computed);
result?;
if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
}
}
}
fn decode_bounded(
encoded: &str,
algorithm: Algorithm,
ceiling: &Params,
) -> Result<crate::encoding::Decoded, Error> {
let max_encoded = crate::encoding::encoded_len(
algorithm,
ceiling.passes(),
ceiling.memory_kib(),
ceiling.lanes(),
BOUNDED_MAX_SALT_LEN,
ceiling.tag_len_bytes() as u32,
);
if encoded.len() > max_encoded {
return Err(Error::DecodingLengthFail);
}
let mut decoded = crate::encoding::decode_string(encoded, algorithm)?;
if decoded.params.tag_len_bytes() > ceiling.tag_len_bytes() {
return Err(Error::OutputTooLong);
}
if decoded.params.memory_kib() > ceiling.memory_kib() {
return Err(Error::MemoryTooMuch);
}
if decoded.params.passes() > ceiling.passes() {
return Err(Error::TimeTooLarge);
}
if decoded.params.lanes() > ceiling.lanes() {
return Err(Error::LanesTooMany);
}
let threads = ceiling.threads().min(decoded.params.lanes());
if threads != decoded.params.threads() {
decoded.params = decoded.params.to_builder().threads(threads).build()?;
}
Ok(decoded)
}
pub struct Hasher {
argon2: Argon2,
workspace: Workspace,
}
impl Hasher {
#[inline]
#[must_use]
pub const fn argon2(&self) -> &Argon2 {
&self.argon2
}
#[inline]
pub fn set_argon2(&mut self, argon2: Argon2) {
self.argon2 = argon2;
}
#[inline]
#[must_use]
pub const fn algorithm(&self) -> Algorithm {
self.argon2.algorithm
}
#[inline]
#[must_use]
pub const fn version(&self) -> Version {
self.argon2.version
}
#[inline]
#[must_use]
pub const fn params(&self) -> &Params {
&self.argon2.params
}
pub fn reserve(&mut self) -> Result<(), Error> {
self.workspace.reserve(self.argon2.params.memory_blocks() as usize)
}
#[inline]
#[must_use]
pub fn reserved_blocks(&self) -> usize {
self.workspace.capacity()
}
pub fn clear(&mut self) {
self.workspace.clear();
}
#[inline]
pub fn hash_into(&mut self, pwd: &[u8], salt: &[u8], out: &mut [u8]) -> Result<(), Error> {
self.hash_into_with_ad(pwd, salt, &[], &[], out)
}
pub fn hash_into_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
let argon2 = self.argon2;
self.hash_into_using(&argon2, pwd, salt, secret, ad, out)
}
pub fn hash(&mut self, pwd: &[u8], salt: &[u8]) -> Result<Vec<u8>, Error> {
self.hash_with_ad(pwd, salt, &[], &[])
}
pub fn hash_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<Vec<u8>, Error> {
let mut out = try_zeroed_vec(self.argon2.params.tag_len_bytes())?;
self.hash_into_with_ad(pwd, salt, secret, ad, &mut out)?;
Ok(out)
}
pub fn hash_encoded(&mut self, pwd: &[u8], salt: &[u8]) -> Result<String, Error> {
self.hash_encoded_with_ad(pwd, salt, &[], &[])
}
pub fn hash_encoded_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<String, Error> {
let argon2 = self.argon2;
let mut tag = self.hash_with_ad(pwd, salt, secret, ad)?;
let encoded = crate::encoding::encode_string_alloc(
argon2.algorithm,
argon2.version,
&argon2.params,
salt,
&tag,
);
clear_internal_memory(&mut tag);
encoded
}
pub fn verify(&mut self, pwd: &[u8], salt: &[u8], expected: &[u8]) -> Result<(), Error> {
self.verify_with_ad(pwd, salt, &[], &[], expected)
}
pub fn verify_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
expected: &[u8],
) -> Result<(), Error> {
let argon2 = self.argon2;
self.verify_using_ad(&argon2, pwd, salt, secret, ad, expected)
}
pub fn verify_encoded(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = crate::encoding::decode_string(encoded, algorithm)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
if decoded.params.memory_blocks() as usize > self.pooled_ceiling() {
return argon2.verify(pwd, &decoded.salt, &decoded.hash);
}
self.verify_using(&argon2, pwd, &decoded.salt, &decoded.hash)
}
pub fn verify_encoded_with_ad(
&mut self,
encoded: &str,
pwd: &[u8],
secret: &[u8],
ad: &[u8],
algorithm: Algorithm,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = crate::encoding::decode_string(encoded, algorithm)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
if decoded.params.memory_blocks() as usize > self.pooled_ceiling() {
let mut computed = try_zeroed_vec(argon2.params.tag_len_bytes())?;
let result =
argon2.hash_into_with_ad(pwd, &decoded.salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, &decoded.hash);
clear_internal_memory(&mut computed);
result?;
return if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
};
}
self.verify_using_ad(&argon2, pwd, &decoded.salt, secret, ad, &decoded.hash)
}
#[inline]
pub fn hash_password_into(
&mut self,
pwd: &[u8],
salt: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
self.hash_into(pwd, salt, out)
}
#[inline]
pub fn hash_password(&mut self, pwd: &[u8], salt: &[u8]) -> Result<String, Error> {
self.hash_encoded(pwd, salt)
}
#[cfg(feature = "std")]
pub fn hash_password_with_random_salt(&mut self, pwd: &[u8]) -> Result<String, Error> {
let mut salt = [0u8; RANDOM_SALT_LEN];
crate::random::os_random(&mut salt)?;
self.hash_encoded(pwd, &salt)
}
#[inline]
pub fn verify_password(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
) -> Result<(), Error> {
self.verify_encoded(encoded, pwd, algorithm)
}
pub fn verify_encoded_bounded(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
ceiling: &Params,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = decode_bounded(encoded, algorithm, ceiling)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
if decoded.params.memory_blocks() as usize > self.pooled_ceiling() {
return argon2.verify(pwd, &decoded.salt, &decoded.hash);
}
self.verify_using(&argon2, pwd, &decoded.salt, &decoded.hash)
}
pub fn verify_encoded_bounded_with_ad(
&mut self,
encoded: &str,
pwd: &[u8],
secret: &[u8],
ad: &[u8],
algorithm: Algorithm,
ceiling: &Params,
) -> Result<(), Error> {
if pwd.len() > MAX_PWD_LENGTH as usize {
return Err(Error::PwdTooLong);
}
let decoded = decode_bounded(encoded, algorithm, ceiling)?;
let argon2 = Argon2::new(decoded.algorithm, decoded.version, decoded.params);
if decoded.params.memory_blocks() as usize > self.pooled_ceiling() {
let mut computed = try_zeroed_vec(argon2.params.tag_len_bytes())?;
let result = argon2.hash_into_with_ad(pwd, &decoded.salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, &decoded.hash);
clear_internal_memory(&mut computed);
result?;
return if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
};
}
self.verify_using_ad(&argon2, pwd, &decoded.salt, secret, ad, &decoded.hash)
}
#[inline]
fn pooled_ceiling(&self) -> usize {
core::cmp::max(
self.workspace.capacity(),
self.argon2.params.memory_blocks() as usize,
)
}
fn hash_into_using(
&mut self,
argon2: &Argon2,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
unsafe {
hash_in_workspace(
&mut self.workspace,
crate::fill_block::backend(),
argon2.algorithm,
argon2.version,
&argon2.params,
pwd,
salt,
secret,
ad,
out,
None,
None,
)
}
}
fn verify_using(
&mut self,
argon2: &Argon2,
pwd: &[u8],
salt: &[u8],
expected: &[u8],
) -> Result<(), Error> {
self.verify_using_ad(argon2, pwd, salt, &[], &[], expected)
}
fn verify_using_ad(
&mut self,
argon2: &Argon2,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
expected: &[u8],
) -> Result<(), Error> {
let mut computed = try_zeroed_vec(argon2.params.tag_len_bytes())?;
let result = self.hash_into_using(argon2, pwd, salt, secret, ad, &mut computed);
let matched = result.is_ok() && constant_time_eq(&computed, expected);
clear_internal_memory(&mut computed);
result?;
if matched {
Ok(())
} else {
Err(Error::VerifyMismatch)
}
}
}
impl core::fmt::Debug for Hasher {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Hasher")
.field("argon2", &self.argon2)
.field("reserved_blocks", &self.reserved_blocks())
.finish()
}
}
fn try_zeroed_vec(len: usize) -> Result<Vec<u8>, Error> {
let mut v = Vec::new();
v.try_reserve(len)
.map_err(|_| Error::MemoryAllocationError)?;
v.resize(len, 0);
Ok(v)
}
#[allow(clippy::too_many_arguments)]
unsafe fn hash_inner(
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
unsafe {
hash_owned(
backend, algorithm, version, params, pwd, salt, secret, ad, out, None, None,
)
}
}
#[allow(clippy::too_many_arguments)]
unsafe fn hash_owned(
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
trace: Option<PassTrace<'_>>,
h0_out: Option<&mut [u8; PREHASH_DIGEST_LENGTH]>,
) -> Result<(), Error> {
let memory_blocks = validate_and_size(params, pwd, salt, secret, ad, out)?;
let mut arena = Arena::new(memory_blocks)?;
unsafe {
hash_in_arena(
&mut arena, backend, algorithm, version, params, pwd, salt, secret, ad, out, trace,
h0_out,
)
}
}
#[allow(clippy::too_many_arguments)]
pub unsafe fn hash_traced(
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
trace: Option<PassTrace<'_>>,
) -> Result<[u8; PREHASH_DIGEST_LENGTH], Error> {
let mut h0 = [0u8; PREHASH_DIGEST_LENGTH];
let result = unsafe {
hash_owned(
backend,
algorithm,
version,
params,
pwd,
salt,
secret,
ad,
out,
trace,
Some(&mut h0),
)
};
if let Err(error) = result {
clear_internal_memory(&mut h0);
return Err(error);
}
Ok(h0)
}
fn validate_and_size(
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &[u8],
) -> Result<usize, Error> {
params.validate_for(pwd.len(), salt.len(), secret.len(), ad.len())?;
if out.len() != params.tag_len_bytes() {
return Err(Error::OutPtrMismatch);
}
Ok(params.memory_layout().0 as usize)
}
#[allow(clippy::too_many_arguments)]
unsafe fn hash_in_arena(
arena: &mut Arena,
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
trace: Option<PassTrace<'_>>,
h0_out: Option<&mut [u8; PREHASH_DIGEST_LENGTH]>,
) -> Result<(), Error> {
let (memory_blocks, _segment_length, lane_length) = params.memory_layout();
if arena.len() != memory_blocks as usize {
return Err(Error::MemoryAllocationError);
}
arena.set_workers(params.threads());
let mut blockhash = [0u8; PREHASH_SEED_LENGTH];
if let Err(error) = initial_hash_into(
algorithm,
version,
params,
pwd,
salt,
secret,
ad,
&mut blockhash,
) {
clear_internal_memory(&mut blockhash);
return Err(error);
}
if let Some(h0) = h0_out {
#[cfg(all(test, feature = "std"))]
H0_COPY_COUNT.with(|count| count.set(count.get() + 1));
h0.copy_from_slice(&blockhash[..PREHASH_DIGEST_LENGTH]);
}
let fill_first = fill_first_blocks(
&mut blockhash,
arena.as_mut_slice(),
params.lanes(),
lane_length,
);
clear_internal_memory(&mut blockhash);
fill_first?;
let instance =
unsafe { Instance::new(arena.as_mut_ptr(), arena.len(), algorithm, version, params) };
unsafe { fill_memory_blocks_traced(&instance, backend, trace) }?;
finalize(&instance, out)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
unsafe fn hash_in_workspace(
workspace: &mut Workspace,
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
trace: Option<PassTrace<'_>>,
h0_out: Option<&mut [u8; PREHASH_DIGEST_LENGTH]>,
) -> Result<(), Error> {
let memory_blocks = validate_and_size(params, pwd, salt, secret, ad, out)?;
let mut arena = workspace.acquire(memory_blocks)?;
unsafe {
hash_in_arena(
&mut arena, backend, algorithm, version, params, pwd, salt, secret, ad, out, trace,
h0_out,
)
}
}
#[cfg(feature = "internal-api")]
#[allow(clippy::too_many_arguments)]
pub unsafe fn hash_with_backend(
backend: Backend,
algorithm: Algorithm,
version: Version,
params: &Params,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error> {
unsafe {
hash_inner(
backend, algorithm, version, params, pwd, salt, secret, ad, out,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_bounded_clamps_workers_to_the_ceilings_thread_budget() {
const LANES: u32 = 256;
let params = Params::builder()
.memory(Memory::kib(u64::from(8 * LANES)))
.passes(1)
.lanes(LANES)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let encoded = argon2.hash_encoded(b"pw", b"somesalt").expect("encode");
let ceiling = Params::builder()
.memory(Memory::kib(u64::from(8 * LANES)))
.passes(1)
.lanes(LANES)
.threads(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("ceiling");
let decoded =
decode_bounded(&encoded, Algorithm::Argon2id, &ceiling).expect("within the ceiling");
assert_eq!(decoded.params.lanes(), LANES, "lanes must survive: it picks the tag");
assert_eq!(decoded.params.threads(), 1, "workers must obey the ceiling");
assert_eq!(decoded.params.effective_threads(), 1);
}
#[test]
fn decode_bounded_leaves_workers_alone_when_the_ceiling_is_generous() {
let params = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(1)
.lanes(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let encoded = argon2.hash_encoded(b"pw", b"somesalt").expect("encode");
let ceiling = Params::builder()
.memory(Memory::kib(1 << 16))
.passes(8)
.lanes(8)
.tag_len(TagLen::bytes(32))
.build()
.expect("ceiling");
let decoded =
decode_bounded(&encoded, Algorithm::Argon2id, &ceiling).expect("within the ceiling");
assert_eq!(decoded.params.lanes(), 4);
assert_eq!(decoded.params.threads(), 4, "clamped to lanes, not raised to 8");
}
fn instance_for(params: &Params, algorithm: Algorithm, arena: &mut [Block]) -> Instance {
unsafe {
Instance::new(
arena.as_mut_ptr(),
arena.len(),
algorithm,
Version::V0x13,
params,
)
}
}
#[test]
fn index_alpha_pass0_slice0_is_all_but_the_previous() {
let params = Params::builder()
.memory(Memory::kib(1 << 12))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let inst = instance_for(¶ms, Algorithm::Argon2i, &mut arena);
for index in 2..64u32 {
for pseudo in [0u32, 1, 0x7FFF_FFFF, 0x8000_0000, u32::MAX] {
let pos = Position::new(0, 0, 0, index);
let alpha = index_alpha(&inst, &pos, pseudo, true);
assert!(alpha < index, "index={index} pseudo={pseudo} -> {alpha}");
}
}
}
#[test]
fn index_alpha_never_selects_the_current_or_a_concurrent_block() {
let params = Params::builder()
.memory(Memory::kib(1024))
.passes(3)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let inst = instance_for(¶ms, Algorithm::Argon2d, &mut arena);
let seg = inst.segment_length;
for pass in 0..3u32 {
for slice in 0..SYNC_POINTS {
for index in 0..seg {
if pass == 0 && slice == 0 && index < 2 {
continue;
}
let pos = Position::new(pass, 1, slice, index);
for pseudo in [0u32, 1, 12345, 0x8000_0000, u32::MAX] {
if !(pass == 0 && slice == 0) {
let alpha = index_alpha(&inst, &pos, pseudo, false);
let alpha_slice = alpha / seg;
assert_ne!(
alpha_slice, slice,
"cross-lane reference into the live slice: \
pass={pass} slice={slice} index={index} pseudo={pseudo}"
);
}
let alpha = index_alpha(&inst, &pos, pseudo, true);
if alpha / seg == slice {
assert!(
alpha % seg < index,
"same-lane reference at or past the current block: \
pass={pass} slice={slice} index={index} -> {alpha}"
);
}
}
}
}
}
}
#[test]
fn index_alpha_wraps_at_index_zero_across_lanes() {
let params = Params::builder()
.memory(Memory::kib(8))
.passes(1)
.lanes(1)
.threads(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let inst = instance_for(¶ms, Algorithm::Argon2i, &mut arena);
assert_eq!(inst.segment_length, 2);
let pos = Position::new(0, 0, 1, 0);
for pseudo in [0u32, 1, 0x1234_5678, u32::MAX] {
assert_eq!(index_alpha(&inst, &pos, pseudo, false), 0);
}
}
#[test]
fn index_alpha_start_position_skips_the_current_slice() {
let params = Params::builder()
.memory(Memory::kib(1024))
.passes(2)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let inst = instance_for(¶ms, Algorithm::Argon2d, &mut arena);
let seg = inst.segment_length;
for slice in 0..SYNC_POINTS {
let pos = Position::new(1, 0, slice, 5);
let ras = inst.lane_length - seg + 5 - 1;
let start = if slice == SYNC_POINTS - 1 {
0
} else {
(slice + 1) * seg
};
assert_eq!(
index_alpha(&inst, &pos, 0, true),
(start + ras - 1) % inst.lane_length
);
}
}
#[test]
fn index_alpha_reference_area_size_zero_uses_32_bit_arithmetic() {
const CASES: [(u32, u32); 8] = [
(2, 7),
(3, 3),
(5, 15),
(7, 3),
(11, 3),
(13, 47),
(100, 95),
(341, 3),
];
let params = Params::builder()
.memory(Memory::kib(1 << 12))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let mut inst = instance_for(¶ms, Algorithm::Argon2i, &mut arena);
for (segment_length, expected) in CASES {
inst.segment_length = segment_length;
inst.lane_length = segment_length * SYNC_POINTS;
let pos = Position::new(0, 0, 0, 1);
for pseudo in [0u32, 1, 0x7FFF_FFFF, 0x8000_0000, u32::MAX, 0xDEAD_BEEF] {
assert_eq!(
index_alpha(&inst, &pos, pseudo, true),
expected,
"segment_length={segment_length} pseudo={pseudo:#010x}"
);
assert_eq!(index_alpha(&inst, &pos, pseudo, false), expected);
}
}
}
#[test]
fn index_alpha_degenerate_instance_does_not_panic() {
let params = Params::builder()
.memory(Memory::kib(8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = [Block::ZERO; 2];
let mut inst = instance_for(¶ms, Algorithm::Argon2i, &mut arena);
inst.lane_length = 0;
inst.segment_length = 0;
assert_eq!(index_alpha(&inst, &Position::new(0, 0, 0, 0), 7, true), 0);
}
#[test]
fn constant_time_eq_matches_argon2_compare() {
assert!(constant_time_eq(b"", b""));
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"abcd"));
assert!(!constant_time_eq(b"", b"a"));
assert!(!constant_time_eq(&[0u8; 32], &{
let mut b = [0u8; 32];
b[31] = 1;
b
}));
assert!(!constant_time_eq(&[0u8; 4], &[0, 0, 0, 0x80]));
}
#[cfg(feature = "std")]
#[test]
fn stable_hashes_do_not_request_an_h0_output_copy() {
H0_COPY_COUNT.with(|count| count.set(0));
let params = Params::builder()
.memory(Memory::kib(32))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut tag = [0u8; 32];
argon2
.hash_into(b"password", b"somesalt", &mut tag)
.expect("one-shot hash");
let mut hasher = argon2.hasher();
hasher
.hash_into(b"password", b"somesalt", &mut tag)
.expect("pooled hash");
H0_COPY_COUNT.with(|count| assert_eq!(count.get(), 0, "stable paths copied H0"));
let mut h0 = unsafe {
hash_traced(
Backend::Scalar,
argon2.algorithm,
argon2.version,
&argon2.params,
b"password",
b"somesalt",
&[],
&[],
&mut tag,
None,
)
}
.expect("traced hash");
H0_COPY_COUNT.with(|count| assert_eq!(count.get(), 1, "trace did not copy H0"));
clear_internal_memory(&mut h0);
}
#[test]
fn fill_first_blocks_rejects_a_short_internal_arena() {
let mut blockhash = [0xA5; PREHASH_SEED_LENGTH];
let mut arena = [];
assert_eq!(
fill_first_blocks(&mut blockhash, &mut arena, 1, 8),
Err(Error::IncorrectParameter)
);
}
#[test]
fn initial_hash_matches_the_genkat_pre_hashing_digest() {
let params = Params::builder()
.memory(Memory::kib(32))
.passes(3)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let h = initial_hash(
Algorithm::Argon2id,
Version::V0x13,
¶ms,
&[1u8; 32],
&[2u8; 16],
&[3u8; 8],
&[4u8; 12],
)
.expect("initial_hash");
let expected = "2889de487eb42ae500c0007ed9252f1069eadec40d5765b485de6dc2437a67b8\
546a2f0acc1a0882db8fcf74714b472e94df421a5da1112ffa11434370a1e997";
let mut hex = String::new();
for byte in &h[..PREHASH_DIGEST_LENGTH] {
hex.push_str(&alloc::format!("{byte:02x}"));
}
assert_eq!(hex, expected);
assert_eq!(&h[PREHASH_DIGEST_LENGTH..], &[0u8; 8]);
}
#[test]
fn initial_hash_field_order_is_load_bearing() {
let a = Params::builder()
.memory(Memory::kib(64))
.passes(1)
.lanes(2)
.threads(2)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let b = Params::builder()
.memory(Memory::kib(64))
.passes(1)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let ha = initial_hash(
Algorithm::Argon2i,
Version::V0x13,
&a,
b"p",
b"salt",
&[],
&[],
)
.expect("h");
let hb = initial_hash(
Algorithm::Argon2i,
Version::V0x13,
&b,
b"p",
b"salt",
&[],
&[],
)
.expect("h");
assert_ne!(ha, hb);
let h10 = initial_hash(
Algorithm::Argon2i,
Version::V0x10,
&a,
b"p",
b"salt",
&[],
&[],
)
.expect("h");
assert_ne!(ha, h10);
let hid = initial_hash(
Algorithm::Argon2id,
Version::V0x13,
&a,
b"p",
b"salt",
&[],
&[],
)
.expect("h");
assert_ne!(ha, hid);
let h1 = initial_hash(
Algorithm::Argon2i,
Version::V0x13,
&a,
b"ab",
b"saltsalt",
&[],
&[],
)
.expect("h");
let h2 = initial_hash(
Algorithm::Argon2i,
Version::V0x13,
&a,
b"a",
b"bsaltsalt",
&[],
&[],
)
.expect("h");
assert_ne!(h1, h2);
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::new();
for byte in bytes {
s.push_str(&alloc::format!("{byte:02x}"));
}
s
}
#[test]
fn one_official_vector_end_to_end() {
let params = Params::builder()
.memory(Memory::kib(1 << 16))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2i, Version::V0x13, params);
let tag = argon2.hash(b"password", b"somesalt").expect("hash");
assert_eq!(
hex(&tag),
"c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0"
);
}
#[test]
fn genkat_tag_matches_for_all_three_types() {
let params = Params::builder()
.memory(Memory::kib(32))
.passes(3)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
for (algorithm, version, expected) in [
(
Algorithm::Argon2d,
Version::V0x13,
"512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb",
),
(
Algorithm::Argon2i,
Version::V0x13,
"c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8",
),
(
Algorithm::Argon2id,
Version::V0x13,
"0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659",
),
(
Algorithm::Argon2d,
Version::V0x10,
"96a9d4e5a1734092c85e29f410a45914a5dd1f5cbf08b2670da68a0285abf32b",
),
(
Algorithm::Argon2i,
Version::V0x10,
"87aeedd6517ab830cd9765cd8231abb2e647a5dee08f7c05e02fcb763335d0fd",
),
(
Algorithm::Argon2id,
Version::V0x10,
"b64615f07789b66b645b67ee9ed3b377ae350b6bfcbb0fc95141ea8f322613c0",
),
] {
let argon2 = Argon2::new(algorithm, version, params);
let mut tag = [0u8; 32];
argon2
.hash_into_with_ad(&[1u8; 32], &[2u8; 16], &[3u8; 8], &[4u8; 12], &mut tag)
.expect("hash");
assert_eq!(hex(&tag), expected, "{algorithm:?} {version:?}");
}
}
#[test]
fn tiny_single_threaded_hash_matches_the_c_reference() {
let params = Params::builder()
.memory(Memory::kib(8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
assert_eq!(params.memory_layout(), (8, 2, 8));
for (algorithm, expected) in [
(
Algorithm::Argon2i,
"cbf2bce47e6d23999626143fabc5db69164743ee000ddd3f8895a6f82cfb9a6e",
),
(
Algorithm::Argon2d,
"c519e603ac603ec1aeb5b71ec44a6179e3f3975b14c0c97e3914c79e6363e178",
),
(
Algorithm::Argon2id,
"f137f8e186a403a679ccd0606e5ab5dcdafe43c1640855ac8c6e33e9bd63eeb3",
),
] {
let mut tag = [0u8; 32];
Argon2::new(algorithm, Version::V0x13, params)
.hash_into(b"password", b"somesalt", &mut tag)
.expect("hash");
assert_eq!(hex(&tag), expected, "{algorithm:?}");
}
}
#[test]
fn tiny_two_lane_hash_matches_the_c_reference() {
let params = Params::builder()
.memory(Memory::kib(16))
.passes(2)
.lanes(2)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
assert_eq!(params.memory_layout(), (16, 2, 8));
for (algorithm, expected) in [
(
Algorithm::Argon2i,
"7fbb85db7e9636115f2fd0f29ea4214baaada18b39fffed7875eeb9fa9b308c5",
),
(
Algorithm::Argon2d,
"59f20a66a4c31bf0438a2f494867c32120409a91380f0687aefee984ba86bda8",
),
(
Algorithm::Argon2id,
"747d7631b182faf749d7efc31aec31df4ecfe3b57c792f53800ac2c9978b4888",
),
] {
let mut tag = [0u8; 32];
Argon2::new(algorithm, Version::V0x13, params)
.hash_into(b"password", b"somesalt", &mut tag)
.expect("hash");
assert_eq!(hex(&tag), expected, "{algorithm:?} (threads = lanes = 2)");
}
}
#[test]
fn out_length_mismatch_is_out_ptr_mismatch() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut out = [0u8; 16];
assert_eq!(
argon2.hash_into(b"password", b"somesalt", &mut out),
Err(Error::OutPtrMismatch)
);
}
#[test]
fn trace_fires_once_per_pass_with_the_whole_arena() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(3)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut passes = alloc::vec::Vec::new();
let mut out = [0u8; 32];
let mut trace = |pass: u32, blocks: &[Block]| {
passes.push((pass, blocks.len()));
};
let h0 = unsafe {
hash_traced(
crate::fill_block::backend(),
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesalt",
&[],
&[],
&mut out,
Some(&mut trace),
)
}
.expect("hash_traced");
assert_eq!(passes, alloc::vec![(0, 256), (1, 256), (2, 256)]);
assert_eq!(h0.len(), PREHASH_DIGEST_LENGTH);
}
#[test]
#[cfg(feature = "parallel")]
#[cfg_attr(target_arch = "wasm32", ignore = "no unwinding on wasi (panic=abort)")]
fn a_panicking_trace_callback_unwinds_instead_of_deadlocking_the_pool() {
let params = Params::builder()
.memory(Memory::kib(64))
.passes(2)
.lanes(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = Arena::new(params.memory_blocks() as usize).expect("arena");
let mut blockhash = initial_hash(
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesaltsomesalt",
&[],
&[],
)
.expect("H0");
let (_, _, lane_length) = params.memory_layout();
fill_first_blocks(
&mut blockhash,
arena.as_mut_slice(),
params.lanes(),
lane_length,
)
.expect("first blocks");
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let instance = unsafe {
Instance::new(
arena.as_mut_ptr(),
arena.len(),
Algorithm::Argon2id,
Version::V0x13,
¶ms,
)
};
let mut boom = |_pass: u32, _blocks: &[Block]| panic!("trace exploded");
unsafe {
fill_memory_blocks_traced(&instance, Backend::Scalar, Some(&mut boom)).expect("fill")
};
}));
assert!(caught.is_err(), "the callback's panic must reach the caller");
}
#[test]
fn threads_do_not_change_the_tag() {
for lanes in [2u32, 4] {
let single = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(2)
.lanes(lanes)
.threads(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let multi = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(2)
.lanes(lanes)
.threads(lanes)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let a = Argon2::new(Algorithm::Argon2id, Version::V0x13, single)
.hash(b"password", b"somesalt")
.expect("st");
let b = Argon2::new(Algorithm::Argon2id, Version::V0x13, multi)
.hash(b"password", b"somesalt")
.expect("mt");
assert_eq!(a, b, "lanes={lanes}");
}
}
#[test]
fn verify_round_trips_and_rejects() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let encoded = argon2.hash_encoded(b"password", b"somesalt").expect("enc");
assert!(encoded.starts_with("$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$"));
assert_eq!(
Argon2::verify_encoded(&encoded, b"password", Algorithm::Argon2id),
Ok(())
);
assert_eq!(
Argon2::verify_encoded(&encoded, b"passwore", Algorithm::Argon2id),
Err(Error::VerifyMismatch)
);
assert_eq!(
Argon2::verify_encoded(&encoded, b"password", Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let tag = argon2.hash(b"password", b"somesalt").expect("hash");
assert_eq!(argon2.verify(b"password", b"somesalt", &tag), Ok(()));
assert_eq!(
argon2.verify(b"password", b"somesalt", &tag[..16]),
Err(Error::VerifyMismatch)
);
}
#[test]
fn password_flavoured_names_are_the_same_functions() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut a = [0u8; 32];
let mut b = [0u8; 32];
argon2
.hash_into(b"password", b"somesalt", &mut a)
.expect("hash_into");
argon2
.hash_password_into(b"password", b"somesalt", &mut b)
.expect("hash_password_into");
assert_eq!(a, b);
let encoded = argon2.hash_password(b"password", b"somesalt").expect("enc");
assert_eq!(
encoded,
argon2.hash_encoded(b"password", b"somesalt").expect("enc")
);
assert!(encoded.starts_with("$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$"));
assert_eq!(
Argon2::verify_password(&encoded, b"password", Algorithm::Argon2id),
Ok(())
);
assert_eq!(
Argon2::verify_password(&encoded, b"passwore", Algorithm::Argon2id),
Err(Error::VerifyMismatch)
);
}
type Dump = (
alloc::vec::Vec<(u32, alloc::vec::Vec<Block>)>,
[u8; PREHASH_DIGEST_LENGTH],
[u8; 32],
);
unsafe fn dump_one_shot(backend: Backend, argon2: &Argon2, pwd: &[u8], salt: &[u8]) -> Dump {
let mut tag = [0u8; 32];
let mut passes: alloc::vec::Vec<(u32, alloc::vec::Vec<Block>)> = alloc::vec::Vec::new();
let mut trace = |pass: u32, blocks: &[Block]| passes.push((pass, blocks.to_vec()));
let h0 = unsafe {
hash_traced(
backend,
argon2.algorithm,
argon2.version,
&argon2.params,
pwd,
salt,
&[3u8; 8],
&[4u8; 12],
&mut tag,
Some(&mut trace),
)
}
.expect("one-shot hash");
(passes, h0, tag)
}
unsafe fn dump_pooled(
workspace: &mut Workspace,
backend: Backend,
argon2: &Argon2,
pwd: &[u8],
salt: &[u8],
) -> Dump {
let mut tag = [0u8; 32];
let mut h0 = [0u8; PREHASH_DIGEST_LENGTH];
let mut passes: alloc::vec::Vec<(u32, alloc::vec::Vec<Block>)> = alloc::vec::Vec::new();
let mut trace = |pass: u32, blocks: &[Block]| passes.push((pass, blocks.to_vec()));
unsafe {
hash_in_workspace(
workspace,
backend,
argon2.algorithm,
argon2.version,
&argon2.params,
pwd,
salt,
&[3u8; 8],
&[4u8; 12],
&mut tag,
Some(&mut trace),
Some(&mut h0),
)
}
.expect("pooled hash");
(passes, h0, tag)
}
fn assert_same_dump(what: &str, expected: &Dump, actual: &Dump) {
assert_eq!(actual.1, expected.1, "{what}: H0 differs");
assert_eq!(actual.0.len(), expected.0.len(), "{what}: pass count");
for (want, got) in expected.0.iter().zip(actual.0.iter()) {
assert_eq!(got.0, want.0, "{what}: pass index");
assert_eq!(
got.1.len(),
want.1.len(),
"{what}: arena length after pass {}",
want.0
);
for (block, (wb, gb)) in want.1.iter().zip(got.1.iter()).enumerate() {
for (word, (w, g)) in wb.0.iter().zip(gb.0.iter()).enumerate() {
assert_eq!(
g, w,
"{what}: pass {}, block {block}, word {word}",
want.0
);
}
}
}
assert_eq!(actual.2, expected.2, "{what}: tag differs");
}
#[test]
fn a_pooled_hash_reproduces_the_one_shot_arena_word_for_word() {
let params = Params::builder()
.memory(Memory::kib(32))
.passes(3)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
for algorithm in [Algorithm::Argon2d, Algorithm::Argon2i, Algorithm::Argon2id] {
for version in [Version::V0x10, Version::V0x13] {
let argon2 = Argon2::new(algorithm, version, params);
for &backend in Backend::ALL {
if !backend.is_available() {
continue; }
let expected =
unsafe { dump_one_shot(backend, &argon2, &[1u8; 32], &[2u8; 16]) };
let mut workspace = Workspace::new();
for round in 0..3 {
let actual = unsafe {
dump_pooled(&mut workspace, backend, &argon2, &[1u8; 32], &[2u8; 16])
};
assert_same_dump(
&alloc::format!("{algorithm:?} {version:?} {backend} round {round}"),
&expected,
&actual,
);
}
}
}
}
}
#[test]
fn hasher_agrees_with_the_one_shot_api() {
let configs = [
Params::builder()
.memory(Memory::kib(8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("minimum"),
Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("st"),
Params::builder()
.memory(Memory::kib(1 << 9))
.passes(2)
.lanes(4)
.threads(4)
.tag_len(TagLen::bytes(32))
.build()
.expect("mt"),
Params::builder()
.memory(Memory::kib(64))
.passes(3)
.lanes(2)
.threads(2)
.tag_len(TagLen::bytes(24))
.build()
.expect("odd outlen"),
];
for params in configs {
for algorithm in [Algorithm::Argon2d, Algorithm::Argon2i, Algorithm::Argon2id] {
let argon2 = Argon2::new(algorithm, Version::V0x13, params);
let mut hasher = argon2.hasher();
for round in 0..4u8 {
let pwd = [round; 7];
let mut want = alloc::vec![0u8; params.tag_len_bytes()];
let mut got = alloc::vec![0u8; params.tag_len_bytes()];
argon2.hash_into(&pwd, b"somesalt", &mut want).expect("one");
hasher.hash_into(&pwd, b"somesalt", &mut got).expect("pool");
assert_eq!(got, want, "{algorithm:?} round {round}");
argon2
.hash_into_with_ad(&pwd, b"somesalt", &[3u8; 8], &[4u8; 12], &mut want)
.expect("one ad");
hasher
.hash_into_with_ad(&pwd, b"somesalt", &[3u8; 8], &[4u8; 12], &mut got)
.expect("pool ad");
assert_eq!(got, want, "{algorithm:?} round {round} with ad");
}
}
}
}
#[test]
fn tiny_pooled_hashes_match_the_c_reference() {
let one_lane = Params::builder()
.memory(Memory::kib(8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let two_lane = Params::builder()
.memory(Memory::kib(16))
.passes(2)
.lanes(2)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, one_lane).hasher();
let mut tag = [0u8; 32];
for round in 0..2 {
hasher
.hash_into(b"password", b"somesalt", &mut tag)
.expect("single lane");
assert_eq!(
hex(&tag),
"f137f8e186a403a679ccd0606e5ab5dcdafe43c1640855ac8c6e33e9bd63eeb3",
"single lane, round {round}"
);
}
hasher.set_argon2(Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
two_lane,
));
for round in 0..2 {
hasher
.hash_into(b"password", b"somesalt", &mut tag)
.expect("two lanes");
assert_eq!(
hex(&tag),
"747d7631b182faf749d7efc31aec31df4ecfe3b57c792f53800ac2c9978b4888",
"two lanes, round {round}"
);
}
hasher.set_argon2(Argon2::new(
Algorithm::Argon2id,
Version::V0x13,
one_lane,
));
hasher
.hash_into(b"password", b"somesalt", &mut tag)
.expect("single lane again");
assert_eq!(
hex(&tag),
"f137f8e186a403a679ccd0606e5ab5dcdafe43c1640855ac8c6e33e9bd63eeb3"
);
assert_eq!(hasher.reserved_blocks(), two_lane.memory_blocks() as usize);
}
#[test]
fn reuse_lands_on_one_allocation() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let blocks = params.memory_blocks() as usize;
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher();
assert_eq!(hasher.reserved_blocks(), 0, "nothing allocated up front");
let mut tag = [0u8; 32];
hasher.hash_into(b"password", b"somesalt", &mut tag).expect("first");
assert_eq!(hasher.reserved_blocks(), blocks);
let first = hasher.workspace.acquire(blocks).expect("peek").as_ptr();
for round in 0..8 {
hasher.hash_into(b"password", b"somesalt", &mut tag).expect("again");
assert_eq!(
hasher.workspace.acquire(blocks).expect("peek").as_ptr(),
first,
"round {round} reallocated"
);
}
assert_eq!(hasher.reserved_blocks(), blocks);
}
#[test]
fn a_finished_hash_leaves_the_whole_arena_full_of_derived_material() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut arena = Arena::new(params.memory_blocks() as usize).expect("arena");
let mut out = [0u8; 32];
unsafe {
hash_in_arena(
&mut arena,
Backend::Scalar,
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesalt",
&[],
&[],
&mut out,
None,
None,
)
}
.expect("hash");
let dirty = arena.as_slice().iter().filter(|b| **b != Block::ZERO).count();
assert_eq!(
dirty,
arena.len(),
"every block should still hold derived material before the wipe"
);
}
#[test]
#[cfg(feature = "zeroize-memory")]
fn the_arena_a_hash_borrowed_comes_back_wiped() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let blocks = params.memory_blocks() as usize;
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher();
let mut tag = [0u8; 32];
for round in 0..3 {
hasher.hash_into(b"password", b"somesalt", &mut tag).expect("hash");
let parked = hasher.workspace.acquire(blocks).expect("peek");
assert!(
parked.as_slice().iter().all(|b| *b == Block::ZERO),
"round {round}: the arena still holds derived material"
);
}
}
#[test]
fn an_error_does_not_disturb_reuse() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let blocks = params.memory_blocks() as usize;
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher();
let mut tag = [0u8; 32];
hasher.hash_into(b"password", b"somesalt", &mut tag).expect("warm up");
let before = hasher.workspace.acquire(blocks).expect("peek").as_ptr();
let mut short = [0u8; 16];
assert_eq!(
hasher.hash_into(b"password", b"somesalt", &mut short),
Err(Error::OutPtrMismatch)
);
assert!(hasher.hash_into(b"password", b"salt", &mut tag).is_err());
assert_eq!(hasher.reserved_blocks(), blocks, "capacity survived");
assert_eq!(
hasher.workspace.acquire(blocks).expect("peek").as_ptr(),
before,
"and it is the same allocation"
);
let mut after = [0u8; 32];
hasher.hash_into(b"password", b"somesalt", &mut after).expect("still works");
assert_eq!(after, tag);
}
#[test]
fn changing_the_configuration_keeps_the_memory_and_the_answers() {
let small = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("small");
let large = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("large");
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, small).hasher();
let mut tag = [0u8; 32];
let mut want = [0u8; 32];
for (params, label) in [(small, "small"), (large, "large"), (small, "small again")] {
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
hasher.set_argon2(argon2);
assert_eq!(hasher.params().memory_kib(), params.memory_kib(), "{label}");
assert_eq!(hasher.algorithm(), Algorithm::Argon2id);
assert_eq!(hasher.version(), Version::V0x13);
assert_eq!(hasher.argon2(), &argon2);
hasher.hash_into(b"password", b"somesalt", &mut tag).expect(label);
argon2.hash_into(b"password", b"somesalt", &mut want).expect(label);
assert_eq!(tag, want, "{label}");
}
assert_eq!(
hasher.reserved_blocks(),
large.memory_blocks() as usize,
"a smaller configuration must not shrink the arena"
);
}
#[test]
fn reserve_and_clear_move_the_allocation_around() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut hasher = argon2.hasher();
hasher.reserve().expect("reserve");
assert_eq!(hasher.reserved_blocks(), params.memory_blocks() as usize);
let reserved = hasher
.workspace
.acquire(params.memory_blocks() as usize)
.expect("peek")
.as_ptr();
let mut tag = [0u8; 32];
hasher.hash_into(b"password", b"somesalt", &mut tag).expect("hash");
assert_eq!(
hasher
.workspace
.acquire(params.memory_blocks() as usize)
.expect("peek")
.as_ptr(),
reserved,
"the first hash must use the reserved arena, not a new one"
);
hasher.clear();
assert_eq!(hasher.reserved_blocks(), 0);
let mut again = [0u8; 32];
hasher.hash_into(b"password", b"somesalt", &mut again).expect("after clear");
assert_eq!(again, tag);
assert_eq!(hasher.reserved_blocks(), params.memory_blocks() as usize);
}
#[test]
fn hasher_encodes_and_verifies_like_argon2() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(2)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut hasher = argon2.hasher();
let encoded = hasher.hash_encoded(b"password", b"somesalt").expect("enc");
assert_eq!(
encoded,
argon2.hash_encoded(b"password", b"somesalt").expect("enc")
);
assert_eq!(
encoded,
hasher.hash_password(b"password", b"somesalt").expect("enc")
);
assert_eq!(
hasher.verify_encoded(&encoded, b"password", Algorithm::Argon2id),
Ok(())
);
assert_eq!(
hasher.verify_password(&encoded, b"passwore", Algorithm::Argon2id),
Err(Error::VerifyMismatch)
);
assert_eq!(
hasher.verify_encoded(&encoded, b"password", Algorithm::Argon2i),
Err(Error::DecodingFail)
);
let tag = hasher.hash(b"password", b"somesalt").expect("hash");
assert_eq!(tag, argon2.hash(b"password", b"somesalt").expect("hash"));
assert_eq!(hasher.verify(b"password", b"somesalt", &tag), Ok(()));
assert_eq!(
hasher.verify(b"password", b"somesalt", &tag[..16]),
Err(Error::VerifyMismatch)
);
let mut into = [0u8; 32];
hasher
.hash_password_into(b"password", b"somesalt", &mut into)
.expect("hash_password_into");
assert_eq!(&into[..], &tag[..]);
}
#[test]
fn verifying_a_mix_of_costs_never_lets_a_string_grow_the_arena() {
let small = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("small");
let large = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("large");
let encoded_small = Argon2::new(Algorithm::Argon2id, Version::V0x13, small)
.hash_encoded(b"password", b"somesalt")
.expect("enc small");
let encoded_large = Argon2::new(Algorithm::Argon2id, Version::V0x13, large)
.hash_encoded(b"password", b"somesalt")
.expect("enc large");
let mut hasher = Argon2::new(Algorithm::Argon2i, Version::V0x10, small).hasher();
for round in 0..3 {
assert_eq!(
hasher.verify_encoded(&encoded_large, b"password", Algorithm::Argon2id),
Ok(()),
"round {round} large"
);
assert_eq!(
hasher.verify_encoded(&encoded_small, b"password", Algorithm::Argon2id),
Ok(()),
"round {round} small"
);
assert_eq!(
hasher.reserved_blocks(),
small.memory_blocks() as usize,
"round {round}: the encoded string set the high-water mark"
);
}
hasher.set_argon2(Argon2::new(Algorithm::Argon2id, Version::V0x13, large));
assert_eq!(
hasher.verify_encoded(&encoded_large, b"password", Algorithm::Argon2id),
Ok(())
);
assert_eq!(hasher.reserved_blocks(), large.memory_blocks() as usize);
}
#[test]
fn a_decoded_cost_under_the_configured_one_pools_from_the_very_first_call() {
let tiny = Params::builder()
.memory(Memory::kib(1 << 7))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("tiny");
let configured = Params::builder()
.memory(Memory::kib(1 << 10))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("configured");
let encoded_tiny = Argon2::new(Algorithm::Argon2id, Version::V0x13, tiny)
.hash_encoded(b"password", b"somesalt")
.expect("enc tiny");
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, configured).hasher();
assert_eq!(hasher.reserved_blocks(), 0);
assert_eq!(
hasher.verify_encoded(&encoded_tiny, b"password", Algorithm::Argon2id),
Ok(())
);
assert_eq!(
hasher.reserved_blocks(),
tiny.memory_blocks() as usize,
"a cost under the ceiling should still use the pool"
);
assert!(
hasher.reserved_blocks() <= configured.memory_blocks() as usize,
"an input must never push the pool past the owner's configuration"
);
let mut tag = [0u8; 32];
hasher
.hash_into(b"password", b"somesalt", &mut tag)
.expect("hash");
assert_eq!(
hasher.reserved_blocks(),
configured.memory_blocks() as usize
);
}
#[test]
fn a_hasher_is_send() {
const fn assert_send<T: Send>() {}
assert_send::<Hasher>();
}
#[test]
fn a_wrongly_sized_arena_is_an_error_not_undefined_behaviour() {
let params = Params::builder()
.memory(Memory::kib(1 << 8))
.passes(1)
.lanes(1)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
assert_eq!(params.memory_blocks(), 256);
let mut arena = Arena::new(64).expect("64 blocks");
let mut out = [0u8; 32];
let result = unsafe {
hash_in_arena(
&mut arena,
Backend::Scalar,
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesalt",
&[],
&[],
&mut out,
None,
None,
)
};
assert_eq!(result.err(), Some(Error::MemoryAllocationError));
assert_eq!(out, [0u8; 32], "nothing was written");
}
#[test]
fn every_available_backend_agrees_with_scalar() {
let params = Params::builder()
.memory(Memory::kib(1 << 9))
.passes(2)
.lanes(2)
.threads(2)
.tag_len(TagLen::bytes(32))
.build()
.expect("params");
let mut reference = [0u8; 32];
unsafe {
hash_inner(
Backend::Scalar,
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesalt",
&[],
&[],
&mut reference,
)
}
.expect("scalar");
for &backend in Backend::ALL {
if !backend.is_available() {
continue;
}
let mut out = [0u8; 32];
unsafe {
hash_inner(
backend,
Algorithm::Argon2id,
Version::V0x13,
¶ms,
b"password",
b"somesalt",
&[],
&[],
&mut out,
)
}
.expect("backend");
assert_eq!(out, reference, "{backend}");
}
}
}