For decades, the golden rule of password security was simple: never store passwords in plain text. However, as computing power has increased exponentially, the methods we used to protect those passwords have become obsolete. If you are still using MD5, SHA-1, or even unadorned SHA-256 to hash passwords in your application, your users' data is likely at risk. In this post, we will explore the evolution of password hashing, why brute-force and rainbow table attacks make legacy algorithms dangerous, and how to implement modern, resilient hashing strategies like Argon2 and bcrypt.
The Evolution of Hashing: From Digest to Derivation
It is crucial to distinguish between a cryptographic hash function and a key derivation function (KDF). General-purpose hash functions like SHA-256 are designed for speed and integrity verification. They are deterministic; the same input always yields the same output. While SHA-256 is computationally expensive for attackers compared to MD5, it is still too fast. A modern GPU can calculate billions of SHA-256 hashes per second, making brute-force attacks feasible even for long passwords.
Password hashing requires a KDF. These functions are specifically designed to be slow and resource-intensive, incorporating a salt and configurable parameters to resist hardware-accelerated attacks. The industry standard has shifted toward algorithms that are not just slow, but also memory-hard, making it difficult for attackers to use specialized hardware like ASICs or FPGAs.
Why Legacy Algorithms Fail
Let’s look at why older strategies fall short. MD5 is cryptographically broken and riddled with collision vulnerabilities. It is essentially useless for security. SHA-1 shares similar weaknesses and is deprecated by NIST. SHA-256, while still mathematically sound, lacks the built-in slowing mechanisms (like iterations or memory usage) required for password storage.
Another common misconception is that adding a "salt" to a fast hash like SHA-256 is sufficient. While salting prevents rainbow table attacks by ensuring that identical passwords have different hashes, it does not slow down the brute-force process. An attacker can still guess millions of passwords per second per GPU, even if every password is unique.
Recommended Strategies: bcrypt and Argon2
Currently, the two most recommended algorithms for password hashing are bcrypt and Argon2. bcrypt has been the industry standard for over a decade and is well-supported across all major languages. It uses the Blowfish cipher and allows you to set a cost factor (work factor) to determine the computational expense.
However, Argon2 is the winner of the Password Hashing Competition (PHC) and is increasingly becoming the new gold standard. Argon2 is memory-hard, meaning it requires a significant amount of memory to compute the hash. This feature neutralizes the advantage of parallelized hardware attacks. There are three variants: Argon2id (recommended for password hashing), Argon2i, and Argon2d.
Practical Example: Implementing Argon2 in Python
Implementing Argon2 is straightforward using the argon2-cffi library. Below is a practical example of how to hash a password and verify it securely.
import argon2
# Initialize the hasher with recommended default settings for Argon2id
ph = argon2.PasswordHasher(
time_cost=3,
memory_cost=65536,
parallelism=4,
hash_len=16,
salt_len=16
)
# Hashing a password
password = "user_secure_password_123"
hashed_password = ph.hash(password)
print(f"Hashed Password: {hashed_password}")
# Verifying a password
try:
is_valid = ph.verify(hashed_password, password)
print(f"Password is valid: {is_valid}")
except argon2.exceptions.VerifyMismatchError:
print("Invalid password")
In this example, time_cost represents the number of iterations, memory_cost is the memory used in KiB, and parallelism is the number of threads. You should tune these values based on your server's capacity to ensure the hashing takes roughly 0.5 seconds on your specific hardware, which is the recommended balance between user experience and security.
Best Practices Beyond the Algorithm
Choosing the right algorithm is only half the battle. Here are additional best practices to ensure robust security:
- Always Salt: Modern libraries like Argon2 handle salting automatically. Ensure you never roll your own salt generation.
- Re-hash on Login: If you upgrade your cost factors or switch algorithms, re-hash the password after a successful login. This ensures all passwords eventually migrate to the stronger settings.
- Rate Limiting: Implement rate limiting on your authentication endpoints to prevent online brute-force attacks.
- Regular Audits: Periodically review your security dependencies and adhere to OWASP guidelines for password storage.
Conclusion
Password hashing is not a "set it and forget it" task. As technology evolves, so do the methods attackers use to compromise user data. By moving away from fast, generic hash functions and adopting memory-hard algorithms like Argon2 or the battle-tested bcrypt, you significantly raise the barrier for entry for malicious actors. Remember, the goal of password hashing is not to make passwords unbreakable, but to make cracking them prohibitively expensive and time-consuming. Stay vigilant, keep your libraries updated, and prioritize user security in every layer of your application.