Hash Passwords With Argon2id, Not bcrypt-Just-Because
Securely storing user passwords is a fundamental requirement for any application or system handling user authentication. Incorrect password storage…
Securely storing user passwords is a fundamental requirement for any application or system handling user authentication. Incorrect password storage practices, such as storing plaintext passwords or using insecure hashing algorithms, pose significant security risks. This article details the recommended approach for password hashing using Argon2id, contrasting it with bcrypt, and provides practical implementation guidance.
The core principle is to make it computationally expensive for an attacker to test many passwords, even if they obtain the password hash database. This is achieved through slow hashing algorithms, sufficient work factors, and unique salt values for each password.
Understanding Password Hashing Fundamentals
A password hash function transforms a password into a fixed-size string of characters (the hash). Key properties for secure password hashing include:
- One-way: Irreversible; you cannot regenerate the original password from the hash.
- Deterministic: The same password always produces the same hash (when using the same salt and parameters).
- Collision Resistance: Extremely difficult to find two different passwords that produce the same hash.
- Slow/Costly: Designed to be computationally intensive to deter brute-force and dictionary attacks.
- Salt: A unique, random string added to each password before hashing. This prevents pre-computed rainbow table attacks and ensures identical passwords have different hashes.
- Work Factor/Cost Parameters: Configurable parameters (e.g., memory, iterations, parallelism) that control the computational expense of the hashing process. Higher values mean more security but also more processing time.
It's crucial to understand that general-purpose cryptographic hash functions like SHA-256 or MD5 are unsuitable for password hashing. They are designed for speed and collision resistance, not for resistance against brute-force attacks on low-entropy inputs like passwords. An attacker with a database of SHA-256 password hashes can quickly crack them using dedicated hardware (GPUs) and pre-computed tables.
Argon2id: The Modern Standard
Argon2id is a hybrid version of the Argon2 algorithm, winner of the Password Hashing Competition (PHC) in 2015. It is designed to resist both GPU-based brute-force attacks and side-channel attacks. Argon2id combines the memory-hardness of Argon2d with the time-hardness of Argon2i, offering robust protection.
Argon2id Parameters and Recommendations
Argon2id uses three primary work factors:
- Memory Cost (
m): Specifies the amount of memory (in KiB) Argon2id will use. Higher memory usage makes it harder for attackers to use dedicated hardware efficiently. - Time Cost (
t): Specifies the number of iterations or passes over the memory. Higher values increase the computational time. - Parallelism Factor (
p): Specifies the number of parallel threads or lanes Argon2id can use. This helps utilize multi-core processors but provides diminishing returns beyond a few threads.
OWASP's current recommendations (as of late 2023/early 2024, subject to change) for Argon2id parameters are:
- Memory: 19 MiB (
m=19456KiB) - Iterations (Time Cost): 2 (
t=2) - Parallelism: 1 (
p=1)
These values are a starting point. For systems with higher security requirements or available computational resources, increasing memory (e.g., to 64 MiB or 128 MiB) and iterations (e.g., to 3 or 4) is beneficial. Parallelism can be increased if the server has multiple cores and the hashing operation needs to be faster on legitimate login attempts, but p=1 is often sufficient for security.
It is critical to regularly review and adjust these parameters as computing power increases. A good practice is to benchmark the hashing time on your production hardware and aim for a verification time between 0.5 and 1.0 seconds for an interactive login, or longer for non-interactive processes.
Example Python Implementation (using passlib library)
from passlib.hash import argon2
# Generate a password hash
password_plaintext = "MySup3rS3cur3P@ssw0rd!"
hashed_password = argon2.using(
time_cost=2,
memory_cost=19456, # 19 MiB = 19 * 1024 KiB
parallelism=1,
salt_size=16, # Recommended salt size (128 bits)
digest_size=32 # Recommended hash output size (256 bits)
).hash(password_plaintext)
print(f"Hashed Password: {hashed_password}")
# Example output: $argon2id$v=19$m=19456,t=2,p=1$c2FsdHNraXBibGFibGE$N4fR3s...
# Verify a password
password_to_check = "MySup3rS3cur3P@ssw0rd!"
try:
argon2.verify(password_to_check, hashed_password)
print("Password verified successfully.")
except ValueError:
print("Invalid password.")
# Verifying with incorrect password
incorrect_password = "WrongPassword!"
try:
argon2.verify(incorrect_password, hashed_password)
except ValueError:
print("Invalid password (as expected).")
The passlib library automatically extracts the salt and parameters from the hash string, making verification straightforward.
Bcrypt: The Previous Default
Bcrypt, introduced in 1999, was the de-facto standard for password hashing for many years, especially before Argon2's widespread adoption. It's built upon the Blowfish cipher and is designed to be slow and memory-intensive, making it resistant to brute-force attacks. Bcrypt's primary tunable parameter is the cost factor (or work factor).
Bcrypt Parameters and Trade-offs
The bcrypt cost factor is a logarithmic value (base 2) that determines the number of iterations. A cost factor of 10 means 210 (1024) iterations. Increasing the cost factor by 1 doubles the computational work.
OWASP recommended bcrypt cost factors have evolved over time:
- Earlier (e.g., ~2015): 10-12
- More Recent (e.g., ~2020): 12-14
Currently, a cost factor of 12-14 is generally considered a reasonable minimum for new deployments if Argon2id is not an option. This should result in a hashing time of 250ms to 1000ms on typical server hardware. Just like Argon2id, this should be benchmarked and adjusted.
Why Argon2id over Bcrypt?
While bcrypt is still acceptable, Argon2id offers superior resistance against modern attacks, particularly:
- GPU Resistance: Argon2id's memory-hardness makes it much more resistant to GPU-based attacks than bcrypt. GPUs are highly efficient at the kind of operations bcrypt performs, but less so for memory-intensive operations.
- Dedicated Hardware (ASIC/FPGA) Resistance: Argon2id is more difficult to optimize for custom hardware, further increasing an attacker's cost.
- Flexibility: Argon2id's three distinct parameters (memory, time, parallelism) offer finer-grained control over the work function, allowing better adaptation to different threat models and hardware environments.
Recommendation: If you are building a new system or have the flexibility to update your dependencies, choose Argon2id. If you are constrained by legacy systems or very restrictive environments where adding a new cryptography library is impractical, bcrypt with a high-enough cost factor (12-14) remains a viable, though less optimal, alternative.
Storing and Verifying Hashes
Regardless of the algorithm chosen, the storage and verification process follows a similar pattern:
- Password Input: User enters a plaintext password.
- Hashing (on registration/password change):
- Generate a unique, cryptographically secure random salt for this password.
- Hash the plaintext password using the chosen algorithm (Argon2id or bcrypt) with the salt and appropriate work factors.
- Store the full hash output (which includes the algorithm, parameters, and salt embedded within it) in your database. Never store the salt separately.
- Verification (on login):
- Retrieve the stored hash from the database for the given username.
- Hash the plaintext password provided by the user using the algorithm, salt, and parameters extracted directly from the stored hash.
- Compare this newly computed hash with the stored hash using a constant-time comparison function (most libraries do this automatically). If they match, the password is correct.
The standard output format for Argon2id (and bcrypt) is a self-contained string that includes all necessary information:
# Argon2id example format:
$argon2id$v=19$m=19456,t=2,p=1$c2FsdHNraXBibGFibGE$N4fR3s...
# Components:
# $argon2id - Algorithm identifier
# $v=19 - Argon2 version (0x13)
# $m=19456,t=2,p=1 - Memory cost, time cost, parallelism
# $c2FsdHNraXBibGFibGE - Base64 encoded salt
# $N4fR3s... - Base64 encoded hash digest
# Bcrypt example format:
$2b$12$Ea2qj92e9F0ZkMvR5p1GWe.mE0gS/qB8B/aJ.j/L3.m1.L3.L9.
# Components:
# $2b - Algorithm identifier (bcrypt, variant 'b')
# $12 - Cost factor (2^12 iterations)
# $Ea2qj92e9F0ZkMvR5p1GWe - Salt (22 characters)
# .mE0gS/qB8B/aJ.j/L3.m1.L3.L9. - Hash digest (31 characters)
This self-contained format simplifies implementation as all necessary parameters for verification are present in the stored hash itself.
Common Pitfalls and Troubleshooting
- Using the wrong algorithm: Avoid MD5, SHA-1, SHA-256/512 (without slow key derivation functions), PBKDF2 with too few iterations, or Scrypt with insufficient parameters. These are not memory-hard enough or are too fast for password hashing.
- Insufficient work factors: Default work factors in libraries might be too low. Always benchmark and configure them appropriately for your environment. Update them periodically.
- Reusing salts or not salting: Each password *must* have a unique, randomly generated salt. Failing to do so makes rainbow table attacks trivial. Modern libraries handle this automatically.
- Truncating the hash: Storing only a portion of the hash output can significantly weaken its security. Store the entire output string from the hashing function.
- Improper storage: Never store plaintext passwords or reversibly encrypted passwords. If your database is compromised, all accounts are compromised.
- Lack of entropy for salts: Ensure your salt generation uses a cryptographically secure pseudo-random number generator (CSPRNG), not a simple
rand()function. Libraries likepassliborgolang.org/x/crypto/argon2handle this correctly. - Not checking library updates: Keep your cryptographic libraries updated to benefit from security fixes and performance improvements.
- Benchmarking on dev machines: Benchmark your work factors on production or production-representative hardware, as performance characteristics can vary significantly.