Хеширование и соление паролей
- Как реализовать хеширование паролей и использовать соли
- Лучшие практики безопасного хранения паролей
- Зачем использовать соль в хешировании паролей
- Как предотвратить атаки с использованием радужных таблиц
- Что такое соление паролей
- Как повысить безопасность паролей с помощью pepper
- Что такое соль и перец в хэшировании паролей
Простое хеширование
Хранить пользовательские пароли и PIN-коды в открытом виде небезопасно.
Первое, что приходит в голову – захешировать пароль и хранить в БД только хеш:
- Клиент отправляет серверу PIN-код в открытом виде (по защищенному HTTPS)
- Сервер хеширует PIN-код и сохраняет его в БД
- Когда клиенту нужно проверить PIN-код позже, он снова отправляет PIN-код в открытом виде по HTTPS
- Сервер хеширует полученный PIN-код и сравнивает его с тем, который хранится в БД
- Сервер возвращает результат проверки
- Мы можем не беспокоиться о возможном взломе БД, потому что в ней не хранятся открытые пароли (или все-таки стоит?)
Есть такая штука как “радужные таблицы”. Это предварительно вычисленные таблицы в следующем формате:
пароль | хэш
----------------------
"1234" | "a1b2c3"
"changeit" | "x7y8z9"
"qwerty" | "m4n5p6"
...и миллионы других...
В случае утечки БД с хешированными паролями злоумышленник может сравнить утекшие хеши с радужными таблицами и, таким образом, узнать PIN-код/пароль.
Вы можете сказать: “подожди, могут же быть hash collisions, так что разные данные могут порождать один и тот же хеш”. Дело в том, что hash collisions крайне редки с современными хеш-функциями, такими как SHA-256, SHA-3 или BLAKE2 (именно поэтому они используются для проверки целостности файлов и цифровых подписей).
Это значит, что если мы видим хеш x7y8z9 в БД, мы почти можем быть уверены, что пароль – это changeit.
Хеширование + соление
Хитрость для повышения криптографической стойкости заключается в добавлении соли следующим образом:
- Клиент отправляет серверу PIN-код в открытом виде (по защищенному HTTPS)
- Сервер генерирует соль для этого конкретного пользователя
- Сервер хеширует комбинацию PIN-кода и соли и сохраняет результат в БД ПЛЮС соль (в ее исходном виде)
- Когда клиенту нужно проверить PIN-код позже, он снова отправляет PIN-код в открытом виде по HTTPS
- Сервер извлекает соль для этого пользователя из БД, добавляет соль к полученному PIN-коду, хеширует эту комбинацию и сравнивает с тем, что хранится в БД
- Сервер возвращает результат проверки.
Важно:
- Открытый PIN-код никогда не хранится
- В БД хранятся только хеш (полученный из PIN-кода и соли) и сама соль
- Хеширование выполняется на стороне сервера
- Клиент всегда отправляет PIN-код в открытом виде (но по HTTPS)
- У каждого пользователя уникальная соль
Почему это сработает?
Без уникальной соли злоумышленник мог бы заранее вычислить хеши для всех возможных PIN-кодов. Затем просто сравнить их с базой данных.
С уникальной солью – одинаковые PIN-коды приводят к разным хешам для разных пользователей:
User1: PIN "1234" + соль "abc" = хеш "x7y8z9"
User2: PIN "1234" + соль "def" = хеш "a1b2c3"
Следовательно, злоумышленнику нужно создать новую радужную таблицу для КАЖДОЙ соли. Это делает атаку вычислительно дорогой.
Пример реализации:
# Создание PIN-кода/пароля
def store_pin(plain_pin):
salt = generate_random_salt()
hash = hash_function(plain_pin + salt)
database.save(user_id, hash, salt)
# Проверка PIN-кода/пароля
def verify_pin(received_pin):
stored_salt = database.get_salt(user_id)
stored_hash = database.get_hash(user_id)
calculated_hash = hash_function(received_pin + stored_salt)
return calculated_hash == stored_hash
Бонус: перец (pepper)
- Перец (pepper) – это значение, одинаковое для всех пользователей
- Хранится отдельно (не в базе данных)
- Это секретное значение (как ключ приложения)
хэш = hash(пароль + соль + перец)
В базе данных хранятся только: хэш + соль.
Перец полезен, когда база данных скомпрометирована, но конфигурация сервера остаётся в безопасности.
Пример реализации:
# Перец хранится в конфигурации сервера/окружении
PEPPER = "secret_key_123"
def store_password(password):
salt = generate_salt()
hash = hash(password + salt + PEPPER)
db.save(hash, salt) # перец не сохраняется!
Это дополнительный слой безопасности – даже при доступе к базе данных злоумышленнику понадобиться доступ к серверу, чтобы узнать значение перца.