@digital-ether
I agree with you 100%, although 100.000 iterations seem a bit excessive to me. (But that's just me :-P)
However, I got to ask why you specifically mention high memory usage?
I think she recommended high memory usage so that the effort needed to try to recreate/hack any of the passwords would be excessive and not worth it. But if you use up that much processing/memory, wouldn't you make it untenable to be used within a login/registering system? For even a reasonable amount of requests even.
/** * Generate cryptographic Hashes for passwords * * Features: * Harderned against precomputation attacks like rainbow tables (using salt) * Harderned against brute force and dictionary attacks (using key stretching, large memory usage and an optional secret key) * * http://en.wikipedia.org/wiki/Password_cracking * * Note: for PHP4 and lower, just remove the "public static" before function declaration * * @author gabe@fijiwebdesign.com * @link http://www.fijiwebdesign.com/ * @version $Id$ */ class Password_Hash { /** * Generate the Hash * @return String * @param $password String * @param $salt String[optional] * @param $iterations Int[optional] * @param $secret String[optional] */ public static function generate($password, $salt = null, $iterations = 10000, $hash_function = 'sha1', $secret = '') { $salt or $salt = self::generateToken(); $hashes = array(); $hash = $password; // stores a sequence of 10000 unique hashes in memory $i = $iterations; while(--$i) { $hash = $hashes[] = $hash_function($hash.$salt.$secret); } // hash the 10000 unique hashes into a single hash $hash = $hash_function(implode('', $hashes).$salt.$secret); return implode(':', array($hash, $iterations, $hash_function, $salt)); } /** * Verify a password meets a hash * @return Bool * @param $password String * @param $hash String * @param $secret String[optional] */ public static function verify($password, $hash, $secret = '') { list($_hash, $iterations, $hash_function, $salt) = explode(':', $hash); return ($hash == self::generate($password, $salt, $iterations, $hash_function, $secret)); } /** * Generate a random hex based token * @return String * @param $length Int[optional] */ public static function generateToken($length = 40) { $token = array(); for( $i = 0; $i < $length; ++$i ) { $token[] = dechex( mt_rand(0, 15) ); } return implode('', $token); } }
// generating the hash $password = 'test'; $hash = Password_Hash::generate($password); // verifying a password $result = Password_Hash::verify($password, $hash); // dump results var_dump($hash, $result);
// define our custom hash function function whirlpool($str) { return hash('whirlpool', $str); } $password = 'test'; $salt = password_Hash::generateToken(128); $secret = password_Hash::generateToken(128); $iterations = 10000; // generate hash $hash = Password_Hash::generate($password, $salt, $iterations, 'whirlpool', $secret); // verify $result = Password_Hash::verify($password, $hash, $secret); // dump results var_dump($result);
/** * Handles member security. * * Assumes a DB table named `member` structured like so: * CREATE TABLE `member` ( * `id` int Unsigned NOT NULL Auto_Increment Primary Key, * `name` VarChar(64) Not Null Unique, * `pw_salt` char(32) NOT NULL, * `pw_memory` int(10) unsigned DEFAULT 10485760, * `pw_algo` varchar(32) DEFAULT 'sha512', * `password` char(128) NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * Static Methods: * generate_hash - Generates a has based on the input and the user's pw_ details. * validate_password - Validates the given password against the member password. * */ class MemberSecurity { /** * Generates a hash based on the user-specific salt and the passed data. * @param string $input The keyword (password) to hash. * @param uint $userID The ID of the user, who's salt will be used. * @return string The hash. Variable length, based on the algorithm used. * @throws Exception */ public static function generate_hash($input, $userID) { try { // Fetch and initialize data. $sql = "SELECT `pw_salt`, `pw_memory`, `pw_algo` FROM `member` WHERE `id` = {$userID}"; $resultSet = Database::Get()->query($sql); $resultRow = $resultSet->fetch_assoc(); $salt = $resultRow['pw_salt']; $algo = $resultRow['pw_algo']; $memorySize = $resultRow['pw_memory']; $hash = $input . $salt; // In case of a normal hash algorithm. if(in_array($algo, hash_algos())) { // Set up a memory buffer and fill it with hashes created using // the input and the salt, then hash the whole thing as the output. // Should eat up loads of memory and CPU time. $buffer = ""; while(strlen($buffer) < $memorySize) { $buffer .= hash($algo, $input . $salt); } $hash = hash($algo, $buffer); $buffer = null; } // Fall back to bcrypt, if possible. // No memory limit. (The memory field will be used as the key length) // This eats up much more CPU time than memory. else if($algo == 'bcrypt' && CRYPT_BLOWFISH == 1) { $keyLength = ($memorySize < 10 ? "0".$memorySize : $memorySize); $hash = substr(crypt($input, '$2a$'. $keyLength .'$'. $salt . '$'), 28); } // Algorithm not available. Bail out! else { throw new Exception("The hashing algorithm '$algo' is not available.", 501); } return $hash; } catch(DatabaseException $ex) { throw new Exception("Failed to fetch user data.", 304, $ex); } } /** * Validates the password against the one stored in the user database. * @param string $password The password to validate. * @param string $userName The name of the member. * @return bool * @throws Exception */ public static function validate_password($password, $userName) { try { $sql = "SELECT `id`, `password` FROM `member` WHERE `name` = '{$userName}'"; $resultSet = Database::Get()->query($sql); if($resultSet && $resultSet->num_rows == 1) { $resultRow = $resultSet->fetch_assoc(); $generatedHash = self::generate_hash($password, $resultRow['id']); return ($resultRow['password'] == $generatedHash); } else { throw new DatabaseException("Failed to fetch user data.", 500, $sql, Database::Get()->error); return false; } } catch(DatabaseException $ex) { throw new Exception("Failed to fetch user data.", 500, $ex); } } }
mysql> SELECT * FROM member\G *************************** 1. row *************************** id: 1 name: User 1 pw_salt: 0e20d1dc3c6e60225597be pw_memory: 10485760 pw_algo: sha512 password: 09254dc706eb1acb5420adcbc97d67a0992fb3530e8c844c4c3a930614d447f371f2f0a2af369dd1c73452819d3aac30bdeea7f889b11f4cf038435c26d5036e
$buffer .= hash($algo, $input . $salt);
You could optimize the string concatenation.
$buffer = array(); $hash = hash($algo, $input . $salt); for($y = floor($memorySize / strlen($hash) / 2); $y > 0; --$y) { $buffer[] = hash($algo, $y . $hash . $salt); } $hash = hash($algo, implode($buffer));
You could calculate the number of iterations you'll need by dividing the memory usage (in bytes) by the length of the hash. That way you also don't have to re-check the length of the string.
Thanks.
I did use an array at first, adding each hash as an element and them imploding it before creating the final hash. (Like you do in your code.)
However, after testing that I found that this method uses double the amount of memory the string concatenation method uses.
Makes sense when you think about it. Both the array and the string will have to exist in memory at the same time when the implode function is called.
After testing this a bit more, I've decided to just use half the array size. The array takes up half the memory, and the string the implode creates uses the rest. Seems to be most efficient.
This takes only half the time my previous code did :-)php Syntax (Toggle Plain Text)
$buffer = array(); $hash = hash($algo, $input . $salt); for($y = floor($memorySize / strlen($hash) / 2); $y > 0; --$y) { $buffer[] = hash($algo, $y . $hash . $salt); } $hash = hash($algo, implode($buffer));
This also fixed another problem in my previous code.
The loop that created the memory buffer in my previous code just hashed and added the same hash over and over. It could have been optimized to practically nothing by hashing it once outside the loop and adding that each loop, rather than recreating the hash each time.
By adding the loop index to the string that gets hashed, I now get a unique hash added to the buffer each loop.
Good point, thanks.
// generate a hash for 2MB file $hash = sha1_file('hash.txt'); echo 'memory peak use: '.floor(memory_get_peak_usage(true)/1000).' Kb';
$memorySize variable to 10Mb, the peak usage of the script goes to around 10.5Mb.floor(memory_get_peak_usage(true)/1000) But neither of our codes need the hash functions themselves to use a lot of memory. They both use arrays and strings we created to fill the memory.
I mean, using my last code, setting the$memorySizevariable to 10Mb, the peak usage of the script goes to around 10.5Mb.
Or am I not getting your point?
P.S.
floor(memory_get_peak_usage(true)/1000)
1Kb = 1024b ;-)
// sha1_stream.php echo sha1_file('php://stdin');
$salt = 'e5cbe45c71a0b805278f2b5f94eb108dba532af3'; $hash = 'test'; $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 2 => array("file", "error-output.txt", "a") // stderr is a file to write to ); $cwd = './'; $env = array(); $options = array('bypass_shell'); $php_path = 'c:\xampp\php\php.exe'; // change to php path $file_path = dirname(__FILE__).'/sha1_stream.php'; $process = proc_open("$php_path $file_path", $descriptorspec, $pipes, $cwd, $env, $options); if (is_resource($process)) { $i = 1000; while(--$i) { // instead of saving the hashes in memory, we write each hash to the other PHP process which computes the overall hash for the whole string. fwrite($pipes[0], sha1($hash.$salt)); } fclose($pipes[0]); $hash = stream_get_contents($pipes[1]); fclose($pipes[1]); $return_value = proc_close($process); } else { throw new Exception('Could not create process. see: error-output.txt'); } echo $hash; echo '<br />';
| DaniWeb Message | |
| Cancel Changes | |