Хеширование паролей: соль и медленные хеши
Сайты не должны хранить твой пароль. Они хранят его хеш — отпечаток, по которому нельзя восстановить пароль. Но сделать это правильно сложнее, чем кажется.
Почему не открытым текстом
Если база данных хранит пароли как есть и её украдут, утекут все пароли сразу. А люди часто используют один пароль везде — пострадает и почта, и банк. Поэтому хранят хеш: при входе сайт хеширует введённый пароль и сравнивает хеши.
Проблема обычного хеша
Казалось бы, берём SHA-256 — и готово. Но есть две беды.
Беда 1: одинаковые пароли дают одинаковый хеш
import hashlib
def sha(p):
return hashlib.sha256(p.encode()).hexdigest()
print("Аня пароль '123456':", sha("123456"))
print("Боря пароль '123456':", sha("123456"))
print("Хеши совпали -> видно, что у них одинаковый пароль")
Злоумышленник заранее считает хеши популярных паролей (это называется радужная таблица) и мгновенно узнаёт исходники.
Беда 2: SHA-256 слишком быстрый
Видеокарта считает миллиарды SHA-256 в секунду. Перебрать все короткие пароли — дело минут.
Решение 1: соль
Соль — случайные данные, добавляемые к каждому паролю перед хешированием. У каждого пользователя своя соль, поэтому одинаковые пароли дают разные хеши, а радужные таблицы бесполезны.
import hashlib, secrets
def hash_with_salt(password):
salt = secrets.token_bytes(16) # уникальная соль
h = hashlib.sha256(salt + password.encode()).hexdigest()
return salt.hex(), h
salt_a, hash_a = hash_with_salt("123456")
salt_b, hash_b = hash_with_salt("123456")
print("Аня: ", hash_a)
print("Боря:", hash_b)
print("Тот же пароль — РАЗНЫЕ хеши:", hash_a != hash_b)
Решение 2: медленные хеши
Против перебора пароль нужно хешировать намеренно медленно. Специальные алгоритмы — bcrypt, scrypt, Argon2 — настраиваются так, чтобы один хеш считался, скажем, 0.1 секунды. Для входа это незаметно, а для перебора миллиардов вариантов — непреодолимо.
Покажем идею медленности «растягиванием» — много раундов хеша подряд:
import hashlib, secrets, time
def slow_hash(password, rounds=200_000):
salt = secrets.token_bytes(16)
h = password.encode()
for _ in range(rounds): # тысячи повторов = медленно
h = hashlib.sha256(salt + h).digest()
return h.hex()
t = time.time()
result = slow_hash("123456")
print("Хеш:", result[:32], "...")
print(f"Считался {time.time()-t:.3f} сек — намеренно медленно")
print("Это упрощённая идея PBKDF2/bcrypt/Argon2")
В реальном коде не пишите это руками — берите готовый bcrypt или argon2-cffi. Они правильно реализуют соль и стоимость.
Вывод:
Пароли хранят как хеш, а не открытым текстом. Обычный SHA-256 плох: одинаковые пароли дают одинаковый хеш и он слишком быстрый. Лечат солью (уникальной для каждого) и медленными хешами (bcrypt/scrypt/Argon2).