Password hashing
Introduction
One of the most widely used password hashing methods is MD5 (using md5("password")). Although, It’s been discovered that MD5-sum hashing is insecure because of the fairly simple method of finding collisions (as described by Wang et al. in 2004) and recently discovery that MD5-sum hashing brings insecurity to https sites (as described by Sotirov et al. in 2007).
This MD5 insecurity made me want to create a password hashing system that uses a modern hash algorithm and a modern way of thinking. So I sat down and began to read about internet security and I came up with 3 significant improvements compared to the md5("password") method:
- SHA-256 hashing, instead of MD5.
- Adding a random ’salt’ to the password.
- Adding a fixed ’salt’ to the password
Salts
A salt is a string that is appended to the password to make the more difficult to brute force the hash. Adding a salt also makes the use of rainbow tables impossible.
When appending a random salt to the password we ensure that two equal passwords is completely different in the database, whereby a potential hacker can’t brute force more than one user at a time – he has to brute force every password by itself.
When adding a fixed salt we add a significantly long string to the password, making the hash even more difficult and time demanding to brute force because:
- It’s highly unlikely that a hacker grants access to both the database (containing the hash) and the php-file (containing the fixed salt)
- Even if a hacker finds out fixed salt string; it will take more computer power to brute force and thereby more time.
So how do we do this is practice? First of all we have to ensure to store the random hash, so we’re able to compare a user input password with the hashed one in the database. This can be done like this (this example will be a simplified version of the final outcome):
1 2 3 4 5 | <?php $RandomSalt = substr((sha1(uniqid(rand(),true))), 0, 12); $hash = hash(sha256, $RandomSalt . $plain_text_password); $final_hash = $RandomSalt . $hash; ?> |
substr cut the string from the second parameter to the third parameter; in this case it takes characters 0 to 12 in our generated string.
We generate the random string by taking the SHA-1 hash of a 23 character long unique ID created from the build-in PHP-function uniqid with the second parameter set to true to give us more entropy.
In the last step we append the random salt to the final hash value. This way we can retrieve it for comparison.
Now we want to append a fixed salt to the password hash as well:
1 2 3 4 5 6 | <?php define('FIXED_SALT', '@ywaE~H*JSA}7w2I||t%E%ywb}<]Y-I='); $hash = hash(sha256, $RandomSalt . $plain_text_password . FIXED_SALT); $final_hash = $RandomSalt . $hash; ?> |
The fixed salt in this example is 32 characters long but could be any length, although the longer the salt is, the more time demanding it will be to brute force the hash.
I’ve created a function to randomly create a fixed salt with variable length. In the example below will output a 32 character long random string:
1 2 3 4 5 6 7 8 9 10 11 12 | function generateRandSalt($salt_length){ $string = "define('FIXED_SALT', '"; for($x=0;$x<$salt_length;$x++){ $rnd_nr = rand(33,126); while($rnd_nr == 34 || $rnd_nr == 39) $rnd_nr = rand(33,126); $string .= htmlentities(chr($rnd_nr)) ; } $string .= "');"; return $string; } echo generateRandSalt(32) . "<br/>"; |
Hash functions
This example uses the hash function SHA-256, which creates a 64 character long hash string. If you want to save space in your database you can use the SHA-1 hash function instead. You should always use a known hashing system instead of trying to make your own, trying to fool a potential hacker. “Security through obscurity” is a widely used term in this industry – and is widely advised against. The term covers the type of security that is based upon making something weird or obscure. This might seem like a good protection, but if you’re not a mathematical genius it’s often easy to analyse how you created your security, and then it’s normally already broken.
Approximate lifetime left for some known hashing algorithms:
MD5 is completely dead.
SHA-1 is expected to die within 1-3 years.
SHA-256 is expected to have a lifetime of more than 20 years.
The code
The final code is presented below. It’s advised that you change the random salt length and you don’t use the same fixed salt as in the example below.
Remember: the longer you make the random salt, the more space your hash will take up in your database, since the random salt is appended to the hash.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?php define('SALT_LENGTH', 24); define('FIXED_SALT', 'q*x<D:_I7c:IG~O]B5Tv&}-V|DOM(~@z'); function generateHash($plainText, $salt = null){ // The salt variable is set to NULL if no parameter is set. if ($salt === null){ // triple equal signs means "defined as" and is used instead of the comparison (double equal signs) for the instance that the given salt is either zero (0) or an empty string (""). // Generates a random string with the defined salt length. // Two random functions are used to increase the number of possible outcomes. $salt = substr(hash(sha256,(uniqid(rand(), true)) . (uniqid(rand(), true))), 0, SALT_LENGTH); } else{ // Uses the supplied string. If the string is too long, it will be shortened to the defined salt length. $salt = substr($salt, 0, SALT_LENGTH); } // The hash is produced in a matter, so the random salt is appended to the password and the static salt is appended. The final hash is produced by using the SHA-256 algorithm. $hash = hash(sha256,$salt . $plainText . SECURE_SALT); return $salt . $hash; // Returns the hashed string with the random salt appended to it } $hash = generateHash("test"); // Calls the generateHash function and stores it in the $hash variable. $deHash = generateHash("test",$hash); // hashes again using the same random hash as in $hash. echo $hash . "<br/>" . $deHash; ?> |
To generate a password you call the function like this:
generateHash($plain_text_password);
To compare an input password with the one in the database use:
if(generateHash($input_password,$database_password) == $database_password){ // input password is correct } else{ // input password is incorrect. }
NOTE: No matter which way you hash your passwords it will always be possible to brute force the hash. The measures described in this guide are merely to give a potential hacker a great disadvantage in his brute force attempt. Even with a cluster of 1000 computers it would probably take a month to brute force a single hash.
Furthermore it’s strongly advised to demand a certain password strength from the user. This way it’s much more difficult for a potential hacker to guess the password on the login form. Also, it’s advised that you only allow a certain login attempts from the same computer in a specified timeframe. For instance 5 login attempt per hour.
The source file is available for download: Click here to download the code
Further Improvements
To make the hash even more time demanding to brute force, you could loop the hashing several times. This practice improves the code in two ways:
- A potential hacker doesn’t know how many times the hash function loops.
- If the hacker finds out how many times the hash loops, he still need to hash several times for each brute force attempt.
I’ve written an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | function generateHash($plainText, $salt = null){ if ($salt === null){ $salt = substr(hash(sha256,(uniqid(rand(), true)) . (uniqid(rand(), true))), 0, SALT_LENGTH); } else{ $salt = substr($salt, 0, SALT_LENGTH); } $hash = hash(sha256,$salt . $plainText . SECURE_SALT); for($x=0;$x < 4500;$x++){ $hash = hash(sha256,$salt . $hash . SECURE_SALT); } return $salt . $hash; } |
NOTE: this solution can be very server power demanding, for which reason it shouldn’t be used, at least not with 4500 loops, if your site has many login attempts during short timeframes.
Hi
I really like your function generating random salted hash that still works
http://guides.ricehigh.dk/files/password.php
I just wonder what SECURE_SALT constant does as it is not defined anywhere ? (bug ?)
I don’t know but I think this solution was presented long time before:
http://phpsec.org/articles/2005/password-hashing.html
And what about :
https://gist.github.com/154619/dfcf313301a88f1feef64470b3248e275413c20e ?
Yes, it does seem that similar solutions has been proposed by others. Still, I hope that this guide has been useful for you.
The confusion of the unused constant SECURE_SALT, arises because I erroneously called the constant SECURE_SALT, instead of the FIXED_SALT, which is the constant we defined at the very beginning of the script.
The failure has been corrected in the downloadable file, and will be corrected throughout the article in the next couple of days.
I apologize for the confusion this may have caused.