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.
Backend | Requires | Source it was ported from |
|---|---|---|
Scalar | — | src/ref.c |
Neon | aarch64 | src/opt.c (128-bit path) |
Sse2 | x86/x86_64 + SSE2 | src/opt.c (128-bit path) |
Avx2 | x86_64 + AVX2 | src/opt.c (__AVX2__) |
Avx512 | x86_64 + AVX-512F | src/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, impliesstd) — multi-threaded fill. Onestd::thread::scopefor the whole fill, whose workers meet at a barrier at each of the4 * t_costalgorithmic sync points, rather than a fresh scope per sync point. Note that the thread count does not change the tag; onlyParams::lanesdoes.zeroize-memory(default) — securely wipe internal buffers, the equivalent ofFLAG_clear_internal_memoryin the C.bump-alloc— internal test/bench control. Together withinternal-api, givesmemory::Workspacea reusable bump allocator for measuring small scratch buffers. It does not change the stable hash/encode/verify paths, which deliberately keep theirVecs. Measured upper bound: 17 ns per hash, or 0.00013% of an RFC 9106 hash.internal-api— exposes__internalfor 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_codesfromphc-winner-argon2/include/argon2.h. - params
- Limits,
Algorithm,VersionandParams.
Structs§
Enums§
- Backend
- Which
fill_segmentimplementation to use.
Constants§
- BOUNDED_
MAX_ SALT_ LEN - Longest salt the
*_boundedverify entry points will accept, in bytes. - RANDOM_
SALT_ LEN - Salt length used by the
*_with_random_saltentry points, in bytes.
Functions§
- detected_
backend - The
Backendthis CPU resolved to, cached after the first call. - encoded_
len argon2_encodedlen(...)fromsrc/argon2.c.