Password Hashing
One of the primary uses for one-way hash algorithms is to store user passwords securely.
Never Plain Text
Passwords should NEVER be stored in plain text. This is one of the worst security sins possible. A website compromises its own user accounts by storing passwords in plain text. Staff can easily read and use the passwords. They can be retained insecurely in old data backups or on discarded hardware. Attackers can steal them through techniques like SQL Injection. But plain text passwords also compromise the security of other websites because passwords are often re-used. It is common for stolen or leaked passwords from one site to be used to log in to other sites in the days and weeks that follow.
The Plain Text Offenders website attempts to shame this bad behavior by publishing the names of sites which are discovered to either store or email plain text passwords.
Hash, Don’t Encrypt
Passwords should be hashed, not encrypted. Encryption is a two-way function: anyone who holds the key can decrypt the stored value and recover the original password. Hashing is a one-way function—there is no decryption key and no decryption step, so a stored hash cannot be reversed to reveal the password: not by an attacker, not by staff, not even by administrators. If stolen, hashes provide the most security.
It might seem that a password needs to be recoverable so that it can be compared against a password submitted during login. It is not necessary because hash algorithms are deterministic: the same password, hashed with the same algorithm, the same salt, and the same cost parameters, always produces the same output. A website can hash a user’s original password and store the result—modern password hashes such as bcrypt and Argon2id store the salt and cost parameters alongside the hash itself. When a user logs in, the website recomputes the hash of the submitted password using the stored salt and parameters. If the two results match, then the password is correct.
A good choice for a password hashing algorithm will be cryptographically secure and strictly one-way, allowing no decryption. Calculation speed is not an important consideration, in fact, there are some reasons to favor a slow calculation.
Slower is Better
A slower algorithm provides a defense against rapid-fire password attempts in a Brute Force Attack. A user will not notice if they have to wait an extra 0.1 seconds during the login process. However, a Brute Force Attack needs to try billions of random strings to guess the correct password. An extra 0.1 seconds will significantly slow down their attempts. For every one billion attempts, an extra 0.1 second adds three additional years to the time it takes to crack one password!
Password Hashing Algorithms
Historically, the MD5 and SHA-1 algorithms have been popular choices for storing passwords. MD5 was popular from 1991-2004 until a number of collision vulnerabilities were discovered. SHA-1 was popular from 1996-2010, largely as a replacement for MD5. It is more secure than MD5 in part because it generates a larger hash (160-bit vs. 128-bit) which makes collisions much less likely. While not as vulnerable as MD5, some collision vulnerabilities have been discovered for SHA-1. Because there are better alternatives, these algorithms are not recommended for hashing passwords.
bcrypt is a popular and secure choice for hashing passwords. It is built from the Blowfish cipher, but bcrypt itself is a one-way hashing function, not reversible encryption. One of the distinguishing characteristics of bcrypt (and Blowfish) is that it is a relatively slow algorithm. This is a good trait for password hashing because it provides a defense against a Brute Force Attack.
Argon2id is the current OWASP-recommended algorithm for new systems. It won the 2015 Password Hashing Competition and is supported in PHP 7.3+ via password_hash($password, PASSWORD_ARGON2ID). bcrypt remains a solid fallback when Argon2id is unavailable.
Hashing using bcrypt
Example in PHP using the modern password_hash() function:
<?php
$password = $_POST['password']; // plain-text password from user input
$hashed_password = password_hash($password, PASSWORD_BCRYPT);
// Store $hashed_password in the database
?>
Verification using bcrypt
Example in PHP using password_verify():
<?php
$attempted_password = $_POST['password']; // plain-text password from login form
$hashed_password = /* hash retrieved from the database for this user */;
$is_match = password_verify($attempted_password, $hashed_password);
?>
bcrypt Cost Parameter
The cost parameter controls the key-expansion iteration count — in plain language, how many times the values are scrambled. A higher cost makes the algorithm slower, which is a good thing for password hashing because it slows brute-force attacks. It also provides a defense against the use of Rainbow Tables. The default cost in PHP 8.4+ is 12 (raised from 10). To set a custom cost:
<?php
$password = $_POST['password']; // plain-text password from user input
$hashed_password = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
?>
bcrypt Salts
bcrypt also uses Salts when hashing. Using a salt when hashing passwords is the best defense against the use of Rainbow Tables. password_hash() automatically generates a cryptographically secure random salt for every password — you do not need to supply one manually.