The database leak scenario
You are building a login page. The user types a password. You save it to the database. A week later, a misconfigured cloud bucket exposes your entire user table. The hacker downloads the CSV. They see the password column. If you saved the password as plain text, the hacker logs in as every user instantly. Your application is compromised.
Password hashing stops this. Hashing turns the password into a fixed string of gibberish. You can go forward, never back. When the user logs in, you hash the input and compare the result. If the hashes match, the password is correct. The database only stores the hash. If the database leaks, the hacker gets gibberish. They cannot reverse the hash to find the password. They have to guess passwords and hash them until they find a match.
Argon2 makes guessing expensive. It is the current winner of the Password Hashing Competition. It is designed to be slow and hungry for memory. This forces attackers to burn electricity and time. Argon2 raises the cost of brute-force attacks so high that they become impractical.
Hashing is a one-way blender
Think of hashing like a blender. You put in fruit. You get smoothie. You cannot un-blend the smoothie to get the fruit back. Password hashing works the same way. You put in a password. You get a hash. You cannot recover the password from the hash.
Argon2 adds two twists to the blender. First, it adds salt. Salt is random data mixed into the password before hashing. Salt ensures that two identical passwords produce different hashes. This stops rainbow table attacks, where hackers precompute hashes for common passwords. With salt, the hacker must compute a new table for every user.
Second, Argon2 makes the blender slow and memory-intensive. The algorithm runs for a configurable amount of time and touches a large block of RAM. This is called memory hardness. GPUs and ASICs are great at parallel computation but have limited memory. By forcing the hash to use lots of RAM, Argon2 throttles the speed of specialized hardware. Attackers cannot run millions of guesses in parallel. They are forced to use expensive equipment and accept slow speeds.
Minimal working example
The argon2 crate handles the heavy lifting. You import the types, generate a salt, hash the password, and verify. The crate uses the PHC string format, which bundles the hash, salt, and parameters into a single string. You store this string in your database.
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
};
fn main() {
// Generate a random salt to prevent rainbow table attacks.
// SaltString::generate uses the system RNG by default.
let salt = SaltString::generate(&mut rand::thread_rng());
// Argon2::default() selects Argon2id with safe parameters for modern hardware.
// It targets a hash time of roughly 0.5 to 1 second.
let hasher = Argon2::default();
// Hash the password. The result is a PHC string containing the algorithm,
// version, parameters, salt, and hash.
let hash = hasher.hash_password(b"my_secure_password", &salt).unwrap().to_string();
println!("Stored hash: {}", hash);
// Verify by parsing the PHC string and checking the password against it.
// The parser extracts the salt and parameters automatically.
let parsed_hash = PasswordHash::new(&hash).unwrap();
let is_valid = hasher.verify_password(b"my_secure_password", &parsed_hash).is_ok();
println!("Password valid: {}", is_valid);
}
The hash_password call returns a PasswordHash struct. Calling .to_string() converts it to the PHC format. The output looks like $argon2id$v=19$m=65536,t=3,p=1$c29tZXNhbHQ$.... This string is self-contained. You do not need a separate column for the salt. You do not need to store the parameters. The string carries everything needed to verify the password later.
Convention aside: Use Argon2::default() for new projects. The defaults are tuned by security experts and updated as hardware improves. Only customize parameters if you have measured performance requirements.
Copy this pattern. Do not invent your own salt generation or parameter selection.
The PHC string holds the keys
The PHC string format is the standard for password hashing in Rust. It ensures interoperability and eliminates common bugs. The format uses $ as a delimiter. Each field has a specific meaning.
The algorithm identifier tells you which variant of Argon2 was used. The version number tracks changes to the algorithm. The parameters define memory, iterations, and parallelism. The salt follows. Finally, the hash itself appears at the end.
When you verify a password, you parse the string. The crate extracts the salt and parameters. It re-runs the hash with the candidate password. If the result matches, the password is correct. This design means you can upgrade parameters over time. If you change the default parameters, new hashes will use the new values. Old hashes still verify because the parameters are stored in the string. You never break existing users.
Store the PHC string in a text column. Do not split the string manually. Do not store the salt in a separate column. The string is the source of truth.
Trust the PHC string. It carries the context. Parse it, do not split it.
Parameters and the Argon2id default
Argon2 has three parameters that control security and performance. The m parameter sets the memory usage in kilobytes. The t parameter sets the number of iterations. The p parameter sets the degree of parallelism.
Memory hardness is the most important defense. The m parameter forces the algorithm to allocate a large memory block. Each hash attempt must touch this memory. GPUs have limited VRAM. By increasing m, you reduce the number of parallel threads a GPU can run. A setting of m=65536 uses 64MB of RAM per hash. A GPU with 16GB VRAM can only run 256 threads. This throttles the attack speed.
The t parameter adds CPU cost. More iterations mean more work. This helps against CPUs with large caches. The p parameter allows multi-threading. Most password hashing runs single-threaded, so p=1 is common.
Argon2 has three variants. Argon2d uses data-dependent memory access. It is fast but vulnerable to side-channel attacks. Argon2i uses data-independent memory access. It is resistant to side-channels but slower on GPUs. Argon2id mixes both. It uses data-dependent access early to resist GPU attacks, then switches to data-independent access to resist side-channels.
Argon2::default() uses Argon2id. This is the community consensus. Argon2id provides the best balance of security. It resists both GPU brute-force and side-channel attacks.
Convention aside: When you see Argon2::default(), you are getting Argon2id with safe parameters. When you see Argon2::new(Algorithm::Argon2i, ...), someone made a conscious choice. Default is almost always right.
Trust the defaults. Tune only if you measure.
Realistic implementation with lazy rehashing
Production code needs error handling and a strategy for upgrading parameters. Hardware improves over time. Parameters that were safe in 2020 might be weak in 2025. You need a way to strengthen hashes without forcing users to reset passwords.
The standard pattern is lazy rehashing. When a user logs in, verify the password. If the hash parameters are outdated, rehash the password with new parameters and save the new hash. The user feels nothing. Your database gets stronger over time.
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
};
use rand::rngs::OsRng;
/// Configuration for current security standards.
const CURRENT_PARAMS: argon2::Params = argon2::Params::default();
/// Hashes a password using Argon2 with secure defaults.
/// Uses OsRng for cryptographically secure salt generation.
fn hash_password(password: &[u8]) -> Result<String, argon2::Error> {
let salt = SaltString::generate(&mut OsRng);
let hasher = Argon2::default();
let hash = hasher.hash_password(password, &salt)?;
Ok(hash.to_string())
}
/// Verifies a password and returns true if valid.
/// Returns an error if the hash format is invalid.
fn verify_password(password: &[u8], hash: &str) -> Result<bool, argon2::Error> {
let parsed_hash = PasswordHash::new(hash)?;
let hasher = Argon2::default();
hasher.verify_password(password, &parsed_hash).map(|_| true)
}
/// Checks if the hash needs rehashing based on current parameters.
/// This is a simplified check. Real code might compare specific fields.
fn needs_rehash(parsed_hash: &PasswordHash) -> bool {
// Compare the stored parameters against the current standard.
// If they differ, rehash is needed.
parsed_hash.params != CURRENT_PARAMS
}
/// Lazy rehashing logic.
/// Verifies the password. If valid and outdated, rehashes and returns the new hash.
fn verify_and_rehash(
password: &[u8],
stored_hash: &str,
) -> Result<Option<String>, argon2::Error> {
let parsed_hash = PasswordHash::new(stored_hash)?;
// Verify the password first.
let hasher = Argon2::default();
if hasher.verify_password(password, &parsed_hash).is_err() {
return Ok(None);
}
// Password is valid. Check if parameters are outdated.
if needs_rehash(&parsed_hash) {
let new_hash = hash_password(password)?;
Ok(Some(new_hash))
} else {
Ok(None)
}
}
The verify_and_rehash function returns Some(new_hash) if the password is valid but needs upgrading. Your application can save the new hash to the database. The next login will use the stronger hash. This keeps your security current without user friction.
Use OsRng for salt generation in production. thread_rng is faster but OsRng is the standard for security-sensitive operations. The difference is negligible for password hashing.
Rehash on login. Your security improves while users sleep.
Pitfalls and compiler traps
Rust's type system catches many mistakes. Password hashing involves bytes and strings. Mixing them up causes compiler errors.
If you pass a String to hash_password, the compiler rejects you with E0308 (mismatched types). The function expects &[u8]. Call .as_bytes() on your string to convert it. Passwords are byte sequences. Unicode characters encode to multiple bytes. Always work with bytes.
Timing attacks are a subtle risk. If you compare hashes using ==, the comparison might leak information about the password. An attacker can measure the time taken to guess passwords. The verify_password function uses constant-time comparison. It takes the same amount of time regardless of where the mismatch occurs. Never write your own comparison logic.
Argon2 has a maximum password length. The crate enforces this limit. If a user enters a password that is too long, hash_password returns an error. Check the documentation for the current limit. Most applications do not hit this limit. If you do, truncate the password before hashing and document the behavior.
Convention aside: Use let _ = result to discard values you intentionally ignore. This signals to readers that you considered the value and chose to drop it. In password hashing, you rarely discard results, but the pattern applies elsewhere.
Check your types. Use the verifier. Respect the limits.
Decision matrix
Use Argon2 when you need the strongest available protection against GPU and ASIC attacks. Argon2id is the current winner of the Password Hashing Competition and resists side-channel attacks better than older algorithms.
Use bcrypt when you are maintaining legacy systems or have strict compatibility requirements with older services. Bcrypt is secure but lacks memory-hardness, making it slightly more vulnerable to specialized hardware attacks than Argon2.
Use scrypt when you need a memory-hard function but cannot use Argon2 due to licensing or ecosystem constraints. Scrypt is effective but older and less audited for side-channel resistance compared to Argon2.
Reach for plain hashing like SHA-256 only when you are hashing data that isn't a password, such as file integrity checks. Never use fast hashes for passwords. Fast hashes allow attackers to guess billions of passwords per second.
Argon2 is the default choice for new projects. Bcrypt is the fallback for compatibility.