Все о паролях: стойкость, хранение и хеширование

Все о паролях: стойкость, хранение и хеширование Полезное

Пароль — это секрет, который пользователь предъявляет системе, чтобы подтвердить, что он тот, за кого себя выдает. Это фактор аутентификации из категории «то, что я знаю». Дальше разберем два разных вопроса, которые часто путают: как сделать пароль стойким к подбору (сторона пользователя) и как его безопасно хранить на сервере через хеширование (сторона разработчика), а также 2FA, менеджеры паролей и типичные ошибки.

Три близких понятия: не путать кодирование, шифрование и хеширование

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

Операция Обратима? Нужен ключ? Для чего Пример
Кодирование да, всегда нет смена формата представления Base64, URL-encode
Шифрование да, при наличии ключа да конфиденциальность данных AES, RSA-OAEP
Хеширование нет (одностороннее) нет проверка без хранения оригинала Argon2, bcrypt

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

Как правильно хранить пароли на сервере

Правило простое: сервер не должен хранить пароль в открытом виде и не должен уметь его восстановить. Хранят только хеш, полученный специализированной «медленной» функцией с солью.

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

Почему нельзя брать обычные быстрые хеши вроде MD5 или одиночного SHA-256: они создавались для скорости. На видеокарте перебираются миллиарды вариантов в секунду, поэтому по хешу быстро подбирают короткий пароль. Для паролей нужны намеренно медленные и настраиваемые функции.

Функция Тип Годится для паролей Замечание
MD5, SHA-1 быстрый хеш нет рассчитаны на скорость — дешевый массовый перебор; у MD5/SHA-1 вдобавок есть криптослабости
SHA-256 сам по себе быстрый хеш нет слишком быстрый, перебор на GPU
bcrypt адаптивный, медленный да, для legacy для новых систем предпочтителен Argon2id; настраиваемая «цена», предел входа 72 байта
scrypt требователен к памяти да память затрудняет атаку на GPU/ASIC; запасной вариант, если Argon2id недоступен
Argon2id требователен к памяти и времени да, для новых систем выбор по умолчанию по OWASP; вариант семейства Argon2 — победителя конкурса PHC (2015)

Антипример и как исправить

Так делать нельзя — быстрый хеш без соли подбирается перебором мгновенно:

import hashlib

# ПЛОХО: MD5 без соли, быстрый хеш
bad = hashlib.md5(b"123456").hexdigest()
print(bad)   # e10adc3949ba59abbe56e057f20f883e

Этот хеш известного пароля есть в любой готовой таблице, обращение занимает доли секунды. Для новых систем OWASP рекомендует по умолчанию Argon2id (пакет argon2-cffi): он требователен и к времени, и к памяти, что затрудняет перебор на GPU и ASIC. Начинайте с него:

from argon2 import PasswordHasher

ph = PasswordHasher()   # параметры по умолчанию - Argon2id
hashed = ph.hash("correct horse battery staple")
print(hashed)
# $argon2id$v=19$m=65536,t=3,p=4$...  соль внутри строки, случайна

print(ph.verify(hashed, "correct horse battery staple"))  # True
# ph.verify(hashed, "wrong") -> выбросит VerifyMismatchError

Соль генерируется автоматически и встраивается в строку хеша, а verify сам достает ее для сравнения. Если Argon2id недоступен, OWASP допускает scrypt; bcrypt берут прежде всего для совместимости с уже существующими системами:

bcrypt — для наследуемых систем. Рабочий и распространенный вариант, но с пределом входа в 72 байта: все, что длиннее (в том числе длинные Unicode-пароли), молча обрезается. Если поддерживаете такие пароли — проверьте поведение конкретной реализации и настройте cost (rounds) по замерам на своем железе.

import bcrypt

password = b"correct horse battery staple"
# rounds (cost) задает вычислительную цену; соль случайна и уже внутри хеша
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed.decode())
# $2b$12$....  у вас будет другая строка - соль случайна

print(bcrypt.checkpw(password, hashed))   # True
print(bcrypt.checkpw(b"wrong", hashed))   # False

Вывод обеих функций недетерминирован: из-за случайной соли строка хеша каждый раз разная, и это правильно. Важная граница: конкретные параметры (объем памяти, число проходов, rounds) подбирают под свое железо так, чтобы одна проверка занимала десятки-сотни миллисекунд. Это компромисс между удобством пользователя и стоимостью атаки, единственно верного числа тут нет.

Требования к паролям и защита входа на стороне приложения

Медленный хеш защищает базу ПРИ утечке — офлайн-перебор становится дорогим. Но он не ограничивает попытки входа через форму: это отдельная угроза (онлайн-подбор), и закрывают ее иначе.

Политика паролей для сервиса (по NIST SP 800-63B):

  • минимальная длина — от 15 символов для однофакторного пароля (в составе MFA допустимо от 8);
  • максимум не меньше 64 символов, разрешать пробелы и Unicode;
  • не навязывать обязательные наборы символов (регистр, цифры, спецсимволы) — это лишь снижает энтропию предсказуемым образом;
  • проверять новый пароль по блок-листу распространенных и уже утекших значений.

Защита от онлайн-подбора (отдельно от хранения хеша):

  • ограничение частоты попыток (rate limiting), адаптивная задержка или аккуратная блокировка;
  • журналирование входов и обнаружение credential stuffing;
  • MFA как страховка на случай кражи самого пароля.

