Как мы можем создать довольно безопасный хэш пароля в PHP?

Я читаю хэширование паролей, но все форумы, которые я читаю, полны сообщений от людей, которые обсуждают теорию за этим, что я действительно не понимаю.

У меня есть старый (и предположительно крайне слабый) скрипт пароля, который читается следующим образом: $ hash = sha1 ($ pass1);

function createSalt() { $string = md5(uniqid(rand(), true)); return substr($string, 0, 3); } $salt = createSalt(); $hash = sha1($salt . $hash); 

Если я правильно понимаю, чем дольше соль, тем больше таблица, которую должен произвести хакер, чтобы сломать хэш. Пожалуйста, поправьте меня, если я ошибаюсь.

Я ищу, чтобы написать новый скрипт, который более безопасен, и я думаю, что что-то вроде этого будет в порядке:

 function createSalt() { $string = hash('sha256', uniqid(rand(), true)); return $string; } $hash = hash('sha256', $password); $salt = createSalt(); $secret_server_hash = 'ac1d81c5f99fdfc6758f21010be4c673878079fdc8f144394030687374f185ad'; $salt2 = hash('sha256', $salt); $hash = $salt2 . $hash . $secret_server_hash; $hash = hash('sha512', $hash ); 

Это более безопасно? У этого есть заметное количество накладных расходов?

Самое главное, есть ли какой-нибудь лучший способ убедиться, что пароли в моей базе данных не могут быть (реалистично) восстановлены криптоанализом, гарантируя тем самым, что единственный способ безопасности будет скомпрометирован через мою собственную ошибку в кодировании?

РЕДАКТИРОВАТЬ:

После прочтения всех ваших ответов и дальнейшего повторного поиска я решил пойти дальше и внедрить метод bcrypt для защиты моих паролей. Говоря это, ради любопытства, если бы я взял мой вышеприведенный код и поставил на него цикл, скажем, 100 000 итераций, выполнил бы что-то подобное силе / безопасности bcrypt?

Соли могут помочь вам до сих пор. Если алгоритм хеширования, который вы используете, настолько быстр, что для создания радужных таблиц практически нет никакой стоимости, ваша безопасность все еще скомпрометирована.

