Хеширование и соление паролей


Что содержится в статье ⌄
  • Как реализовать хеширование паролей и использовать соли
  • Лучшие практики безопасного хранения паролей
  • Зачем использовать соль в хешировании паролей
  • Как предотвратить атаки с использованием радужных таблиц
  • Что такое соление паролей
  • Как повысить безопасность паролей с помощью pepper
  • Что такое соль и перец в хэшировании паролей

Простое хеширование

Хранить пользовательские пароли и PIN-коды в открытом виде небезопасно.

Первое, что приходит в голову – захешировать пароль и хранить в БД только хеш:

  1. Клиент отправляет серверу PIN-код в открытом виде (по защищенному HTTPS)
  2. Сервер хеширует PIN-код и сохраняет его в БД
  3. Когда клиенту нужно проверить PIN-код позже, он снова отправляет PIN-код в открытом виде по HTTPS
  4. Сервер хеширует полученный PIN-код и сравнивает его с тем, который хранится в БД
  5. Сервер возвращает результат проверки
  6. Мы можем не беспокоиться о возможном взломе БД, потому что в ней не хранятся открытые пароли (или все-таки стоит?)

Есть такая штука как “радужные таблицы”. Это предварительно вычисленные таблицы в следующем формате:

пароль     | хэш
----------------------
"1234"     | "a1b2c3"
"changeit" | "x7y8z9"
"qwerty"   | "m4n5p6"
...и миллионы других...

В случае утечки БД с хешированными паролями злоумышленник может сравнить утекшие хеши с радужными таблицами и, таким образом, узнать PIN-код/пароль.

Вы можете сказать: “подожди, могут же быть hash collisions, так что разные данные могут порождать один и тот же хеш”. Дело в том, что hash collisions крайне редки с современными хеш-функциями, такими как SHA-256, SHA-3 или BLAKE2 (именно поэтому они используются для проверки целостности файлов и цифровых подписей).

Это значит, что если мы видим хеш x7y8z9 в БД, мы почти можем быть уверены, что пароль – это changeit.


Хеширование + соление

Хитрость для повышения криптографической стойкости заключается в добавлении соли следующим образом:

  1. Клиент отправляет серверу PIN-код в открытом виде (по защищенному HTTPS)
  2. Сервер генерирует соль для этого конкретного пользователя
  3. Сервер хеширует комбинацию PIN-кода и соли и сохраняет результат в БД ПЛЮС соль (в ее исходном виде)
  4. Когда клиенту нужно проверить PIN-код позже, он снова отправляет PIN-код в открытом виде по HTTPS
  5. Сервер извлекает соль для этого пользователя из БД, добавляет соль к полученному PIN-коду, хеширует эту комбинацию и сравнивает с тем, что хранится в БД
  6. Сервер возвращает результат проверки.

Важно:

  • Открытый 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)  # перец не сохраняется!

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