Skip to main content

Crate argon2_rust

Crate argon2_rust 

Source
Expand description

A pure-Rust port of the reference Argon2 implementation (phc-winner-argon2), with runtime-dispatched SIMD backends.

Argon2 is the winner of the 2015 Password Hashing Competition and is specified in RFC 9106. Three variants exist, see Algorithm:

  • Argon2d — data-dependent addressing: fastest, but leaks a memory access pattern that depends on the password.
  • Argon2i — data-independent addressing: side-channel resistant.
  • Argon2id — independent for the first half-pass, dependent afterwards. The default, and what RFC 9106 recommends.

§Example

use argon2_rust::{Algorithm, Argon2, Error, Params, Version};

// `Params::default()` is m=19456 KiB (19 MiB), t=2, 1 lane, 32-byte tag:
// the OWASP-style figure this crate ships as its default, and a sound
// starting point for a password store. One hash of it measures about 8 ms
// in a release build on an M-series laptop, cheap enough that this runs as
// a real doctest. Raise `m_cost` until it fits your own timing budget.
let params = Params::default();
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);

let mut tag = [0u8; 32];
argon2.hash_into(b"password", b"somesalt", &mut tag)?;
assert_eq!(argon2.verify(b"password", b"somesalt", &tag), Ok(()));
assert_eq!(
    argon2.verify(b"wrong", b"somesalt", &tag),
    Err(Error::VerifyMismatch),
);

§SIMD backends

The compression function is selected by runtime CPU feature detection, never by cfg(target_feature) alone, so one binary runs at full speed on every machine. Detection happens at most once per process and the result is cached; a hash call resolves one function pointer before entering its loops. Ask detected_backend what this CPU picked. The cost model is documented on the private fill_block module.

BackendRequiresSource it was ported from
Scalarsrc/ref.c
Neonaarch64src/opt.c (128-bit path)
Sse2x86/x86_64 + SSE2src/opt.c (128-bit path)
Avx2x86_64 + AVX2src/opt.c (__AVX2__)
Avx512x86_64 + AVX-512Fsrc/opt.c (__AVX512F__)

The unpadded standard-Base64 codec used by PHC strings has a separate, cached dispatch: AVX2, SSSE3, AArch64 NEON, wasm SIMD128, then scalar. Its vector kernels follow base64-simd/aklomp/base64, while inputs shorter than one vector stay on the original scalar loop. As in base64-simd, AVX-512-capable CPUs use this codec’s AVX2 path; the wider backend remains specific to the Argon2 compression function above.

§Features

  • std (default) — runtime CPU feature detection.
  • parallel (default, implies std) — multi-threaded fill. One std::thread::scope for the whole fill, whose workers meet at a barrier at each of the 4 * t_cost algorithmic sync points, rather than a fresh scope per sync point. Note that the thread count does not change the tag; only Params::lanes does.
  • zeroize-memory (default) — securely wipe internal buffers, the equivalent of FLAG_clear_internal_memory in the C.
  • bump-alloc — internal test/bench control. Together with internal-api, gives memory::Workspace a reusable bump allocator for measuring small scratch buffers. It does not change the stable hash/encode/verify paths, which deliberately keep their Vecs. Measured upper bound: 17 ns per hash, or 0.00013% of an RFC 9106 hash.
  • internal-api — exposes __internal for tests and benches. Not stable.

The crate is #![no_std] and needs only alloc; that stays true with every feature turned on. Without std, backend selection falls back to compile-time target_feature cfgs.

§Not a password-hash provider

This is a port of the C reference, not an implementation of the RustCrypto password-hash traits: there is no PasswordHasher or PasswordVerifier here, and no dependency that would supply one. Params carries no serde impls either, though its full state round-trips through the accessors and Params::to_builder. Interoperation is at the string level — the PHC strings this crate reads and writes are the ones the argon2 crate reads and writes.

§Reusing memory across hashes

Each Argon2 hash acquires its block arena once and releases it on the way out. A process that hashes repeatedly can instead keep a Hasher, from Argon2::hasher, which parks the arena between calls and wipes it on release, so the next call gets a zeroed arena that is already mapped and already resident.

