Best Practices for Storing Passwords
When storing user passwords, security is paramount. Poor password storage practices can lead to breaches where user data is compromised, leading to stolen accounts, identity theft, and other malicious activities. Here are the best practices for securely storing passwords:
1. Never Store Passwords in Plain Text
- Storing passwords as plain text is a significant security risk. If an attacker gains access to the database, they can immediately use the passwords to log in to users' accounts.
2. Use Strong Hashing Algorithms
- Hashing converts a password into a fixed-length string of characters. It is a one-way process, meaning the original password cannot be retrieved from the hash (without brute-forcing it). Always use cryptographically secure hashing algorithms.
Recommended Hashing Algorithms:
- bcrypt
- Argon2 (widely regarded as one of the most secure)
- PBKDF2 (Password-Based Key Derivation Function 2)
- scrypt
These algorithms are designed for password hashing because they are slow by design, making brute-force attacks much harder.
Example using bcrypt:
import bcrypt
# Hash a password
password = b"supersecretpassword"
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
# Verify a password
bcrypt.checkpw(password, hashed_password)
3. Use a Salt for Each Password
A salt is a random string of data added to a password before hashing it. The salt ensures that even if two users have the same password, their hashes will be different. This protects against rainbow table attacks, which use precomputed hash values to crack passwords.
- Unique salt per password: Each password should be salted with a unique value before hashing. This prevents attackers from identifying common passwords based on the same hash value.
- Many modern hashing algorithms (like bcrypt and Argon2) handle salting automatically, so you don’t need to manage it manually.
Example of Manual Salting with a Simple Hash:
import hashlib
import os
# Generate a random salt
salt = os.urandom(16)
# Combine password with salt and hash
password = b"supersecretpassword"
hashed_password = hashlib.pbkdf2_hmac('sha256', password, salt, 100000)
4. Use Adaptive Hashing Functions
Adaptive hashing functions like bcrypt, Argon2, and PBKDF2 allow you to adjust the hashing difficulty (through cost factors or iterations) as hardware improves. This helps maintain security in the long term, as attackers' hardware becomes more powerful.
- bcrypt uses a work factor that increases the number of iterations, making brute-force attacks slower.
- Argon2 and PBKDF2 similarly allow configuring the number of iterations or memory usage, making password cracking increasingly expensive.
5. Pepper Your Passwords
- A pepper is an additional secret value (stored outside the database) that is appended to the password before hashing. The pepper is stored separately from the password database, adding another layer of security.
- Even if an attacker gains access to the database, without the pepper (which could be stored in a configuration file or environment variable), it becomes much harder to crack passwords.
Example:
pepper = "randomPepperString"
# Combine password, salt, and pepper before hashing
password = "supersecretpassword"
salt = os.urandom(16)
peppered_password = password + pepper
hashed_password = hashlib.pbkdf2_hmac('sha256', peppered_password.encode(), salt, 100000)
6. Use Multi-Factor Authentication (MFA)
Even with securely stored passwords, a breach can still happen. Adding an extra layer of protection through multi-factor authentication (MFA) ensures that even if the password is compromised, attackers cannot log in without another factor (such as a code from an app or SMS).
7. Enforce Strong Password Policies
- Minimum length: Ensure passwords are at least 8-12 characters long.
- Complexity: Encourage users to use a mix of uppercase and lowercase letters, numbers, and special characters.
- Avoid common passwords: Use password blacklists to prevent the use of weak or commonly used passwords (e.g., “123456”, “password”).
8. Rate-Limit Login Attempts
To prevent brute-force attacks, implement rate-limiting or exponential backoff on failed login attempts. This makes it harder for attackers to guess passwords by trying many combinations.
9. Implement Secure Password Reset Mechanisms
- Ensure password reset links are one-time-use and have an expiration period.
- Token-based reset: Send a unique, secure token to the user’s email that they must use to reset their password.
- Never send passwords via email: Sending plaintext passwords in emails is highly insecure.
10. Regularly Update Your Hashing Algorithm
As computational power increases, weaker hashing algorithms can become vulnerable over time. It’s important to regularly review and update the hashing algorithm you're using. If transitioning from an old algorithm (e.g., MD5 or SHA-1) to a modern one (e.g., bcrypt or Argon2), you can update user passwords the next time they log in.
11. Store Password Hashes in Secure Environments
- Protect the database where password hashes are stored using strong access controls.
- Use encryption for the database or storage where the password hashes are located.
- Monitor and audit database access to detect unusual patterns or breaches.
12. Avoid Reversible Encryption for Password Storage
While it might be tempting to encrypt passwords instead of hashing them (allowing for recovery), this is a bad practice. Encryption is reversible, and if the encryption key is exposed, all user passwords could be compromised. Hashing, on the other hand, is irreversible, making it more secure.
Summary of Best Practices:
- Hash passwords with a strong algorithm like bcrypt, Argon2, or PBKDF2.
- Use unique salts to prevent identical passwords from having the same hash.
- Pepper passwords for an extra layer of security.
- Never store passwords in plain text or in reversible encryption.
- Enforce strong password policies and implement multi-factor authentication.
- Use secure password reset mechanisms and limit login attempts.