The suprnova::hashing module is the framework's password hashing surface, with three first-class drivers - bcrypt (default, matches Laravel), Argon2i (memory-hard, side-channel-resistant), and Argon2id (OWASP 2024 recommendation). Use it when storing user passwords, hashing remember-me verifier tokens, or anywhere a one-way function is the right primitive. Driver selection is env-driven, and the facade is algorithm-aware end-to-end (info, is_hashed, needs_rehash, verify) so a stored bcrypt hash still verifies after you flip HASH_DRIVER=argon2id.
Overview
use hashing;
// Async (preferred inside Tokio request handlers - runs the CPU-bound
// hash on spawn_blocking so the worker thread stays free):
let hashed = hash_async.await?;
let valid = verify_async.await?;
// Sync (tests, CLI tools, non-async contexts):
let hashed = hash?;
let valid = verify?;
The free-function facade reads the active driver from HASH_DRIVER (or falls back to bcrypt). For explicit-driver calls, construct the driver type directly and pass it to hash_with / verify_with / needs_rehash_with.
Configuration
| Variable | Description | Default | Range |
|---|---|---|---|
HASH_DRIVER |
Active algorithm | bcrypt |
bcrypt | argon | argon2i | argon2id |
HASH_ROUNDS |
Bcrypt cost factor | 12 |
4..=31 (bcrypt only) |
HASH_MEMORY |
Argon memory cost in KiB | 65536 (64 MiB) |
>= 8 (argon only) |
HASH_TIME |
Argon time iterations | 4 |
>= 1 (argon only) |
HASH_THREADS |
Argon parallelism / lanes | 1 |
>= 1 (argon only) |
HASH_VERIFY |
When true, verify() rejects cross-algorithm hashes |
false |
true / false |
Misconfiguration (bad value, out-of-range parameter) surfaces as a FrameworkError::param at the first call to hash / verify / needs_rehash - not as a silent default.
Example .env for argon2id
HASH_DRIVER=argon2id
HASH_MEMORY=65536
HASH_TIME=4
HASH_THREADS=1
Why Suprnova's Argon2 defaults are stronger than Laravel's
| Param | Laravel default | Suprnova default | Source |
|---|---|---|---|
| Memory | 1 024 KiB (1 MiB) | 65 536 KiB (64 MiB) | OWASP 2024 |
| Time | 2 iterations | 4 iterations | OWASP 2024 |
| Threads | 2 | 1 | OWASP 2024 / libsodium-aligned |
Laravel's defaults assume PHP's request-per-process model - a worker can only spend so much on each password hash before the box is full. Tokio's spawn_blocking lets Suprnova hand the hash off to a blocking thread pool without freezing the request loop, so the OWASP 2024 numbers are realistic on real production hardware.
Drivers
Bcrypt (default)
use ;
let driver = new;
let hashed = hash_with?;
assert!;
Bcrypt has a 72-byte block-size cap on the password input - the underlying primitive silently truncates longer inputs, which means two distinct passphrases sharing their first 72 bytes hash to the same value. Suprnova rejects up-front (the framework's bcrypt path errors on hash() and returns Ok(false) on verify() for oversized passwords, keeping the auth flow's "invalid credentials" response uniform). Argon2 has no such ceiling.
The bcrypt cap is exposed as suprnova::hashing::MAX_BCRYPT_PASSWORD_BYTES (71 - the usable limit after the bcrypt null terminator).
Argon2id (OWASP 2024 recommendation)
use ;
let driver = new?;
let hashed = hash_with?;
assert!;
// Argon2 accepts arbitrary-length passphrases - the bcrypt 72-byte cap
// doesn't apply.
let long = "x".repeat;
let h = hash_with?;
assert!;
Argon2i
Same shape as Argon2id; Argon2iHasher::new(opts). Use Argon2id for new projects - Argon2i is supported for parity but Argon2id is the modern recommendation.
Bcrypt with an explicit cost (hash_with_cost)
hash_with_cost(password, cost) and hash_with_cost_async(password, cost) mint a bcrypt hash at a caller-supplied cost factor regardless of HASH_DRIVER. Use these when policy or per-tenant config flows a cost into the call site rather than into the process env - for example, a high-security account class that uses cost 14 while the rest of the app runs at the default 12.
use ;
// Sync - tests, CLI tools.
let h = hash_with_cost?;
// Async - inside Tokio request handlers.
let h = hash_with_cost_async.await?;
Both entry points reject cost outside MIN_BCRYPT_COST..=MAX_BCRYPT_COST (4..=31) with FrameworkError::param, mirroring the env-side HASH_ROUNDS validation:
use ;
assert!; // < 4
assert!; // > 31
The bounds check matters because each cost increment doubles CPU time. At cost 31 a single bcrypt hash takes hours on commodity hardware - bounds-checking inside the framework keeps a policy/config typo from accidentally pinning a worker thread for the rest of the day. The async variant goes through spawn_blocking so even a legitimately high cost doesn't freeze the request loop.
Algorithm-aware needs_rehash
needs_rehash returns true when the stored hash should be re-hashed under the active driver. It covers three cases:
- Algorithm mismatch - bcrypt hash stored while
HASH_DRIVER=argon2id(or vice versa). Triggers a rotation on next successful verify. - Parameter weakness - bcrypt cost below
HASH_ROUNDS, or argonm/t/pbelowHASH_MEMORY/HASH_TIME/HASH_THREADS. - Bcrypt legacy variants -
$2a$,$2x$,$2y$rotate to canonical$2b$even at the configured cost.
if needs_rehash
Malformed input returns true - the caller naturally rotates anything it can't parse.
Hash inspection (info + is_hashed)
use ;
let h = hash_async.await?;
let i = info;
println!;
println!;
println!;
// True for any recognised algorithm hash; false for plaintext / garbage.
assert!;
assert!;
info().algo is one of: Bcrypt, Argon2i, Argon2id, Argon2d (recognised but never minted), Unknown.
is_hashed is what the AsHashed eloquent cast uses to skip re-hashing an already-hashed column - works across all three drivers, so flipping HASH_DRIVER mid-project doesn't cause a hash-of-hash loop on the next save.
Cross-algorithm verification gate (HASH_VERIFY)
By default, verify() checks the password against the hash regardless of which algorithm produced the hash - this is what lets legacy bcrypt hashes still verify after you flip HASH_DRIVER=argon2id (so you can rotate them on login). Set HASH_VERIFY=true once every user is rotated to enforce the active algorithm strictly:
HASH_VERIFY=true
With the gate on, verify() returns Ok(false) for any hash whose algorithm differs from the active driver - same shape as Laravel's RuntimeException, but Suprnova returns false rather than throwing because the auth-flow caller expects a Result<bool> either way.
Async vs sync
Both bcrypt at cost 12 (~250 ms) and Argon2id at memory=64 MiB (~80 ms) are intentionally CPU-bound - that's the entire point of slow hashing. Calling the sync hash / verify directly from a Tokio request handler blocks the worker thread for the whole hash duration, starving other requests on the same worker.
Use the *_async siblings inside async fn handlers. They wrap the CPU-bound call in tokio::task::spawn_blocking so the worker stays free for other requests:
// GOOD - inside an async handler
let hashed = hash_async.await?;
// BAD - blocks the worker for ~250 ms
let hashed = hash?;
The sync variants are for tests, CLI tools, and other non-async contexts where blocking is fine.
Eloquent integration: AsHashed cast
The #[cast(AsHashed)] eloquent cast hashes a plaintext field on write using the active driver, and is idempotent across all drivers - saving a model whose password column already contains a recognised hash (bcrypt or argon) passes the value through unchanged. Without this guard, User::find(id).await?.save().await? would hash the existing hash on every save, breaking authentication.
use AsHashed;
The idempotence check uses hashing::is_hashed, so flipping HASH_DRIVER mid-project is safe - both the legacy bcrypt hashes and the fresh argon2id hashes are recognised and skipped on re-save.
Use with Auth::attempt
Auth::attempt(&credentials) calls UserProvider::validate_credentials, which in turn calls hashing::verify_async against the user's stored hash. Verify dispatches on the stored hash's algorithm, not the configured driver - so after you flip HASH_DRIVER=argon2id, every existing bcrypt hash still verifies, and needs_rehash returns true so the standard rotate-on-login pattern carries the user base across to the new algorithm one login at a time.
Overriding the driver in tests
set_default_driver(Box<dyn Hasher>) installs a driver programmatically for tests and embedded CLI tools that build the driver without going through HASH_DRIVER. It is one-shot - the first call wins, and a second call returns FrameworkError::internal rather than swapping the driver mid-process. Use it at suite startup, before any code path resolves the default.
Next
- Authentication -
Auth::attempt, the user-provider trait, and how hashing integrates with login - Auth flows -
PasswordReset::completerotates the stored password hash through the active driver; remember-me tokens are hashed before storage viahash_async - Eloquent -
#[cast(AsHashed)]reference and the broader cast surface - Encryption - two-way authenticated encryption for at-rest data; the complement to one-way hashing
- Error Model - what
FrameworkError::paramlooks like when a hashing config value is rejected