Skipping the mmap, the first-touch faults and the munmap is worth around -25% at m_cost = 64 MiB, and next to nothing below 1 MiB, where a hash is mostly BLAKE2b; Hasher has the per-cost measurements.

use argon2_rust::{Algorithm, Argon2, Hasher, Params, Version, params::Memory};

// Nameable, so it can be a field, a `thread_local!` or a worker slot —
// which is the only shape in which reuse is worth anything.
struct Worker {
    hasher: Hasher,
}

let params = Params::builder().memory(Memory::kib(1 << 8)).passes(1).build()?;
let mut worker = Worker {
    hasher: Argon2::new(Algorithm::Argon2id, Version::V0x13, params).hasher(),
};
let mut tag = [0u8; 32];
worker.hasher.hash_into(b"password", b"somesalt", &mut tag)?;

§Salts

Argon2::hash_password_with_random_salt (and the pooled Hasher::hash_password_with_random_salt) draw a RANDOM_SALT_LEN-byte salt from the OS and put it in the returned PHC string, so nothing has to be stored alongside. The entropy comes from whichever entry point is correct for the target — getrandom(2), getentropy, CCRandomGenerateBytes, ProcessPrng, WASI random_get, or /dev/urandom — each declared by hand, so this costs no dependency. Callers who already run a CSPRNG should keep passing their own salt.

§Verifying strings you did not write

m_cost in a PHC string is up to ten digits of decimal, and Argon2::verify_encoded will honour all of them — up to params::MAX_MEMORY KiB, which is 4 TiB — because argon2_verify does too. That is fine for a config file and a denial of service for a login endpoint. Argon2::verify_encoded_bounded takes a ceiling and rejects an over-large cost while it is still a number, before anything is allocated:

use argon2_rust::{Algorithm, Argon2, Error, Params, params::Memory};

let hostile = "$argon2id$v=19$m=4294967295,t=1,p=1$c29tZXNhbHQ$\
               CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc";
let ceiling = Params::builder().memory(Memory::mib(64)).passes(8).lanes(4).build()?;
assert_eq!(
    Argon2::verify_encoded_bounded(hostile, b"pw", Algorithm::Argon2id, &ceiling),
    Err(Error::MemoryTooMuch),
);

Memory is not the only resource the string spends. Decoding sets threads = lanes (C parity), so p also picks how many OS threads the verify spawns; the ceiling’s own threads bounds that, and a ceiling that leaves ParamsBuilder::threads unset bounds it together with lanes. Set that one setter to accept wide strings without spawning wide. The clamp cannot change a verdict — only lanes feeds the tag.

§Panics

No fallible path in this crate panics: hashing, verifying, encoding, decoding and parameter validation all report failure as an Error, whose numeric Error::as_c_code matches the C reference — except for the crate-specific codes below Error::MIN_C_CODE, which the C has no equivalent for.

There is exactly one intentional exception, and it is not on a fallible path: ParamsBuilder::build_or_panic panics on invalid parameters. It exists so a const item can turn bad parameters into a compile error, which is what a panic in a const evaluation is. Anywhere a runtime error is the right answer, use ParamsBuilder::build — it is the normal way in.

Re-exports§

pub use crate::error::Error;
pub use crate::params::Algorithm;
pub use crate::params::Params;
pub use crate::params::Version;

Modules§

error
Error codes, mirroring argon2_error_codes from phc-winner-argon2/include/argon2.h.
params
Limits, Algorithm, Version and Params.

Structs§

Argon2
A configured Argon2 hasher.
Hasher
An Argon2 that keeps its block arena between calls.

Enums§

Backend
Which fill_segment implementation to use.

Constants§

BOUNDED_MAX_SALT_LEN
Longest salt the *_bounded verify entry points will accept, in bytes.
RANDOM_SALT_LEN
Salt length used by the *_with_random_salt entry points, in bytes.

Functions§

detected_backend
The Backend this CPU resolved to, cached after the first call.
encoded_len
argon2_encodedlen(...) from src/argon2.c.