Проблема здесь в основном в проблеме энтропии. Итак, начнем искать там:
Энтропия на символ
Количество бит энтропии на байт:
- Шестнадцатеричные символы
- Биты: 4
- Значения: 16
- Энтропия в 72 символах: 288 бит
- Буквенно-цифровой
- Биты: 6
- Значения: 62
- Энтропия в 72 символа: 432 бита
- «Общие» символы
- Биты: 6.5
- Значения: 94
- Энтропия в 72 символах: 468 бит
- Полные байты
- Биты: 8
- Значения: 255
- Энтропия в 72 символах: 576 бит
Итак, то, как мы будем действовать, зависит от того, каких персонажей мы ожидаем.
Первая проблема
Первая проблема с вашим кодом заключается в том, что ваш шаг хеширования "перец" выводит шестнадцатеричные символы (поскольку четвертый параметр hash_hmac()
не установлен).
Следовательно, хешируя свой перец, вы фактически сокращаете максимальную энтропию, доступную для пароля, в 2 раза (с 576 до 288 возможных бит).
Вторая проблема
Однако в первую очередь sha256
предоставляет только 256
биты энтропии. Таким образом, вы фактически сокращаете возможные 576 бит до 256 бит. Ваш шаг хеширования * немедленно * по самому определению теряет
не менее 50% возможной энтропии пароля.
Вы можете частично решить эту проблему, переключившись на SHA512
, где вы уменьшите доступную энтропию только примерно на 12%. Но это все же немаловажная разница. Эти 12% сокращают количество перестановок в раз 1.8e19
. Это большое число ... И это фактор, который уменьшает его на ...
Основная проблема
Основная проблема заключается в том, что существует три типа паролей длиной более 72 символов. Воздействие, которое оказывает на них эта система стилей, будет совсем другим:
Примечание: с этого момента я предполагаю, что мы сравниваем систему перца, которая использует SHA512
необработанный вывод (не шестнадцатеричный).
Случайные пароли с высокой энтропией
Это ваши пользователи, использующие генераторы паролей, которые генерируют сколько угодно больших ключей для паролей. Они случайны (генерируются, а не выбираются человеком) и имеют высокую энтропию на символ. Эти типы используют старшие байты (символы> 127) и некоторые управляющие символы.
Для этой группы ваша функция хеширования значительно уменьшит доступную энтропию в bcrypt
.
Позвольте мне сказать это еще раз. Для пользователей, которые используют длинные пароли с высокой энтропией, ваше решение значительно снижает надежность их паролей на измеримую величину. (62 бита энтропии потеряны для пароля из 72 символов и больше для более длинных паролей)
Случайные пароли со средней энтропией
В этой группе используются пароли, содержащие общие символы, но без старших байтов или управляющих символов. Это ваши вводимые пароли.
Для этой группы вы собираетесь немного разблокировать больше энтропии (не создавать ее, но позволить большему количеству энтропии соответствовать паролю bcrypt). Когда я говорю «слегка», я имею в виду «слегка». Безубыточность происходит, когда вы максимально используете 512 бит, которые имеет SHA512. Следовательно, пик составляет 78 символов.
Позвольте мне сказать это еще раз. Для этого класса паролей вы можете сохранить только 6 дополнительных символов, прежде чем у вас закончится энтропия.
Неслучайные пароли с низкой энтропией
Это группа, которая использует буквенно-цифровые символы, которые, вероятно, не генерируются случайным образом. Что-то вроде цитаты из Библии или чего-то подобного. Эти фразы имеют примерно 2,3 бита энтропии на символ.
Для этой группы вы можете значительно разблокировать больше энтропии (не создавать ее, но позволить большему количеству вписаться во ввод пароля bcrypt) с помощью хеширования. Безубыточность составляет около 223 символов, прежде чем у вас закончится энтропия.
Скажем еще раз. Для этого класса паролей предварительное хеширование определенно значительно повышает безопасность.
Назад в реальный мир
Подобные вычисления энтропии не имеют большого значения в реальном мире. Важно угадать энтропию. Это напрямую влияет на то, что могут делать злоумышленники. Это то, что вы хотите максимизировать.
Хотя есть небольшое исследование, посвященное угадыванию энтропии, есть некоторые моменты, на которые я хотел бы указать.
Шансы случайно угадать 72 правильных символа подряд крайне низки. Вы с большей вероятностью выиграете в лотерею Powerball 21 раз, чем столкнетесь с этим столкновением ... Вот какое большое число мы говорим.
Но статистически мы не можем на это наткнуться. В случае фраз вероятность совпадения первых 72 символов намного выше, чем для случайного пароля. Но он все еще тривиально низок (у вас больше шансов выиграть лотерею Powerball 5 раз, исходя из 2,3 бита на символ).
Практически
Практически это не имеет значения. Шансы на то, что кто-то правильно угадает первые 72 символа, а последние имеют существенное значение, настолько низки, что не стоит беспокоиться. Зачем?
Ну, допустим, вы берете фразу. Если человек может правильно понять первые 72 символа, ему либо действительно повезло (маловероятно), либо это обычная фраза. Если это обычная фраза, единственная переменная - как долго ее делать.
Возьмем пример. Возьмем цитату из Библии (просто потому, что это общий источник длинного текста, а не по какой-либо другой причине):
Не желай дома ближнего твоего. Не желай жены ближнего твоего, его слуги или служанки, его вола или осла или всего, что принадлежит твоему ближнему.
Это 180 символов. 73-й символ - g
во втором neighbor's
. Если вы так много догадались, вы, скорее всего, не остановитесь на этом nei
, а продолжите читать остальную часть стиха (поскольку именно так, вероятно, будет использоваться пароль). Таким образом, ваш "хеш" мало что добавил.
BTW: Я АБСОЛЮТНО НЕ защищаю использование цитаты из Библии. На самом деле с точностью до наоборот.
Вывод
На самом деле вы не очень сильно поможете людям, использующим длинные пароли, путем хеширования. Некоторым группам вы определенно можете помочь. Некоторым вы определенно можете навредить.
Но, в конце концов, все это не имеет особого значения. Числа мы имеем дело с просто WAY слишком высоко. Разница в энтропии не будет большой.
Вам лучше оставить bcrypt как есть. У вас больше шансов испортить хеширование (буквально, вы это уже сделали, и вы не первый или последний, кто совершит эту ошибку), чем атака, которую вы пытаетесь предотвратить.
Сосредоточьтесь на обеспечении безопасности остальной части сайта. И добавьте измеритель энтропии пароля в поле пароля при регистрации, чтобы указать надежность пароля (и указать, если пароль слишком длинный, чтобы пользователь мог его изменить) ...
По крайней мере, это мои 0,02 доллара (или, возможно, больше, чем 0,02 доллара) ...
Что касается использования «секретного» перца:
Буквально нет исследований по загрузке одной хеш-функции в bcrypt. Следовательно, в лучшем случае неясно, приведет ли когда-либо подача «приправленного» хэша в bcrypt к неизвестным уязвимостям (мы знаем, что это hash1(hash2($value))
может выявить значительные уязвимости, связанные с сопротивлением столкновениям и атаками с прообразом).
Учитывая, что вы уже подумываете о хранении секретного ключа («перца»), почему бы не использовать его хорошо изученным и понятным способом? Почему бы не зашифровать хэш перед его сохранением?
По сути, после того, как вы хешируете пароль, передайте весь хеш-вывод в надежный алгоритм шифрования. Затем сохраните зашифрованный результат.
Теперь атака SQL-инъекции не даст утечки ничего полезного, потому что у них нет ключа шифрования. И если ключ просочился, злоумышленникам ничуть не лучше, чем если бы вы использовали простой хеш (что доказуемо, чего-то с перцем "pre-hash" не дает).
Примечание: если вы решите это сделать, используйте библиотеку. Для PHP я настоятельно рекомендую Zend\Crypt
пакет Zend Framework 2 . На самом деле это единственный вариант, который я бы рекомендовал на данный момент. Он прошел тщательную проверку и принимает все решения за вас (что очень хорошо) ...
Что-то типа:
use Zend\Crypt\BlockCipher;
public function createHash($password) {
$hash = password_hash($password, PASSWORD_BCRYPT, ["cost"=>$this->cost]);
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
return $blockCipher->encrypt($hash);
}
public function verifyHash($password, $hash) {
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
$hash = $blockCipher->decrypt($hash);
return password_verify($password, $hash);
}
И это полезно, потому что вы используете все алгоритмы хорошо понятными и хорошо изученными способами (по крайней мере, относительно). Помнить:
Любой, от самого невежественного любителя до лучшего криптографа, может создать алгоритм, который сам не сможет сломать.