Skip to main content

Hasher

Struct Hasher 

Source
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

Source

pub const fn argon2(&self) -> &Argon2

The configuration this hasher applies.

Source

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.

Source

pub const fn algorithm(&self) -> Algorithm

The configured algorithm.

Source

pub const fn version(&self) -> Version

The configured version.

Source

pub const fn params(&self) -> &Params

The configured parameters.

Source

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

Error::MemoryAllocationError.

Source

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.

Source

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.

Source

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

As Argon2::hash_into.

Source

pub fn hash_into_with_ad( &mut self, pwd: &[u8], salt: &[u8], secret: &[u8], ad: &[u8], out: &mut [u8], ) -> Result<(), Error>

Argon2::hash_into_with_ad, reusing the arena.

§Errors

As Argon2::hash_into.

Source

pub fn hash(&mut self, pwd: &[u8], salt: &[u8]) -> Result<Vec<u8>, Error>

Argon2::hash, reusing the arena.

§Errors

As Argon2::hash_into.

Source

pub fn hash_encoded(&mut self, pwd: &[u8], salt: &[u8]) -> Result<String, Error>

Argon2::hash_encoded, reusing the arena.

§Errors

As Argon2::hash_into, plus Error::EncodingFail.

Source

pub fn verify( &mut self, pwd: &[u8], salt: &[u8], expected: &[u8], ) -> Result<(), Error>

Argon2::verify, reusing the arena.

§Errors

As Argon2::hash_into, or Error::VerifyMismatch.

Source

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

As Argon2::verify_encoded.

Source

pub fn verify_encoded_with_ad( &mut self, encoded: &str, pwd: &[u8], secret: &[u8], ad: &[u8], algorithm: Algorithm, ) -> Result<(), Error>

Source

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

As Argon2::hash_into.

Source

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.

Source

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.

Source

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

As Argon2::verify_encoded.

Source

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.

§Errors

As Argon2::verify_encoded_bounded.

Source

pub fn verify_encoded_bounded_with_ad( &mut self, encoded: &str, pwd: &[u8], secret: &[u8], ad: &[u8], algorithm: Algorithm, ceiling: &Params, ) -> Result<(), Error>

Trait Implementations§

Source§

impl Debug for Hasher

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.