Every so often, I come across more and more clever schemes implemented by programmers wanting to one-up on what’s already available in their favorite software libraries. My response is DON’T DO IT! For one very simple reason: Crypto is hard. Good crypto is harder. PHP developers in particular are notorious for this, but I’ve seen C# devs do the same.
Even if you come up with a clever hashing scheme of your own, you don’t really want to play around next to something that’s tried and proven, like say bcrypt. Plus there are innumerable instances where very, very smart people have gone out and created their own schemes, only to be defeated by an attack a short while later. It happens. That’s reality.
Consider password hashing
You shouldn’t use anything other than bcrypt. There I said it. There are many examples on the web on how to use bcrypt, but just in case you’re wondering, here are couple of functions :
/** * Hash password with blowfish + salt * * @param string $pass Raw password * @param string $salt Stored salt stored in the db or generated */ public function password( $pass, $salt ) { if ( empty( $pass ) || empty( $salt ) ) { return false; } return crypt( $pass, '$2y$09$'. $salt . '$' ); } /** * Verify password hash against the stored value * * @param string $pass Raw password * @param string $salt Password salt as stored in the database * @param string $storedPass Hashed password to match against */ public function verifyPassword( $pass, $salt, $storedPass ) { if ( empty( $pass ) || empty( $salt ) || empty( $storedPass ) ) { return false; } $pass = $this->password( $pass, $salt ); if ( $storedPass === $pass ) { return true; } return false; }
These are very simple and are fairly self-explanatory. You’re seeing the PHP “crypt” function being used here to create a blowfish hash with a cost of 9 (this is the work factor used to calculate the hash; the higher it is, the longer it takes to hash, thus more expensive to brute-force). The verifyPassword function does the hash again with the salt (even though bcrypt has built in salting, I prefer adding a randomly generated one of my own for more entropy) to see if it matches the stored hash.
Now the DIY version
Simply re-hashing a bunch of times isn’t usually good enough, especially if it’s just md5. I’ve lost count of how many times I’ve seen that. There are well known software packages out there that still insist on putting md5 in a loop 1000x or more, but realistically, these are fairly easy to crack these days with the proliferation of fast GPUs.
If for some reason, you still want to do your own hashing (as mentioned above, I don’t recommend it), I’ve written an alternative you can use :
/** * DIY hash, best to use this for something non-critical */ public function saltedHash( $str, $salt = NULL ) { if ( NULL === $salt ) { $salt = bin2hex( $this->IV( 30 ) ); } for ( $i = 0; $i < 5000; $i++ ) { $str = hash( 'ripemd320', $str ); $arr = str_split( $str, 8 ); $j = $i; foreach( $arr as &$a ) { $salt = strrev( $salt ) . $j; $str = hash( 'ripemd160', strrev( $a ) . $salt ); $j++; } } return $salt . 'x' . $str ; } /** * Verify DIY hash */ public function matchSaltedHash( $stored, $sent ) { if ( empty( $sent ) || empty( $stored ) ) { return false; } $pkg = explode( 'x', $stored ); if ( 1 <= count( $pkg ) ) { return false; } if( $pkg[1] === $this->saltedHash( $sent, $pkg[0] ) ) { return true; } return false; } /** * @returns mixed Binary IV */ public function IV( $size ) { return mcrypt_create_iv( $size, MCRYPT_DEV_URANDOM ); }
This function is expensive enough to be not a simple re-iteration that can create hashes which are easily cracked on a modern GPU in short order. Because the hash is also salted, it should be a bit more difficult to simply run reiterations. It won’t make it uncrackable, but certainly more secure than simply running md5 a thousand times.
If you’re interested in a class for cryptographic purposes (encryption/decryption, password hashing/generation etc…) I’ve created a complete example. Note: I’m using a defined constant called SALT_KEY, which is basically an application specific random string. You can create your one of your own via the WordPress salt generator (do NOT share the same salt among multiple applications).
I’ve already published much of this code previously, but this version includes improvements.
<?php /** * Encryption/Decryption, Encoding/Decoding and password related functions. * * The license below was inherited from the project this originally went into. * If you need a different license for some reason, just email me. * * @author Eksith Rodrigo <reksith at gmail.com> * @license http://opensource.org/licenses/ISC ISC License */ class Crypto { /**#@+ * Instance getter, constructor and destructor */ private static $_instance; public static function getInstance() { if ( !self::$_instance instanceof self ) { self::$_instance = new self(); } return self::$_instance; } private function __construct() {} /**#@-*/ /**#@+ * Password related functions */ /** * Hash password with blowfish + salt * * @param string $pass Raw password * @param string $decodedSalt Stored salt after being decoded or freshly generated */ public function password( $pass, $decodedSalt ) { // This will reject NULL, 0, FALSE etc... but these are terrible passwords anyway if ( empty( $pass ) || empty( $decodedSalt ) ) { return false; } return crypt( $pass, '$2y$09$'. $decodedSalt . '$' ); } /** * Verify password hash against the stored value * * @param string $pass Raw password * @param string $encodedSalt Password salt as stored in the database * @param string $storedPass Hashed password to match against */ public function verifyPassword( $pass, $encodedSalt, $storedPass ) { if ( empty( $pass ) || empty( $encodedSalt ) || empty( $storedPass ) ) { return false; } $salt = $this->decode( $encodedSalt, SALT_KEY ); $pass = $this->password( $pass, $salt ); if ( $storedPass === $pass ) { return true; } return false; } /** * Hashes the passed password and creates a matching salt (also encoded) * This function does NOT create a new password. * * @param string $pass User provided password * @param string $salt Generated salt (encoded using the password) */ public function pairPasswordSalt( &$pass, &$salt ) { if ( empty( $pass ) ) { return false; } $salt = bin2hex( $this->IV( 30 ) ); $pass = $this->password( $pass, $salt ); $salt = $this->encode( $salt, SALT_KEY ); return true; } /** * Random password generator * * @param int $len Password length * @return string Random password with commonly confused characters removed */ public function randomPassword( $len ) { // Extra padding to accomodate removed chars $r = $this->randomStr( $len + 15 ); // Remove confusing chars $r = str_replace( array( '1', 'i', 'l', '0', 'o', '5', 's' ), '', $r ); return substr( $r, 0, $len ); } /**#@-*/ /**#@+ * Random number/string generators */ /** * Generates a random string of a given length (or random between 5 and 10 if length is empty) * * @param int $len Random string length. If empty, will revert to a number between 10 - 20 * @return string Alphanumeric string */ public function randomStr( $len = 0 ) { if ( empty( $len ) ) { $len = $this->_rnd( 10, 20 ); } $r = base64_encode( $this->IV( 30 ) ); $r = str_replace( array( '+', '/', '-', '_', '=' ), '', $r ); if ( strlen( $r ) > $len ) { return substr( $r, 0, $len ); } return $r; } /** * Generates a random number * * @param int $min Minimum number size. Defaults to 1 * @param int $max Maximum number size. Defaults to maximum allowed in 32 bit PHP */ public function randomInt( $min = 1, $max = 0x7FFFFFFF ) { $d = $max - $min; if ( $d < 0 || $d > 0x7FFFFFFF ) { $d = 1; } $r = $this->IV( 4 ); $p = unpack( "Nint", $r ); $f = ( float ) ( $p['int'] & 0x7FFFFFFF ) / 2147483647.0; return round( $f * $d ) + $min; } /**#@-*/ /** * Encrypt and serialize data with a given key * * @param mixed $data Data to encrypt (assumes strings are UTF-8) * @param string $key Encryption key * @return string Serialized and encrypted */ public function cryptSerialize( $data, $key ) { if ( empty( $data ) ) { return ''; } else { $data = json_encode( $data ); return $this->encryption( $data, $key, 'encrypt' ); } } /** * Deserialize and decrypt data * This is safer than PHP's 'deserialize' especially with cookie data * * @param string $data Data to decrypt (this should be completely unaltered) * @param string $key Decryption key * @return mixed Orignally serialized object */ public function cryptDeserialize( $data, $key ) { if ( empty( $data ) ) { return ''; } else { $data = $this->encryption( $data, $key, 'decrypt' ); if ( empty( $data ) ) { return ''; } else { try { return json_decode( $data ); } catch ( Exception $e ) { return ''; } } } } /** * Encode ( so called because this is only ECB ) in Rijndael 256 with Base64 encoding * * @param string $data The content to be encoded * @param string $salt Required nonce * @return string Encoded and optionally base64 encoded string */ public function encode( $data, $salt ) { $enc = mcrypt_encrypt( MCRYPT_RIJNDAEL_256, $salt, $data, MCRYPT_MODE_ECB, '' ); return base64_encode( $enc ); } /** * Decode as encoded above * * @param string $data The content to be decoded * @param string $salt Required matching nonce to the encrypted data * @return string Decoded content */ public function decode( $data, $salt ) { $data = base64_decode( $data ); $dec = mcrypt_decrypt( MCRYPT_RIJNDAEL_256, $salt, $data, MCRYPT_MODE_ECB, '' ); // Padding is automatic, but unpad is not return $this->_unpad( $dec, 32 ); } /** * Generates a random IV for encryption and password/string generation * * @returns mixed Binary IV */ public function IV( $size ) { return mcrypt_create_iv( $size, MCRYPT_DEV_URANDOM ); } /** * Decode as encoded above in Rijndael 256 with optional Base64 decoding * * @param string $data The content to be decoded. * @return string Decoded content */ public function encryption( $str, $key, $mode = 'encrypt', $time = null ) { // Open Rijndael-256 in CBC mode $td = mcrypt_module_open('rijndael-256', '', 'cbc', ''); // Find the IV size for the mcrypt module $size = mcrypt_enc_get_iv_size( $td ); // Block sizes are needed to calculate the padding ( further down ) $bsize = mcrypt_enc_get_block_size( $td ); // Key size $ksize = mcrypt_enc_get_key_size( $td ); // Hashed key ( for consistency ) $hkey = hash( 'ripemd160', $key ); // Hash the key for consistency and use only the key size needed from the front $key = substr( $hkey, 0, $ksize ); // Salt for encoding/decoding is extracted from the back of the key hashed again $salt = substr( hash( 'ripemd160', $hkey ), ( -1 * $ksize ) ); if ( $mode == 'encrypt' ) { /// We're encrypting. Create a new initialization vector $iv = $this->IV( $size ); } else { // Unwrap the combined IV and data packet and extract the IV from the front (using IV size) $str = $this->decode( $str, $salt ); $iv = mb_substr( $str, 0, $size ); // Isolate the data by removing the IV altogether and decode in preparation for decryption $str = mb_substr( $str, mb_strlen( $iv ) ); $str = base64_decode( $str ); } // Initialize mcrypt mcrypt_generic_init( $td, $key, $iv ); if ( $mode == 'encrypt' ) { // Prepare string by padding to match block size and encrypt $str = $this->_pad( $str, $bsize ); $enc = mcrypt_generic( $td, $str ); // Add the IV to the front and Encode $out = $this->encode( $iv . base64_encode( $enc ), $salt ); } else { // Decrypt the data ( we removed the IV and decoded above ) and remove padding $str = mdecrypt_generic( $td, $str ); $out = $this->_unpad( $str, $bsize ); } // Clean up mcrypt_generic_deinit( $td ); mcrypt_module_close( $td ); // Return encrypted/decrypted string return $out; } /**#@+ * Helpers */ /** * DIY hash, best to use this for something non-critical */ public function saltedHash( $str, $salt = NULL ) { if ( NULL === $salt ) { $salt = bin2hex( $this->IV( 30 ) ); } for ( $i = 0; $i < 5000; $i++ ) { $str = hash( 'ripemd320', $str ); $arr = str_split( $str, 8 ); $j = $i; foreach( $arr as &$a ) { $salt = strrev( $salt ) . $j; $str = hash( 'ripemd160', strrev( $a ) . $salt ); $j++; } } return $salt . 'x' . $str ; } /** * Verify DIY hash */ public function matchSaltedHash( $stored, $sent ) { if ( empty( $sent ) || empty( $stored ) ) { return false; } $pkg = explode( 'x', $stored ); if ( 1 <= count( $pkg ) ) { return false; } if( $pkg[1] === $this->saltedHash( $sent, $pkg[0] ) ) { return true; } return false; } /** * Workaround for mt_rand abnormalities */ public function _rnd( $min, $max ) { $r = 0; if ( $min > $max ) { $min ^= $max; $max ^= $min; $min ^= $max; } while( 0 === $r || $r < $min || $r > $max ) { $r = mt_rand( $min, $max ); } return $r; } /** * Pad data to encryption block size. * Michael Corleone says hello. */ private function _pad( $str, $bsize ) { // Find the pad size for this block size and string length $pad = $bsize - ( mb_strlen( $str ) % $bsize ); // Repeat the equivalent character up to the pad size $str .= str_repeat( chr( $pad ), $pad ); return $str; } /** * Remove extra padding added during encryption. * This is a bit of a hack, so if you have improvements, please add them [ and let me know :) ]. * Thanks! */ private function _unpad( $str, $bsize ) { $len = mb_strlen( $str ); // Find the pad character ( last one ) $pad = ord( $str[$len - 1] ); // If padding would have been applied to the string... if ($pad && $pad < $bsize) { // ...find the pad $pm = preg_match( '/' . chr( $pad ) . '{' . $pad . '}$/', $str ); // Pad found, strip it. if( $pm ) { return mb_substr($str, 0, $len - $pad); } } return $str; } /**#@-*/ }