Как правильно хранить пароли: хэш и соль

Главное правило: сервер не должен знать ваш пароль. Он хранит только его необратимый отпечаток.

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

Почему нельзя хранить пароли в открытом виде

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

Почему обычный SHA-256 или MD5 — недостаточно

Хэш-функции вроде MD5 и SHA-256 спроектированы быть быстрыми. Это прекрасно для контрольных сумм, но плохо для паролей: современная видеокарта проверяет миллиарды вариантов в секунду. Если у злоумышленника есть утёкшие хэши, он просто перебирает популярные пароли и сравнивает.

Есть и вторая проблема — одинаковые пароли дают одинаковые хэши. Запустите пример:

import hashlib

# Два пользователя с одинаковым паролем
u1 = hashlib.sha256("password".encode()).hexdigest()
u2 = hashlib.sha256("password".encode()).hexdigest()

print("Хэш пользователя 1:", u1[:24], "...")
print("Хэш пользователя 2:", u2[:24], "...")
print("Хэши совпали?      :", u1 == u2)

Вывод:

Хэш пользователя 1: 5e884898da28047151d0e56f ...
Хэш пользователя 2: 5e884898da28047151d0e56f ...
Хэши совпали?      : True

Видно сразу: у этих двух пользователей одинаковый пароль. А ещё злоумышленник может заранее посчитать хэши миллионов популярных паролей (так называемые радужные таблицы) и мгновенно искать совпадения.

Соль решает проблему одинаковых хэшей

Соль (salt) — случайная строка, уникальная для каждого пользователя, которую добавляют к паролю перед хэшированием.

С солью одинаковые пароли дают разные хэши, а радужные таблицы становятся бесполезными. Соль не секрет — её хранят рядом с хэшем, но она своя у каждого:

import hashlib

def hash_with_salt(password, salt):
    return hashlib.sha256((salt + password).encode()).hexdigest()

# Одинаковый пароль, но разная соль -> разные хэши
print(hash_with_salt("password", "alice_salt")[:24], "...")
print(hash_with_salt("password", "bob_salt")[:24], "...")

Вывод:

6d64dee62dd9b96d3fbe9014 ...
91be6d9fb8aa425665694d17 ...

Теперь по хэшам не видно, что пароли одинаковые.

Медленные хэши: bcrypt, scrypt, argon2

Соль не делает перебор медленнее — она лишь убирает радужные таблицы. Чтобы сам перебор стал дорогим, используют специальные медленные алгоритмы хэширования паролей: bcrypt, scrypt, argon2. Они намеренно требуют много вычислений (и памяти), поэтому даже мощная видеокарта перебирает в тысячи раз меньше вариантов. Эти алгоритмы уже включают соль и параметр «стоимости».

В реальном проекте не пишите хэширование паролей вручную — берите проверенную библиотеку (bcrypt/argon2 для вашего языка). Ниже учебная иллюстрация принципа «медленного» хэша на стандартной библиотеке — PBKDF2 с большим числом итераций:

import hashlib

password = "qwerty123".encode()
salt = b"unique-per-user-salt"

# 100000 итераций делают перебор дорогим
dk = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000)
print("Хэш PBKDF2:", dk.hex()[:24], "...")

# Тот же пароль и соль всегда дают тот же результат -> можно проверить вход
again = hashlib.pbkdf2_hmac("sha256", password, salt, 100_000)
print("Повтор совпал?", dk == again)

Вывод:

Хэш PBKDF2: 049442e95aa0dd900f6fd428 ...
Повтор совпал? True

Как происходит проверка при входе

Когда пользователь вводит пароль, сервер берёт его соль из базы, хэширует введённый пароль тем же алгоритмом и сравнивает с сохранённым хэшем. Сам пароль нигде не хранится и в логи не попадает.

ПодходБезопасно?
Пароль в открытом виденет, катастрофа
MD5 / SHA без солинет, быстро перебирается, радужные таблицы
SHA + уникальная сольлучше, но всё ещё быстро перебирается
bcrypt / scrypt / argon2да, текущий стандарт

Итог

  • Пароли никогда не хранят в открытом виде — только хэш.
  • Простой MD5/SHA слишком быстр и уязвим к радужным таблицам.
  • Соль делает хэши уникальными и обнуляет радужные таблицы.
  • Медленные алгоритмы (bcrypt, argon2) делают перебор невыгодным — используйте готовые библиотеки.
Проверьте себя
1. Почему MD5 и SHA-256 плохо подходят для хэширования паролей?
AОни дают слишком короткий результат
BОни быстрые, поэтому перебор паролей дёшев для злоумышленника
CОни нестандартные
DОни шифруют, а не хэшируют
2. Зачем нужна соль (salt)?
AЧтобы пароль было удобнее запомнить
BЧтобы одинаковые пароли давали разные хэши и обесценить радужные таблицы
CЧтобы зашифровать пароль обратимо
DЧтобы ускорить хэширование
3. Что отличает bcrypt и argon2 от обычного SHA-256?
AОни работают без соли
BОни намеренно медленные и требовательные к ресурсам
CОни обратимы
DОни короче по длине вывода
4. Где сервер хранит сам пароль пользователя?
AВ зашифрованном виде, чтобы потом расшифровать
BНигде — хранится только хэш, а пароль проверяется сравнением хэшей
CВ логах сервера
DВ cookie браузера
Поддержать проект