pub struct Hasher { /* private fields */ }Expand description
An Argon2 that keeps its block arena between calls.
Build one with Argon2::hasher. Every method mirrors the Argon2
method of the same name and returns the same bytes; the only difference is
that the arena is borrowed from a pool instead of allocated and freed each
time. Nothing else about the computation changes — same backend dispatch,
same threading, same wipe.
use argon2_rust::{Algorithm, Argon2, Params, Version, params::Memory};
let params = Params::builder().memory(Memory::kib(1 << 8)).passes(1).build()?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut hasher = argon2.hasher();
let encoded = hasher.hash_encoded(b"password", b"somesalt")?;
assert!(hasher.verify_encoded(&encoded, b"password", Algorithm::Argon2id).is_ok());§What it is worth, measured
Reuse skips the mmap, the first-touch page faults over the whole arena,
and the munmap. Interleaved A/B against Argon2::hash_into, 15 paired
rounds on Linux/x86-64 (Sapphire Rapids, AVX-512):
m_cost t p | one-shot | pooled | delta
---------|-----|-----------|-----------|--------
8 KiB 1 1 | 20.4 us | 20.3 us | -0.7%
64 KiB 1 1 | 27.9 us | 26.5 us | -5.3%
1 MiB 1 1 | 212 us | 185 us | -11.7%
4 MiB 1 1 | 989 us | 786 us | -19.9%
4 MiB 1 4 | 806 us | 592 us | -26.9%
64 MiB 1 1 | 25.89 ms| 19.43 ms | -24.9%
64 MiB 1 4 | 11.65 ms| 8.40 ms | -34.0%
256 MiB 1 1 | 111.74 ms| 86.17 ms | -23.3%
256 MiB 1 4 | 46.14 ms| 35.09 ms | -24.0%
256 MiB 3 4 | 109.85 ms| 99.26 ms | -9.7%The t = 3 rows are smaller for the obvious reason: the same one-time
acquisition is spread over three passes of filling.
It does not remove allocator calls — there was only ever one per hash,
1.7 us out of 306 ms at m_cost = 1 GiB.
§Wiping
Unchanged from the one-shot API. The arena is wiped when the call that
borrowed it returns — success, ? error or unwind alike — so the window in
which a password’s derived material is resident is exactly as long as it was
before. What reuse changes is that the wipe now doubles as the next call’s
zeroing, instead of being followed by a fresh alloc_zeroed that zeroes
again.
Dropping the Hasher releases the arena to the allocator, wiped.
§Threading
One Hasher per thread. It is Send, so it can move to whichever worker
picks up a request, and deliberately not Sync: two threads hashing
through one Hasher would be two hashes sharing one arena. The multi-lane
fill inside a single hash is unaffected — one std::thread::scope owns
its helper pool for the whole fill, over the arena this Hasher lent it for
the duration of that one call.
fn needs_sync<T: Sync>(_: &T) {}
needs_sync(&hasher);§Two spellings
Every alias mirrors Argon2, trap included: Hasher::hash_password_into
writes a raw tag while Hasher::hash_password returns a PHC
string, because _into names a destination and not a format. See
Argon2’s section of the same name for the table.
use argon2_rust::{Algorithm, Argon2, Params, Version, params::Memory};
let params = Params::builder().memory(Memory::kib(1 << 8)).passes(1).build()?;
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher();
// Same prefix, same arena, different return type and different format.
let mut raw = [0u8; 32];
hasher.hash_password_into(b"password", b"somesalt", &mut raw)?;
let phc = hasher.hash_password(b"password", b"somesalt")?;
assert!(phc.starts_with("$argon2id$v=19$m=256,t=1,p=1$c29tZXNhbHQ$"));
assert_eq!(hasher.hash(b"password", b"somesalt")?, raw);Implementations§
Source§impl Hasher
impl Hasher
Sourcepub fn set_argon2(&mut self, argon2: Argon2)
pub fn set_argon2(&mut self, argon2: Argon2)
Point the hasher at a different configuration, keeping the memory.
For a process that has to hash at more than one parameter set — a
password migration, say. The arena grows if the new m_cost needs more
blocks and is kept as-is if it needs fewer, so the steady state is one
allocation sized to the largest configuration seen.
Sourcepub fn reserve(&mut self) -> Result<(), Error>
pub fn reserve(&mut self) -> Result<(), Error>
Allocate the arena now instead of during the first hash.
Only moves the cost; it does not remove it. Worth doing when the first
request must not be the slow one, or to find out at start-up rather than
under load that m_cost does not fit in memory.
§Errors
Sourcepub fn reserved_blocks(&self) -> usize
pub fn reserved_blocks(&self) -> usize
Blocks of arena the hasher is holding on to. 1 KiB each.
0 before the first hash, or after clear. Diagnostic:
it is how a test proves that reuse is actually happening.
Sourcepub fn clear(&mut self)
pub fn clear(&mut self)
Give the arena back to the allocator, wiped, and keep the configuration.
For a worker going idle that would rather not sit on m_cost KiB. The
next hash allocates again.
Sourcepub fn hash_into(
&mut self,
pwd: &[u8],
salt: &[u8],
out: &mut [u8],
) -> Result<(), Error>
pub fn hash_into( &mut self, pwd: &[u8], salt: &[u8], out: &mut [u8], ) -> Result<(), Error>
Derive a tag into out. Argon2::hash_into, reusing the arena.
use argon2_rust::{Algorithm, Argon2, Params, Version, params::Memory};
let params = Params::builder().memory(Memory::kib(64)).passes(1).build()?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut hasher = argon2.hasher();
// `Argon2::hasher` allocates nothing; the first hash sizes the arena.
assert_eq!(hasher.reserved_blocks(), 0);
let mut tags = Vec::new();
for pwd in [&b"first"[..], &b"second"[..]] {
let mut tag = [0u8; 32];
hasher.hash_into(pwd, b"somesalt", &mut tag)?;
tags.push(tag);
}
// Two hashes, one arena: 64 blocks of 1 KiB, the `m_cost` above. The
// second call neither allocated nor grew it.
assert_eq!(hasher.reserved_blocks(), 64);
assert_ne!(tags[0], tags[1]);
// Reuse changes where the memory came from and nothing else.
assert_eq!(argon2.hash(b"second", b"somesalt")?, tags[1]);§Errors
Sourcepub fn hash_into_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
out: &mut [u8],
) -> Result<(), Error>
pub fn hash_into_with_ad( &mut self, pwd: &[u8], salt: &[u8], secret: &[u8], ad: &[u8], out: &mut [u8], ) -> Result<(), Error>
Sourcepub fn hash_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<Vec<u8>, Error>
pub fn hash_with_ad( &mut self, pwd: &[u8], salt: &[u8], secret: &[u8], ad: &[u8], ) -> Result<Vec<u8>, Error>
Sourcepub fn hash_encoded_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
) -> Result<String, Error>
pub fn hash_encoded_with_ad( &mut self, pwd: &[u8], salt: &[u8], secret: &[u8], ad: &[u8], ) -> Result<String, Error>
Argon2::hash_encoded_with_ad, reusing the arena.
§Errors
As Argon2::hash_into, plus Error::EncodingFail.
Sourcepub fn verify_with_ad(
&mut self,
pwd: &[u8],
salt: &[u8],
secret: &[u8],
ad: &[u8],
expected: &[u8],
) -> Result<(), Error>
pub fn verify_with_ad( &mut self, pwd: &[u8], salt: &[u8], secret: &[u8], ad: &[u8], expected: &[u8], ) -> Result<(), Error>
Sourcepub fn verify_encoded(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
) -> Result<(), Error>
pub fn verify_encoded( &mut self, encoded: &str, pwd: &[u8], algorithm: Algorithm, ) -> Result<(), Error>
Argon2::verify_encoded, reusing the arena.
The parameters come from encoded, not from this hasher — that is
what verifying a stored PHC string means, and it is what lets one hasher
check strings written at several different m_costs.
use argon2_rust::{Algorithm, Argon2, Error, Params, Version, params::Memory};
let params = Params::builder().memory(Memory::kib(64)).passes(1).build()?;
let mut hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher();
// Registration: one string, carrying the salt and the parameters.
let stored = hasher.hash_encoded(b"password", b"somesalt")?;
assert_eq!(
stored,
"$argon2id$v=19$m=64,t=1,p=1$c29tZXNhbHQ$cpx6VEQbwTVZvcpxNIxOVUWZ5xnAipUmAe1cg2GMG70",
);
// Two logins, over the arena the registration already paid for.
assert_eq!(
hasher.verify_encoded(&stored, b"password", Algorithm::Argon2id),
Ok(()),
);
assert_eq!(
hasher.verify_encoded(&stored, b"wrong", Algorithm::Argon2id),
Err(Error::VerifyMismatch),
);
// The string's `m=64` is not above what this hasher already holds, so
// the pool served both verifies and did not grow. See below for what
// happens when a decoded `m_cost` is larger.
assert_eq!(hasher.reserved_blocks(), 64);§The string cannot grow this hasher — but it can still be huge
Read this one first: what follows bounds what an untrusted m_cost can
retain, and nothing at all about what it can allocate. A decoded
m_cost of 0xFFFFFFFF still asks for a 4 TiB arena here, exactly as it
does in Argon2::verify_encoded and exactly as it does in the C. If
encoded comes from anywhere an attacker can write, bound it first —
Hasher::verify_encoded_bounded does that — or the process dies on the
allocation regardless of everything below.
encoded is untrusted input: on a login endpoint it is whatever the
database row said, and a m_cost field is four bytes of decimal that can
ask for 4 TiB. A pooled arena is retained, so if a decoded m_cost
were allowed to size it, one string would set a permanent high-water mark
on a long-lived per-worker hasher — memory the process never gives back,
chosen by the caller rather than by this hasher’s owner.
So it is not allowed to. A decoded m_cost that fits in memory this
hasher already holds — reserved_blocks, or
the params it is configured for — is served from the
pool as usual. One that would have to grow the pool gets a private
arena instead, allocated, wiped and freed inside this call exactly as
Argon2::verify_encoded does. Verifying still works at any m_cost
the decoder accepts — including ones that will not fit in this machine.
It just cannot leave anything behind.
That mirrors the C, where finalize() ends every argon2_ctx with
free_memory(...) (core.c:184), so argon2_verify never retains an
arena sized by the string it was handed.
To verify and keep the memory — a migration that re-hashes upward, say
— call set_argon2 first. Then the size is the
owner’s choice, which is the whole distinction being drawn here.
§Errors
Sourcepub fn verify_encoded_with_ad(
&mut self,
encoded: &str,
pwd: &[u8],
secret: &[u8],
ad: &[u8],
algorithm: Algorithm,
) -> Result<(), Error>
pub fn verify_encoded_with_ad( &mut self, encoded: &str, pwd: &[u8], secret: &[u8], ad: &[u8], algorithm: Algorithm, ) -> Result<(), Error>
Sourcepub fn hash_password_into(
&mut self,
pwd: &[u8],
salt: &[u8],
out: &mut [u8],
) -> Result<(), Error>
pub fn hash_password_into( &mut self, pwd: &[u8], salt: &[u8], out: &mut [u8], ) -> Result<(), Error>
Derive a raw tag into out, not a PHC string, reusing the arena.
Argon2::hash_password_into over pooled memory, which is the same
function as Hasher::hash_into. out.len() must equal
Params::tag_len_bytes. For the PHC string, Hasher::hash_password.
§Errors
Sourcepub fn hash_password(
&mut self,
pwd: &[u8],
salt: &[u8],
) -> Result<String, Error>
pub fn hash_password( &mut self, pwd: &[u8], salt: &[u8], ) -> Result<String, Error>
Derive a tag and return the PHC string for it, reusing the arena.
Argon2::hash_password over pooled memory, which is the same function
as Hasher::hash_encoded. For the raw tag instead, its sibling
Hasher::hash_password_into or Hasher::hash.
§Errors
As Argon2::hash_into, plus Error::EncodingFail.
Sourcepub fn hash_password_with_random_salt(
&mut self,
pwd: &[u8],
) -> Result<String, Error>
pub fn hash_password_with_random_salt( &mut self, pwd: &[u8], ) -> Result<String, Error>
Derive a PHC string with a fresh salt from the OS entropy source, reusing the arena.
Argon2::hash_password_with_random_salt over pooled memory, which is
Hasher::hash_encoded with a RANDOM_SALT_LEN-byte salt drawn for
you and carried in the returned string. There is no raw-tag counterpart:
a caller who keeps the tag has to keep the salt too, and then generating
it here saves nothing.
This is the spelling that matters for the case the type exists to serve: a long-lived per-worker hasher registering many users, where every hash wants both the pooled arena and a fresh salt.
§Errors
Error::OsRandom if every OS entropy source fails, plus the errors of
Hasher::hash_encoded.
Sourcepub fn verify_password(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
) -> Result<(), Error>
pub fn verify_password( &mut self, encoded: &str, pwd: &[u8], algorithm: Algorithm, ) -> Result<(), Error>
Check pwd against a PHC string, not a raw tag, reusing the arena.
Argon2::verify_password over pooled memory, which is the same
function as Hasher::verify_encoded and inherits its pooled-arena
rule: a decoded m_cost above this hasher’s high-water mark runs on a
private arena that is freed on the way out, so the string cannot grow
the pool. To check a raw expected tag instead, Hasher::verify.
§Errors
Sourcepub fn verify_encoded_bounded(
&mut self,
encoded: &str,
pwd: &[u8],
algorithm: Algorithm,
ceiling: &Params,
) -> Result<(), Error>
pub fn verify_encoded_bounded( &mut self, encoded: &str, pwd: &[u8], algorithm: Algorithm, ceiling: &Params, ) -> Result<(), Error>
Argon2::verify_encoded_bounded, reusing the arena.
The ceiling is checked before anything is allocated, so it bounds the
allocation — which is the half Hasher::verify_encoded does not
address. Note that the pooled-arena rule still applies underneath: a
decoded m_cost within ceiling but above this hasher’s own high-water
mark runs on a private arena, so passing a generous ceiling cannot
enlarge the pool either.
ceiling.threads() bounds the worker threads exactly as it does on
Argon2::verify_encoded_bounded — worth knowing here in particular,
since a Hasher is what a server holds while verifying strings it did
not write, and the arena it reuses is not the only resource a wide p
can spend.