Updated 18 days ago | GitHub

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.


Use One-way Encryption

One-way encryption algorithms are better for passwords than two-way encryption algorithms. They cannot be decrypted—not by an attacker, not by staff, not even by administrators. If stolen, they provide the most security.

It might seem that a password needs to be decrypted so that it can be compared against a password submitted during login. It is not necessary because one-way encryption algorithms operate on the principle that the same input into the same hash algorithm returns the same output. A website can hash a user’s original password and store the result. When a user logs in, the website can hash the new password with the same algorithm. If the two results match, then the password is correct.

A good choice for a password hashing algorithm will be cryptographically secure (i.e. unbreakable) and use one-way encryption, 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 based on Blowfish, but because it performs one-way encryption, it is more secure. 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.