Чтобы лучше понять "как" и "почему", для начала следует сформулировать задачу и рассмотреть пути решения.
Задача состоит в том, чтобы иметь возможность проверить подлинность секретного параметра пользователя, не храня у себя его, и не давая злоумышленнику возможности узнать этот самый секретный пароль.
Для этого принято использовать криптографические хэш-функции, так как их вычисления не обратимы, а множество выходных значений имеет распределение близкое к равномерному.
Криптография всегда работает на условии, что все параметры системы открыты и известны, кроме непосредственно секретов. То есть в задачи хэширования пароля
секретом является только пароль, а все остальные параметры (соли, алгоритмы, хэши) - известны. Соответственно нужно рассматривать ситуацию, что злоумышленник располагает абсолютно всем - базой хэшей, полным алгоритмом хэширования и всеми его параметрами.
Давайте поставим теперь себя на место злоумышленника. Располагая всем этим, как бы мы взламывали пароль?
1. Радужные таблицы (заранее сгенерированные таблицы "хэш -> значение").
2. Атака по словарю (сначала делается перебор по наиболее вероятным значениям).
3. Полный перебор.
Полный перебор хоть и решит задачу 100%, но это крайняя мера, крайне неэффективная, и, как правило, не рабочая, потому что пусть пароль и можно подобрать за десятки тысяч лет, злоумышленник столько не проживет (как и сама взламываемая система, и сам пользователь). Потому если мы обезопасились от пунктов 1 и 2 кое-как, то задачу, считай, решили.
Если мы просто возьмем какую-то хэш-функцию (например, sha256) и захэшируем пароль, то сделаем очень плохо. Почему? Потому что злоумышленник, видя это, просто возьмет хэш и подставит в радужную таблицу, и, если пользователь не заморачивался с паролем (а как правило так и есть), то пароль будет получен практически сразу.
Что нужно сделать, дабы исключить вариант использования радужных таблиц?
Сделать так, чтобы значения, подаваемые на вход хэш-функции, гарантированно в них отсутствовали. Для этого и придумали соль. В связи с этим к соли есть одно простое требование:
соль должна быть сложно угадываемой.
То есть, если у нас есть пароль "password" и соль "111", то вероятность попадания строки "password111" в радужную таблицу все ещё очень высока, а значит подобная соль плохая. А вот соль "t%Lp-,DU4=w],9c7F.N$" хороша, потому что строку "passwordt%Lp-,DU4=w],9c7F.N$" в радужную таблицу никто записывать не будет.
Вывод: нам
нужна соль для того, чтобы исключить поиск по хэшу в радужных таблицах.
Если при этом Вы сделаете соль уникальной для каждого пользователя, то даже если все пользователи используют одинаковый пароль, хранимые хэши будут разные. Плюс этого в том, что злоумышленнику придется взламывать каждого пользователя отдельно в любом случае.
Вывод:
делаем соль уникальной для каждого пользователя, дабы взлом одного пользователя не давал доступ к другим.
Далее в арсенале злоумышленника остается атака по словарю.
Увы, полностью исключить данный вариант может только пользователь, если будет использовать стойкие и сложные к угадыванию пароли, у которых вероятность попадания в словари "крайне мала"(с).
Но на благоразумие всех пользователей надеятся не стоит, а обезопасить их как-то надо. И здесь у нас остается возможность "вставить палку в колесо" злоумышленнику, увеличив время выполнения алгоритма. То есть просто берем и хэшируем результат повторно надцать раз, пока время выполнения алгоритма не станет достаточно длинным, например, 500ms. Нам при аутентификации пользователя (да и самому пользователю) 500ms ждать не проблема, а вот злоумышленнику делать подбор пароля со скоростью 2 пароля за секунду, уже "ой-какая-головная-боль".
Вывод:
повторяем хэширование много раз, дабы увеличить время выполнения алгоритма, что замедлит подбор паролей.
Вот как бы и вся общая картина, которую нужно понимать рядовому программисту для решения задачи хэширования паролей.
В каждый из указанных моментом можно углубляться, и там есть свои заморочки, но это уже более широкая тема, интересная "безопасникам" и криптографам.
Дабы реализовать все вышесказанное, к велосипеду ручонки тянуть не нужно, именно из-за вышеуказанных "заморочек" в деталях алгоритмов. Криптография дилетанства не прощает. Тем более, что уже есть де-факто промышленные стандарты.
Например, bcrypt алгоритм (спасибо
kpa6uu за упоминание). В PHP он реализован посредством стандартной
password_hash()
функции. В других языках тоже хватает реализаций.
Ну и наконец-то отвечая на Ваш вопрос "как лучше всего?", то на данный момент это алгоритм
Argon2, победитель последнего Password Hashing Competition. С реализациями в языках, к сожалению, пока что не так все радужно как у bcrypt.