Несколько указателей:

  • НЕ используйте одну соль для всех паролей. Используйте случайную соль для каждого пароля.
  • НЕ перефразируйте немодифицированный хэш (проблема с коллизией, см. Мой предыдущий ответ , вам нужен бесконечный ввод для хэширования).
  • НЕ пытайтесь создать свой собственный алгоритм хеширования или алгоритмы согласования смешивания в сложную операцию.
  • Если вы застряли со сломанными / небезопасными / быстрыми примитивами хэша, используйте укрепление ключа . Это увеличивает время, необходимое злоумышленнику для вычисления таблицы радуги. Пример:

 function strong_hash($input, $salt = null, $algo = 'sha512', $rounds = 20000) { if($salt === null) { $salt = crypto_random_bytes(16); } else { $salt = pack('H*', substr($salt, 0, 32)); } $hash = hash($algo, $salt . $input); for($i = 0; $i < $rounds; $i++) { // $input is appended to $hash in order to create // infinite input. $hash = hash($algo, $hash . $input); } // Return salt and hash. To verify, simply // passed stored hash as second parameter. return bin2hex($salt) . $hash; } function crypto_random_bytes($count) { static $randomState = null; $bytes = ''; if(function_exists('openssl_random_pseudo_bytes') && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win $bytes = openssl_random_pseudo_bytes($count); } if($bytes === '' && is_readable('/dev/urandom') && ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) { $bytes = fread($hRand, $count); fclose($hRand); } if(strlen($bytes) < $count) { $bytes = ''; if($randomState === null) { $randomState = microtime(); if(function_exists('getmypid')) { $randomState .= getmypid(); } } for($i = 0; $i < $count; $i += 16) { $randomState = md5(microtime() . $randomState); if (PHP_VERSION >= '5') { $bytes .= md5($randomState, true); } else { $bytes .= pack('H*', md5($randomState)); } } $bytes = substr($bytes, 0, $count); } return $bytes; } 

Вместо того, чтобы развернуть свой собственный алгоритм хеширования / сорта (по сути, с недостатками), почему бы не использовать тот, который был разработан специалистами по безопасности?

Используйте bcrypt . Это было разработано именно для этого. Медленность и несколько раундов гарантируют, что злоумышленник должен развернуть огромные средства и оборудование, чтобы взломать ваши пароли. Добавьте к этим солям по паролю (bcrypt REQUIRES salt), и вы можете быть уверены, что атака практически невозможна без какой-либо смехотворной суммы средств или оборудования.

Портативная PHP Hashing Framework в непереносимом режиме позволяет легко генерировать хэши с помощью bcrypt.

Вы также можете использовать функцию crypt() для генерации хэшей bcrypt входных строк. Если вы спуститесь по этому маршруту, убедитесь, что вы генерируете одну соль за хэш.

Этот класс может автоматически генерировать соли и проверять существующие хэши на вход.

 class Bcrypt { private $rounds; public function __construct($rounds = 12) { if(CRYPT_BLOWFISH != 1) { throw new Exception("bcrypt not supported in this installation. See http://php.net/crypt"); } $this->rounds = $rounds; } public function hash($input) { $hash = crypt($input, $this->getSalt()); if(strlen($hash) > 13) return $hash; return false; } public function verify($input, $existingHash) { $hash = crypt($input, $existingHash); return $hash === $existingHash; } private function getSalt() { $salt = sprintf('$2a$%02d$', $this->rounds); $bytes = $this->getRandomBytes(16); $salt .= $this->encodeBytes($bytes); return $salt; } private $randomState; private function getRandomBytes($count) { $bytes = ''; if(function_exists('openssl_random_pseudo_bytes') && (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN')) { // OpenSSL slow on Win $bytes = openssl_random_pseudo_bytes($count); } if($bytes === '' && is_readable('/dev/urandom') && ($hRand = @fopen('/dev/urandom', 'rb')) !== FALSE) { $bytes = fread($hRand, $count); fclose($hRand); } if(strlen($bytes) < $count) { $bytes = ''; if($this->randomState === null) { $this->randomState = microtime(); if(function_exists('getmypid')) { $this->randomState .= getmypid(); } } for($i = 0; $i < $count; $i += 16) { $this->randomState = md5(microtime() . $this->randomState); if (PHP_VERSION >= '5') { $bytes .= md5($this->randomState, true); } else { $bytes .= pack('H*', md5($this->randomState)); } } $bytes = substr($bytes, 0, $count); } return $bytes; } private function encodeBytes($input) { return strtr(rtrim(base64_encode($input), '='), '+', '.'); } } 

Вы можете использовать этот код как таковой:

 $bcrypt = new Bcrypt(15); $hash = $bcrypt->hash('password'); $isGood = $bcrypt->verify('password', $hash); 

О значениях соли

Если я правильно понимаю, чем дольше соль, тем больше таблица, которую должен произвести хакер, чтобы сломать хэш. Пожалуйста, поправьте меня, если я ошибаюсь.

Да, это правильно. Хотя, если кто-то пытается сломать хэш только одного пользователя, значения соли бесполезны. Соли полезны для предотвращения (замедления) атакующих, совершающих атаку словаря на всех хэш-значениях ваших пользователей.

Позвольте мне объяснить это на примере. Предположим, у вас есть 3 пользователя в вашей системе, и вы не используете значение соли, поэтому вашей базе данных это понравится:

 user1: hash1 user2: hash2 user3: hash3 

Предположим теперь, что злоумышленник получает копию вашей базы данных. Теперь он мог сделать атаку по словарю:

 h = hash(possible_password) h == hash1? h == hash2? h == hash3? 

И поэтому он мог проверить, имеет ли один из 3 пользователей пароль possible_password , только один раз вызывающий хеш-функцию.

Предположим, вы сохранили значения хэша, которые были объединены с значениями соли в вашей базе данных следующим образом:

 user1: hash1_salted, salt1 user2: hash2_salted, salt2 user3: hash3_salted, salt3 

И снова злоумышленник копирует вашу базу данных. Но теперь, чтобы узнать, использует ли possible_password один из трех пользователей, он должен выполнить следующие проверки:

 hash(possible_password + salt1) == hash1_salted? hash(possible_password + salt2) == hash2_salted? hash(possible_password + salt3) == hash3_salted? 

Как вы видите, в этом случае злоумышленник замедляется в 3 раза (количество пользователей в вашей системе), так как он должен иметь три разных строки. Это общая идея соли, вы можете больше узнать о википедии .

Но в вашем случае соль слишком велика. То, что вы хотите предотвратить, – это два разных пользовательских хэша, имеющих одинаковое значение соли. Так, например, соль длиной 2 бита, вероятно, не будет хорошей идеей (для более чем 4 пользователей она будет уверена, что 2 имеют одинаковое значение соли). В любом случае, значение соли более 48 бит будет достаточно.

Кроме того, на самом деле нет смысла в хэшировании соли здесь $salt2 = hash('sha256', $salt); , это может как-то замедлить, но в целом сложность в вашей системе считается плохой при работе с безопасностью.

Генеральная

Наконец, никогда не бывает хорошо иметь определенные значения в вашем коде при работе с безопасностью, например $secret_server_hash , поэтому всегда следует избегать таких постоянных значений.

Лучше использовать SHA-2 вместо MD5, потому что в последние годы в MD5 обнаружены некоторые уязвимости безопасности (хотя они еще не очень практичны).

Поэтому я бы сделал что-то вроде этого:

 function createSalt() { $string = hash('sha256', uniqid(rand(), true)); return susbstr($string, 0, 8); // 8 characters is more than enough } $salt = createSalt(); $hash = hash('sha256', $hash . $password ); 

А затем сохраните $hash в вашей базе данных.

В любом случае, как уже отмечали некоторые пользователи. Вместо того, чтобы создавать собственные функции безопасности (что является хорошим способом изучения безопасности), вам лучше использовать хорошо известные библиотеки, которые проверяются большим количеством людей и, следовательно, более безопасны. В вашем случае вы должны посмотреть на crypt который делает то, что вам нужно.

Нет необходимости пытаться реализовать свою собственную серию хэшей. Вот простой класс, реализующий bcrypt:

 class Password { # return a hashed version of the plain text password. public static function hash($plain_text, $cost_factor = 10) { if ($cost_factor < 4 || $cost_factor > 31) throw new Exception('Invalid cost factor'); $cost_factor = sprintf('%02d', $cost_factor); $salt = ''; for ($i = 0; $i < 8; ++$i) $salt .= pack('S1', mt_rand(0, 0xffff)); $salt = strtr(rtrim(base64_encode($salt), '='), '+', '.'); return crypt($plain_text, '$2a$'.$cost_factor.'$'.$salt); } # validate that a hashed password is the same as the plain text version public static function validate($plain, $hash) { return crypt($plain, $hash) == $hash; } } 

С помощью:

 $hash = Password::hash('foo'); if (Password::validate('foo', $hash)) echo "valid"; 

Потенциал bcrypt заключается в том, что вы можете сделать его вычислительно дорогим для хеширования пароля (через $cost_factor ). Это делает невозможным попытку восстановить пароли всей базы данных с помощью грубой силы.