salted and hashed passwords, using PHP and MySQLi


what is a salt?

In cybersecurity, a salt is an extra layer of protection to prevent attacks from rainbow tables. A rainbow table is a precomputed table for reversing hash functions. For instance, take an example database, consisting of usernames and hashed passwords (hashed with the weak and easily-cracked MD5 ) and with the cleartext password for demonstration purposes:

ID Username Hash Cleartext Password
1 administrator 5f4dcc3b5aa765d61d8327deb882cf99 password
2 john123 e10adc3949ba59abbe56e057f20f883e 123456
3 just_a_guy 5f4dcc3b5aa765d61d8327deb882cf99 password

Notice how administrator and just_a_guy have the same password hash? Now look at a sample rainbow table for MD5 :

Hash Cleartext
21232f297a57a5a743894a0e4a801fc3 admin
5f4dcc3b5aa765d61d8327deb882cf99 password
202cb962ac59075b964b07152d234b70 123
e10adc3949ba59abbe56e057f20f883e 123456

A rainbow table will look for similarities, and will replace the hashes with cleartext

how can we protect against this?

I’m glad you asked! Obviously, to improve our defense, we can do a few things. Namely, don’t use MD5 , use a secure hashing algorithm, like Argon2 . However, we can take this one step further. What if we add a random number to the hash as well? This is called a salt. It is not a secret number. It is often stored alongside the hash, or even in it’s own column. The point of a salt is to make it more difficult for a rainbow table attack to work. Since we add a salt to the hash, a rainbow table attack will no longer improve efficiency for the attacker (theoretically speaking, this increases the worst case scenario of a rainbow table attack from O(n) to O(n * m), where n = # of password guesses & m = # of database entries .

Consider our new and improved table, now with a salt appended to the end of the password before hashing (for this example, I’ll be using a 4 digit number off random.org ):

ID Username Hash Salt Cleartext Password
1 administrator 90f2caf75c216e424ac21de2b6a31a7b 9877 password
2 john123 3d2307bf44bd1da1d8bb401a208a8497 4667 123456
3 just_a_guy a5e3efc433bf190e54d50d012cd663c1 2432 password

Our hashes are completely different because of the salt, even with the same password! Now, if an attacker wants to use a rainbow table, they have to take in to account every single possible salt along with each password. In this example, with a salt of only 4 integers, a rainbow tables size would have to increase by around 1000 times! Imagine doing that for each password with a salt the same length of the hash (and with letters and symbols as well as numbers)!

writing it

I’ll be using PHP’s built-in function md5() for this because I don’t feel like implementing it myself.

For the encryption:

<?php
// connection to the server or die with the error
$connection = mysqli_connect('localhost', 'user', 'password', 'table') || die(mysqli_connect_error());
// get username and password (i'm assuming we're using POST)
$username = $_POST['username'];
$password = $_POST['password'];
// get a salt of 4 integers (should be random, i'm copying a function from openwall.com)
$salt = random_bits(4);
// get the md5 of the password and salt
$hash = md5($password . $salt);
// alternatively, you can use a version where the salt is stored along with the salted hash:
// $hash = $salt . md5($hash . $salt);

// now set up the SQL and execute
$sql_result = mysqli_query("INSERT INTO `users` (username, hash, salt) VALUES ('$username', '$hash', $salt)");
// for the alternative hash:
// $sql_result = mysqli_query("INSERT INTO `users` (username, hash) VALUES ('$username', '$hash')");


/**
 * generate pseudo random bits
 * copied from
 * http://www.openwall.com/phpass/
 */
function random_bits($entropy) {
    $entropy /= 8;
    $state = uniqid();
    $str = '';
    for ($i = 0; $i < $entropy; $i += 16) {
        $state = md5(microtime().$state);
        $str .= md5($state, true);
    }
    $str = unpack('H*', substr($str, 0, $entropy));
    // for some weird reason, on some machines 32 bits binary data comes out as 65! hex characters!?
    // so, added the substr
    return substr(str_pad($str[1], $entropy*2, '0'), 0, $entropy*2);
}
?>

And to decrypt:

<?php
// connection to the server or die with the error
$connection = mysqli_connect('localhost', 'user', 'password', 'table') || die(mysqli_connect_error());
// get username and password (i'm assuming we're using POST)
$username = $_POST['username'];
$password = $_POST['password'];
// get current hash in database
$sql_result = mysqli_query("SELECT hash, salt FROM `users` WHERE username='$username'";
// for alternative hash:
// $sql_result = mysqli_query("SELECT hash FROM `users` WHERE username='$username'";
$rows = mysqli_fetch_row($sql_result);

// now check against the salt
if (md5($password . $rows[1]) == $rows[0]) {
    // success!
} else {
    die("Incorrect username or password!");
}
// for the alternative hash:
/* $hash = substr($rows[0], 4);
   $salt = substr($rows[0], 0, 4));
   if ($salt . md5($hash . $salt) == $rows[0]) {
       // success
   } else {
       die("Incorrect username or password!");
   }
*/
?>


how should i use this on my website?

Don’t. Use PHP’s built-in functions password_hash() and password_verify() for password hashing. The hash generated by password_hash() contains the salt and algorithm used built in. As an added bonus, if you’re going to use your own implementation, or just not PHP, you can use a uniquely generated salt (stored in the database) along with a fixed salt stored in a file seperate from the database. This requires access to both the filesystem and the database.