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
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)!
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!");
}
*/
?>
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.