Как правильно хранить пароли: хэш и соль
Главное правило: сервер не должен знать ваш пароль. Он хранит только его необратимый отпечаток.
Хэширование пароля — преобразование пароля в строку фиксированной длины так, что по ней нельзя восстановить пароль, но можно проверить введённый пароль на совпадение.
Почему нельзя хранить пароли в открытом виде
Если база данных хранит пароли как есть (в виде текста), то одна утечка базы — и злоумышленник получает пароли всех пользователей. Хуже того: люди часто используют один пароль на многих сайтах, поэтому утечка с маленького форума открывает доступ к их почте и банку. Поэтому сервер обязан хранить не пароль, а его хэш.
Почему обычный 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) делают перебор невыгодным — используйте готовые библиотеки.