Password Hashing
18 min read
Storing user passwords is the one place where “just hash it” is dangerous advice. A fast hash like SHA-256 is worse than useless for passwords because attackers can try billions of guesses per second. This page shows how to do it right in Rust with Argon2 (and bcrypt), and how it maps to the Node argon2/bcrypt packages you may already know.
Quick Overview
Section titled “Quick Overview”A password hash is a deliberately slow, salted, one-way transformation of a password that you store instead of the password itself. When a user logs in, you re-run the function on their attempt and compare. The defining feature of a good password hash is that it is memory-hard and tunably slow, so a stolen database is expensive to brute-force.
For a TypeScript/JavaScript developer, the mental model is almost identical to the npm argon2 package: you call a hash function that bakes a random salt into a self-describing string, and a verify function that reads the salt back out. In Rust this is built around the password-hash crate’s PasswordHasher and PasswordVerifier traits, with argon2 as the recommended implementation.
Note: This page is about password hashing specifically. For general-purpose cryptography (encryption, AEAD, MACs) see cryptography.md; for the random number sources used to make salts see secure-randomness.md.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”A typical Node service using the native argon2 package (the recommended choice over bcryptjs today):
// npm install argon2import argon2 from "argon2";
interface UserRecord { username: string; passwordHash: string; // store THIS, never the password}
// Registration: hash the password for storage.async function register(username: string, password: string): Promise<UserRecord> { // argon2.hash() defaults to Argon2id, generates a random salt, // and returns a self-describing PHC string. const passwordHash = await argon2.hash(password); return { username, passwordHash };}
// Login: re-hash the attempt and compare in constant time.async function verifyLogin(record: UserRecord, attempt: string): Promise<boolean> { // verify() reads the salt + parameters out of the stored hash. return argon2.verify(record.passwordHash, attempt);}
const user = await register("alice", "s3cr3t-p@ssw0rd");console.log(user.passwordHash);// $argon2id$v=19$m=65536,t=3,p=4$8ykm3QdFyZnBmyrRKiy2MQ$y+Ds3M9MFMMBACIQHF/c2iZ5U5+oa8d2mS/nyuJ2Kt0
console.log(await verifyLogin(user, "s3cr3t-p@ssw0rd")); // trueconsole.log(await verifyLogin(user, "wrong")); // falseKey points:
argon2.hash(password)returns a PHC string that embeds the algorithm, version, parameters, salt, and digest.- You never store or manage the salt yourself — it travels inside the hash string.
argon2.verify(hash, attempt)returns aPromise<boolean>; it does the constant-time comparison internally.
Warning: Do not use Node’s built-in
crypto.createHash("sha256")ormd5for passwords. Those are fast general-purpose hashes; the same warning applies to Rust’ssha2crate. Password hashing needs a purpose-built, slow function.
Rust Equivalent
Section titled “Rust Equivalent”The same registration/login flow with the argon2 crate. Add the dependency first:
cargo add argon2 --features stdThis pulls in argon2 = "0.5", which re-exports the password-hash crate (the traits, the salt type, and a CSPRNG-backed salt generator).
use argon2::{ password_hash::{ rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Argon2,};
fn main() { let password = b"correct horse battery staple";
// 1. Generate a random salt from the OS CSPRNG. let salt = SaltString::generate(&mut OsRng);
// 2. Argon2::default() is Argon2id with current recommended parameters. let argon2 = Argon2::default();
// 3. Hash into a self-describing PHC string (store THIS). let hash = argon2 .hash_password(password, &salt) .expect("hashing failed") .to_string(); println!("PHC string: {hash}");
// 4. Verify: parse the stored string, then check the attempt. let parsed = PasswordHash::new(&hash).expect("invalid PHC string"); let ok = Argon2::default() .verify_password(password, &parsed) .is_ok(); println!("correct password verifies: {ok}");
let wrong = Argon2::default() .verify_password(b"wrong password", &parsed) .is_ok(); println!("wrong password verifies: {wrong}");
// 5. The same input hashed twice differs, because the salt is random. let salt2 = SaltString::generate(&mut OsRng); let hash2 = Argon2::default() .hash_password(password, &salt2) .unwrap() .to_string(); println!("same input, hashes equal: {}", hash == hash2);}Real output:
PHC string: $argon2id$v=19$m=19456,t=2,p=1$hOtbciLFrd1ihAGsRiEoIg$RN2Tf/CQZ8OcIaVfm3vz0q+bujmy5+/vxzMamQ3t4D0correct password verifies: truewrong password verifies: falsesame input, hashes equal: falseKey points:
hash_passwordandverify_passwordcome from thePasswordHasher/PasswordVerifiertraits — you must bring them into scope withuse.- The output is a PHC string, exactly like the Node
argon2package produces. It is a singleStringyou put in one database column. - Passwords are passed as
&[u8](byte slices), not&str. Calling.as_bytes()on aString/&stris how you get there.
Detailed Explanation
Section titled “Detailed Explanation”Why slow + salted, line by line
Section titled “Why slow + salted, line by line”A password hash defends against two distinct attacks, and each design choice targets one of them:
-
Salting defeats precomputation. A salt is a random value mixed into the hash so that two users with the same password get different hashes. Without it, an attacker uses a rainbow table (a precomputed map of
hash → password) and cracks the whole database at once.SaltString::generate(&mut OsRng)draws ~16 bytes from the operating system CSPRNG. That is why our two-hashes-of-the-same-password check printedfalse. -
Slowness defeats brute force.
Argon2::default()is configured to take meaningful CPU and memory per hash. The PHC parametersm=19456,t=2,p=1mean 19,456 KiB (19 MiB) of memory, 2 iterations, and 1 lane of parallelism. That is fast enough for a login request (single-digit milliseconds) but turns an attacker’s “billions of guesses per second” on a fast hash into a far smaller number on commodity hardware, and the 19 MiB memory cost specifically blunts GPU and ASIC cracking rigs.
Anatomy of the PHC string
Section titled “Anatomy of the PHC string”$argon2id$v=19$m=19456,t=2,p=1$hOtbciLFrd1ihAGsRiEoIg$RN2Tf/CQZ8OcIaVfm3vz0q+bujmy5+/vxzMamQ3t4D0 └─ algo ─┘└ver┘└──── params ────┘└──── salt (b64) ────┘└─────────── digest (b64) ───────────────┘Because the salt and parameters are stored inside the string, verify_password can reproduce the exact computation. You never need a separate salt column, and you can change your global parameters later without breaking existing users — verify uses whatever is embedded in each stored hash.
Argon2id vs Argon2i vs Argon2d
Section titled “Argon2id vs Argon2i vs Argon2d”Argon2 has three variants. Argon2::default() picks Argon2id, the hybrid that the OWASP Password Storage Cheat Sheet recommends for password storage — it resists both side-channel attacks (the i strength) and GPU/time-memory tradeoff attacks (the d strength). Unless you have a specific reason, use the default.
Tuning the cost parameters
Section titled “Tuning the cost parameters”Argon2::default() is a good baseline, but you may want to set parameters explicitly so they are visible and reviewable:
use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Algorithm, Argon2, Params, Version,};
fn main() { // m_cost = 19456 KiB (19 MiB), t_cost = 2 iterations, p_cost = 1 lane. // These are a current OWASP-recommended starting point. let params = Params::new(19_456, 2, 1, None).expect("invalid params"); let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let salt = SaltString::generate(&mut OsRng); let hash = argon2 .hash_password(b"hunter2", &salt) .expect("hash failed") .to_string(); println!("{hash}");}Real output:
$argon2id$v=19$m=19456,t=2,p=1$nDrBHfbTwksob591a0f2XA$wqAf/QsgYOxUHbz/VRWJJs9dlmQt86VXYWmEvTfT5tgTip: Tune so that a single hash takes roughly 0.5–1 second on your production hardware, then back off if that hurts throughput. The right numbers depend on your CPU and how many logins per second you must serve. Benchmark; don’t cargo-cult a number.
The bcrypt alternative
Section titled “The bcrypt alternative”bcrypt predates Argon2 and is still perfectly acceptable for password storage; it is what many existing systems use. The bcrypt crate has a simpler, function-style API:
cargo add bcryptuse bcrypt::{hash, verify, DEFAULT_COST};
fn main() { let password = "correct horse battery staple";
// hash() generates a random salt and embeds it in the output. let hashed = hash(password, DEFAULT_COST).expect("hashing failed"); println!("bcrypt hash: {hashed}"); println!("cost (DEFAULT_COST) = {DEFAULT_COST}");
println!("correct verifies: {}", verify(password, &hashed).unwrap()); println!("wrong verifies: {}", verify("nope", &hashed).unwrap());}Real output:
bcrypt hash: $2b$12$Ttcq3h2TaM9ZeEWCGgiVge5yj33FaydsAeRyhyVQWoxTX/K5YW4Rucost (DEFAULT_COST) = 12correct verifies: truewrong verifies: falseThe $2b$12$... prefix is bcrypt’s own self-describing format: variant 2b, cost factor 12 (meaning 2^12 rounds), then salt+digest. The cost is the one knob — raise it as hardware gets faster.
Warning: bcrypt silently truncates passwords to 72 bytes. Anything past byte 72 is ignored, so a 100-character passphrase is no stronger than its first 72 bytes. Argon2 has no such limit. This is a real, frequently-overlooked footgun (demonstrated under Common Pitfalls).
Key Differences
Section titled “Key Differences”| Concern | TypeScript / Node | Rust |
|---|---|---|
| Recommended package | argon2 (npm) | argon2 crate |
| Default algorithm | Argon2id | Argon2id (Argon2::default()) |
| API shape | async functions hash/verify | trait methods hash_password/verify_password (synchronous) |
| Where the salt lives | inside the PHC string | inside the PHC string |
| Output type | Promise<string> | Result<PasswordHash, Error> → .to_string() |
| Password input | string | &[u8] (use .as_bytes()) |
| Comparison safety | constant-time inside verify | constant-time inside verify_password |
| CPU binding | native addon (libsodium-style) | pure Rust, no system libs |
A few conceptual differences worth internalizing:
-
Synchronous, not async. Node’s
argon2.hashreturns aPromisebecause the native addon offloads work to a thread pool. The Rustargon2crate is a plain synchronous CPU computation. In an async service (axum, tokio) you should wrap a hash call intokio::task::spawn_blockingso the ~1 ms–1 s of CPU work does not stall the async runtime’s worker thread. See ../11-async for the blocking-vs-async distinction. -
Traits, not free functions.
hash_passwordlives on thePasswordHashertrait. If you forget theuse, the method appears to not exist (see Common Pitfalls). This is Rust’s normal “methods come from traits in scope” rule — see ../09-generics-traits. -
Errors are values. Node throws on a malformed hash; Rust returns
Result. A failed verify isErr(...), not an exception, so you handle it withmatch/?like any other error handling. -
No global state. There is no implicit “pepper” or process-wide config. Everything that affects a hash is either in the PHC string or in the
Argon2value you constructed.
Common Pitfalls
Section titled “Common Pitfalls”Forgetting to import the trait
Section titled “Forgetting to import the trait”The methods come from PasswordHasher / PasswordVerifier. Omit the use and the compiler says the method does not exist:
use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2,};
fn main() { let salt = SaltString::generate(&mut OsRng); // does not compile (error[E0599]) — PasswordHasher trait not in scope let _hash = Argon2::default().hash_password(b"pw", &salt).unwrap();}Real compiler error (truncated):
error[E0599]: no method named `hash_password` found for struct `Argon2` in the current scope --> src/main.rs:9:35 | 9 | let _hash = Argon2::default().hash_password(b"pw", &salt).unwrap(); | ^^^^^^^^^^^^^... = help: items from traits can only be used if the trait is in scopehelp: trait `PasswordHasher` which provides `hash_password` is implemented but not in scope; perhaps you want to import it | 1 + use argon2::PasswordHasher; |The fix is exactly what the compiler suggests: use argon2::PasswordHasher; (and PasswordVerifier for verifying).
Comparing hashes with == instead of verifying
Section titled “Comparing hashes with == instead of verifying”A tempting but wrong instinct is to re-hash the attempt and compare strings:
// Logic bug, NOT a compile error — this will reject every correct password.// let attempt_hash = Argon2::default()// .hash_password(attempt, &fresh_salt)?// .to_string();// let ok = attempt_hash == stored_hash; // ALWAYS false: different random salts!Because each hash uses a new random salt, two hashes of the same password never match by string equality — that is the whole point of salting. You must call verify_password, which re-uses the salt embedded in the stored hash and performs a constant-time digest comparison. String == would also leak timing information even if the salts matched.
Using a fast general-purpose hash
Section titled “Using a fast general-purpose hash”// Insecure for passwords (compiles fine, ships a vulnerability).// use sha2::{Digest, Sha256};// let digest = Sha256::digest(password); // GPU-crackable at billions/sec, no saltSHA-256, SHA-512, and MD5 are designed to be fast, which is precisely the wrong property here. They are correct for file integrity and HMACs, never for passwords. Reach for argon2 (or bcrypt) instead.
bcrypt’s 72-byte truncation
Section titled “bcrypt’s 72-byte truncation”use bcrypt::{hash, verify, DEFAULT_COST};
fn main() { let base = "a".repeat(72); let longer = format!("{base}EXTRA-IGNORED-BYTES");
let h = hash(&base, DEFAULT_COST).unwrap(); // Extra bytes past 72 are ignored, so a DIFFERENT password verifies. println!( "72-byte password verifies with +18 extra bytes: {}", verify(&longer, &h).unwrap() );}Real output:
72-byte password verifies with +18 extra bytes: trueIf you must support long passphrases with bcrypt, pre-hash with SHA-256 and base64-encode before bcrypt — or simply use Argon2, which has no length limit.
Logging or returning the hash
Section titled “Logging or returning the hash”A PHC string is not a secret you should display, but it is also not something to scatter through logs. Treat it like any credential material: do not log it, do not return it in API responses. For active in-memory secrets, see secrets-management.md.
Best Practices
Section titled “Best Practices”- Default to Argon2id. Use
Argon2::default()(or explicitAlgorithm::Argon2id) unless you are interoperating with an existing bcrypt store. - Never manage salts manually. Let
SaltString::generate(&mut OsRng)and the PHC string handle it. A salt you generate with a non-CSPRNG (e.g. the defaultrandthread RNG withoutOsRng) is a bug — see secure-randomness.md. - Store the whole PHC string in one column. No separate salt/params columns. This makes parameter upgrades trivial.
- Run hashing off the async executor. In tokio/axum services, wrap
hash_password/verify_passwordinspawn_blockingso a slow hash does not block other requests. - Re-hash on login when parameters change. After a successful
verify_password, check whether the stored hash used your current parameters; if not, transparently re-hash the just-verified plaintext and update the row. This lets you raise cost over time without forcing password resets. - Compare in constant time. Always go through
verify_password(Argon2) orverify(bcrypt); never==on hashes or digests. - Pin and audit your dependencies. Password hashing is exactly the kind of code where a known-vulnerable transitive dependency matters — run
cargo audit(see security-audit.md). - Cap input length before hashing. Reject absurdly long passwords (e.g. > 1 KiB) at the validation layer to avoid a denial-of-service where an attacker submits megabyte passwords to your slow hasher — see input-validation.md.
Real-World Example
Section titled “Real-World Example”A small, production-flavored auth module with a typed error enum. It models a users table row, hashes on registration, and verifies on login, returning a deliberately generic error so the response cannot distinguish “no such user” from “wrong password”.
cargo add argon2 --features stdcargo add thiserroruse argon2::{ password_hash::{ rand_core::OsRng, Error as PwHashError, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Argon2,};use thiserror::Error;
#[derive(Debug, Error)]enum AuthError { #[error("could not hash password")] Hash(#[source] PwHashError), #[error("stored credential is corrupt")] CorruptHash(#[source] PwHashError), #[error("invalid username or password")] BadCredentials,}
/// Stand-in for a `users` table row.struct UserRecord { username: String, password_hash: String, // the PHC string, safe to store in a DB column}
/// Hash a new user's password for storage.fn register(username: &str, password: &str) -> Result<UserRecord, AuthError> { let salt = SaltString::generate(&mut OsRng); let password_hash = Argon2::default() .hash_password(password.as_bytes(), &salt) .map_err(AuthError::Hash)? .to_string();
Ok(UserRecord { username: username.to_owned(), password_hash, })}
/// Check a login attempt against a stored record.fn verify_login(record: &UserRecord, attempt: &str) -> Result<(), AuthError> { // A malformed stored hash is a server fault, distinct from a bad password. let parsed = PasswordHash::new(&record.password_hash).map_err(AuthError::CorruptHash)?;
Argon2::default() .verify_password(attempt.as_bytes(), &parsed) // Collapse any verify failure into ONE generic error: never reveal which part failed. .map_err(|_| AuthError::BadCredentials)}
fn main() { let user = register("alice", "s3cr3t-p@ssw0rd").expect("register failed"); println!("stored for {}: {}", user.username, user.password_hash);
match verify_login(&user, "s3cr3t-p@ssw0rd") { Ok(()) => println!("login ok"), Err(e) => println!("login failed: {e}"), }
match verify_login(&user, "guess") { Ok(()) => println!("login ok"), Err(e) => println!("login failed: {e}"), }}Real output (PHC body redacted here for length; it is a full base64 salt+digest at runtime):
stored for alice: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<digest>login oklogin failed: invalid username or passwordIn a real service the UserRecord would come from a database — see ../17-database — and the register/verify_login calls would be wrapped in spawn_blocking inside your axum handlers (see ../16-web-apis). The generic BadCredentials error is intentional: returning the same message and status for unknown-user and wrong-password prevents username enumeration, and you should also keep the timing of both paths similar in production hardening (see ../28-production/README.md).
Note: This page presents
argon2 = "0.5",bcrypt = "0.19", andthiserror = "2". The current stable toolchain is Rust 1.96.0 on the 2024 edition, whichcargo newselects automatically. Always runcargo add <crate>to resolve the latest compatible versions rather than copying pins.
Further Reading
Section titled “Further Reading”argon2crate docs — the recommended password hasher.password-hashcrate docs — thePasswordHasher/PasswordVerifiertraits and the PHC string types shared across RustCrypto hashers.bcryptcrate docs — the bcrypt alternative.- OWASP Password Storage Cheat Sheet — current parameter recommendations.
- PHC string format spec — the structure of the stored hash string.
- Related sections of this guide:
- secure-randomness.md — where salts come from (
OsRng). - cryptography.md — general crypto; why password hashing is not encryption.
- secrets-management.md — handling secrets in memory.
- input-validation.md — capping password length before hashing.
- security-audit.md — keeping these crates patched.
- ../08-error-handling/README.md and ../09-generics-traits/README.md — the
Resultand trait mechanics used above.
- secure-randomness.md — where salts come from (
Exercises
Section titled “Exercises”Exercise 1: Round-trip a password
Section titled “Exercise 1: Round-trip a password”Difficulty: Easy
Objective: Get comfortable with the hash_password → store → verify_password cycle and confirm salting works.
Instructions: Write a program that hashes the password "hunter2" with Argon2::default() and a freshly generated salt, prints the PHC string, and then verifies both "hunter2" (should succeed) and "Hunter2" (should fail). As a final check, hash "hunter2" a second time and assert the two PHC strings are different. Fill in the /* ??? */ parts:
// The TypeScript you are translating:import argon2 from "argon2";const hash = await argon2.hash("hunter2");console.log(await argon2.verify(hash, "hunter2")); // trueconsole.log(await argon2.verify(hash, "Hunter2")); // falseSolution
// cargo add argon2 --features stduse argon2::{ password_hash::{ rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Argon2,};
fn main() { let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(b"hunter2", &salt) .unwrap() .to_string(); println!("{hash}");
let parsed = PasswordHash::new(&hash).unwrap(); println!( "hunter2 -> {}", Argon2::default().verify_password(b"hunter2", &parsed).is_ok() ); println!( "Hunter2 -> {}", Argon2::default().verify_password(b"Hunter2", &parsed).is_ok() );
let salt2 = SaltString::generate(&mut OsRng); let hash2 = Argon2::default() .hash_password(b"hunter2", &salt2) .unwrap() .to_string(); assert_ne!(hash, hash2, "random salts must produce different hashes"); println!("two hashes differ: {}", hash != hash2);}Running this prints a PHC string, then hunter2 -> true, Hunter2 -> false, and two hashes differ: true.
Exercise 2: A reusable, parameter-aware hasher
Section titled “Exercise 2: A reusable, parameter-aware hasher”Difficulty: Medium
Objective: Wrap Argon2 behind a small struct with explicit cost parameters and clean error handling.
Instructions: Define a Hasher struct that owns an Argon2<'static> configured with Params::new(19_456, 2, 1, None) and Algorithm::Argon2id. Give it two methods: hash(&self, password: &str) -> Result<String, argon2::password_hash::Error> and verify(&self, password: &str, stored: &str) -> Result<bool, argon2::password_hash::Error> (return Ok(true)/Ok(false) for match/mismatch, and propagate only structural errors such as a corrupt PHC string). Demonstrate it on "pa$$w0rd".
Solution
// cargo add argon2 --features stduse argon2::{ password_hash::{ rand_core::OsRng, Error, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, }, Algorithm, Argon2, Params, Version,};
struct Hasher { inner: Argon2<'static>,}
impl Hasher { fn new() -> Self { let params = Params::new(19_456, 2, 1, None).expect("valid params"); Hasher { inner: Argon2::new(Algorithm::Argon2id, Version::V0x13, params), } }
fn hash(&self, password: &str) -> Result<String, Error> { let salt = SaltString::generate(&mut OsRng); Ok(self .inner .hash_password(password.as_bytes(), &salt)? .to_string()) }
fn verify(&self, password: &str, stored: &str) -> Result<bool, Error> { // A bad PHC string is a real error; a wrong password is just `Ok(false)`. let parsed = PasswordHash::new(stored)?; match self.inner.verify_password(password.as_bytes(), &parsed) { Ok(()) => Ok(true), Err(Error::Password) => Ok(false), Err(e) => Err(e), } }}
fn main() -> Result<(), Error> { let hasher = Hasher::new(); let stored = hasher.hash("pa$$w0rd")?; println!("{stored}"); println!("right: {}", hasher.verify("pa$$w0rd", &stored)?); // true println!("wrong: {}", hasher.verify("nope", &stored)?); // false Ok(())}The key idea is distinguishing a wrong password (Error::Password → Ok(false), a normal control-flow outcome) from a structural failure (a corrupt stored hash → propagated Err). This mirrors how you would surface a 401 vs a 500 in a web service.
Exercise 3: Detect outdated hashes for transparent rehashing
Section titled “Exercise 3: Detect outdated hashes for transparent rehashing”Difficulty: Hard
Objective: Implement the “upgrade cost over time” best practice: after a successful login, decide whether a stored hash used weaker-than-current parameters and should be re-hashed.
Instructions: Write needs_rehash(stored: &str, current: &Params) -> bool that parses the stored PHC string, reads its embedded Argon2 Params, and returns true if the stored memory cost (m_cost) or iteration count (t_cost) is lower than current’s. Use argon2::Params::try_from(&PasswordHash) to recover the parameters. Test it with a hash made at m=8,t=1 against a current policy of m=19_456,t=2.
Hint:
Params::try_from(&password_hash)returns the parameters encoded in the PHC string. Compare.m_cost()and.t_cost().
Solution
// cargo add argon2 --features stduse argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString}, Algorithm, Argon2, Params, Version,};
fn needs_rehash(stored: &str, current: &Params) -> bool { let parsed = match PasswordHash::new(stored) { Ok(p) => p, Err(_) => return true, // unparseable -> force a rehash on next login }; match Params::try_from(&parsed) { Ok(used) => used.m_cost() < current.m_cost() || used.t_cost() < current.t_cost(), Err(_) => true, }}
fn main() { // An old, weak hash: 8 KiB memory, 1 iteration. let weak_params = Params::new(8, 1, 1, None).unwrap(); let weak = Argon2::new(Algorithm::Argon2id, Version::V0x13, weak_params) .hash_password(b"pw", &SaltString::generate(&mut OsRng)) .unwrap() .to_string();
// Current policy: 19 MiB memory, 2 iterations. let current = Params::new(19_456, 2, 1, None).unwrap();
println!("weak hash needs rehash: {}", needs_rehash(&weak, ¤t)); // true
// A hash made at current policy does NOT need a rehash. let strong = Argon2::new(Algorithm::Argon2id, Version::V0x13, current.clone()) .hash_password(b"pw", &SaltString::generate(&mut OsRng)) .unwrap() .to_string(); println!("strong hash needs rehash: {}", needs_rehash(&strong, ¤t)); // false}In a real login handler you would call needs_rehash after a successful verify_password, and if it returns true, re-hash the plaintext you just verified (with current parameters) and update the database row — upgrading every active user’s security without a forced password reset.