Что делает пароль стойким

Стойкость к перебору определяется не «сложностью символов на вид», а числом равновероятных вариантов, которые атакующему пришлось бы проверить. Для случайного пароля это оценивают через энтропию: чем больше алфавит и длина, тем больше вариантов.

Отсюда практический вывод, который стоит пометить как упрощение: при случайной генерации длина дает больше стойкости, чем экзотические символы. Оговорка — это верно именно для случайных паролей; осмысленная фраза из словарных слов подбирается по словарю быстрее, чем говорит ее длина.

Работающий прием — длинная парольная фраза из несвязанных слов или пароль, сгенерированный случайно. Не придумывайте случайность в голове, берите ее из криптостойкого источника:

import secrets
import string

alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
password = "".join(secrets.choice(alphabet) for _ in range(16))
print(password)   # напр. 'T7v!qLm2$xR9wZ4a' - у вас будет другой

Модуль secrets предназначен для секретов (в отличие от random, который для этого не годится). Длину 16 символов здесь стоит считать разумным минимумом для важных аккаунтов, а не жестким стандартом.

Частые ошибки

  • Один пароль на много сервисов. После любой утечки его подставляют на другие сайты (атака credential stuffing).
  • Личные данные в пароле: даты, имена, клички, город — все, что можно узнать из соцсетей.
  • Короткие и словарные пароли, «12345», «qwerty», «password».
  • Хранение паролей на сервере в открытом виде или под быстрым хешем без соли — ошибка уже на стороне разработчика.

Менеджеры паролей и двухфакторная аутентификация

Держать в голове десятки длинных уникальных паролей невозможно — и не нужно. Эту задачу решает менеджер паролей: он генерирует случайные пароли, хранит их в зашифрованном хранилище и подставляет по запросу. Пользователь помнит один мастер-пароль. Примеры: KeePassXC, Bitwarden, 1Password. Хранилище защищено шифрованием, ключ выводится из мастер-пароля, поэтому его стойкость критична.

Двухфакторная аутентификация (2FA) добавляет второй фактор к паролю, чтобы одной кражи пароля было недостаточно. Факторы бывают трех родов: знание (пароль), владение (телефон, ключ) и биометрия (отпечаток, лицо). Настоящая двухфакторность — это два разных рода, а не два пароля.

Второй фактор Защищает от кражи пароля Устойчив к фишингу Замечание
SMS-код да нет уязвим к перехвату и подмене SIM, но лучше, чем ничего
TOTP-приложение да нет код можно выманить на фишинговом сайте и сразу переиспользовать; работает офлайн (Google Authenticator, Aegis)
Аппаратный ключ FIDO2 да да WebAuthn привязывает подтверждение к домену, выманить код нельзя

Отдельно стоит упомянуть тренд на беспарольный вход — passkeys на базе стандарта WebAuthn/FIDO2. Вместо пароля используется пара ключей: закрытый остается на устройстве, сайту уходит только подтверждение. Это убирает и подбор, и фишинг пароля как класс, и в 2026 году passkeys поддерживают крупные платформы и сервисы.

Выводы

  • Пароль на стороне пользователя и хранение пароля на сервере — две разные задачи; их решают по-разному.
  • Кодирование, шифрование и хеширование — разные операции; пароли хранят через одностороннее хеширование, а не шифрование.
  • На сервере пароль хранят только как хеш с солью, полученный медленной функцией: для новых систем по умолчанию Argon2id (затем scrypt), а bcrypt — для совместимости с наследуемыми; не MD5/SHA-1 и не одиночный SHA-256.
  • Медленный хеш защищает базу при утечке (офлайн-перебор), но не вход: от онлайн-подбора нужны ограничение попыток, мониторинг и MFA, а политику длины/блок-лист задают по NIST.
  • Стойкость случайного пароля определяет прежде всего длина; уникальность на каждый сервис важнее «сложности» символов.
  • Менеджер паролей снимает нагрузку с памяти, 2FA страхует от кражи пароля (но только FIDO2 устойчив к фишингу), а passkeys постепенно убирают пароль как таковой.

Где применяется / связь с практикой

Освойте тему на практике

Хранение паролей, соль, выбор функции хеширования, настройка 2FA и защита от перебора — это повседневные задачи инженера по безопасности и любого бэкенд-разработчика, который проектирует аутентификацию. Разобрать их системно, с моделью угроз и практикой, помогает курс Информационная безопасность. Посмотреть формат и глубину занятий без оплаты можно на открытых уроках — там разбирают в том числе прикладные темы по защите данных.

Смежные темы: Все об асимметричном шифровании, Все о кодировках в компьютерах.

FAQ

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

Нужно ли периодически менять пароль без причины?
Современные рекомендации (в том числе NIST) от обязательной ротации по расписанию отошли: она подталкивает к предсказуемым вариациям. Менять стоит при подозрении на утечку; проверить свою почту можно в сервисах контроля утечек.

Менеджер паролей — это не опасно, ведь все пароли в одном месте?
Риск концентрации есть, но на практике он ниже, чем повторное использование слабых паролей. Хранилище зашифровано, а мастер-пароль стоит сделать длинным и закрыть двухфакторной аутентификацией.

OTUS Журнал