К списку статей
· 10 мин чтения

Хеширование паролей в Python: bcrypt, Argon2 и почему это важно

Руководство для разработчиков по безопасности паролей в Python 3: почему быстрые хеши приводят к утечкам, как на самом деле работают соли и перцы, как выбирать между Argon2id и bcrypt и как безопасно мигрировать устаревшие базы данных, не ломая вход пользователей.

  • python
  • безопасность
  • криптография
  • пароль
  • backend

Хеширование паролей в Python: bcrypt, Argon2 и почему это важно#

Открытый замок на фоне клавиатуры ноутбука

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

Хранить пароли в открытом виде — это чистая халатность. Одна ошибка SQL-инъекции, открытый снимок резервной копии или утекший лог APM мгновенно передаёт все учётные записи пользователей злоумышленникам на блюдечке.

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

Слишком многие production-системы до сих пор полагаются на наивные вызовы вроде hashlib.sha256(password.encode()).hexdigest(). Этот 64-символьный шестнадцатеричный дайджест выглядит криптографически стойким и безопасным, но современная игровая видеокарта может пересчитывать миллиарды SHA-256 в секунду. Если такая база утечёт, учётные данные ваших пользователей можно считать хранившимися в открытом виде.

Хранение паролей — единственный уголок разработки ПО, где медлительность является фичей, а не багом. Давайте разберёмся, почему быстрые хеши не подходят, какие алгоритмы действительно стоит использовать в Python и как безопасно модернизировать систему аутентификации, не разлогинивая ни одного пользователя.

⚡ Кратко за 30 секунд

  • Никогда не используйте быстрые хеши (MD5, SHA-1, SHA-256, SHA-512) для учётных данных.
  • Строите что-то новое? Используйте Argon2id через argon2-cffi. Это современный золотой стандарт.
  • Унаследовали bcrypt? Оставьте его, но убедитесь, что коэффициент стоимости установлен на 12 или выше.
  • Работаете в режиме соответствия FIPS? Используйте PBKDF2-HMAC-SHA256 минимум с 600 000 итераций.
  • Соли обязательны и автоматичны в современных библиотеках. Не пишите собственную логику солей.
  • Всегда используйте сравнения за постоянное время, чтобы предотвратить атаки по времени.

Почему «быстрые» хеши приведут вас к утечке#

Сколько времени нужно хакеру, чтобы подобрать ваш пароль в 2026 году

Когда учёные проектировали SHA-256, у них была конкретная цель: проверять многогигабайтные ISO-образы и подписывать TLS-сертификаты как можно быстрее.

Одна энтузиастская видеокарта — например, NVIDIA RTX 4090 — может вычислять более 8 миллиардов SHA-256-хешей в секунду. Арендуйте кластер из восьми таких карт в AWS на один день, и злоумышленник сможет перебирать десятки миллиардов паролей-кандидатов каждую секунду.

Серверные стойки в дата-центре

GPU злоумышленника (SHA-256):  ████████████████████ 8 500 000 000 попыток / сек
GPU злоумышленника (Argon2id): █ 1 200 попыток / сек

Для хранения паролей ваша цель — намеренное трение. Вам нужен алгоритм, который занимает около 250–500 миллисекунд на вашем production-железе.

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


Хеширование vs. шифрование: путаница, которая должна умереть#

Пример работы криптографической хеш-функции: входные данные и дайджест

Распространённая ошибка, встречающаяся и в маркетинговых текстах, и в код-ревью у джунов: «Не волнуйтесь, мы шифруем все пароли».

Воспринимайте эту фразу как немедленный тревожный сигнал:

ПримитивОбратим ли?НазначениеЧто произойдёт при утечке?
Шифрование (AES, RSA)Да (через секретный ключ)Защита данных при передаче или хранении, чтобы авторизованные стороны могли прочитать их позже.Если злоумышленники скомпрометируют ключ или KMS, они получат все пароли в открытом виде.
Хеширование (Argon2, bcrypt)Нет (односторонняя математическая ловушка)Проверка личности без знания исходного секрета.Злоумышленникам придётся подбирать каждый пароль отдельно перебором.

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


Помощники: соли и перцы простыми словами#

Как хранить пароли в базе данных: добавление соли и хеширование

1. Соль: убийца «радужных таблиц»#

Если десять пользователей выберут "P@ssword123!", наивный хеш создаст десять одинаковых строк в базе. Злоумышленникам даже не нужно взламывать их по одному; они просто предвычисляют хеши для миллионов популярных паролей (радужная таблица) и мгновенно находят совпадение.

Соль — это просто криптографически стойкая случайная строка, добавляемая к паролю перед хешированием:

$дайджест = Хеш(пароль + соль)$

  • Уникальна для каждого пользователя: Алиса и Боб могут использовать один и тот же ужасный пароль, но из-за разных солей их сохранённые хеши выглядят совершенно не связанными.
  • Не секрет: соль хранится в открытом виде рядом с хешем в вашей базе.
  • Автоматически: современные библиотеки генерируют, форматируют и разбирают соли за вас. Вам не нужен столбец salt в SQL-схеме.

2. Перец: дополнительный сейф за стойкой#

Если злоумышленник выгрузит вашу SQL-базу через уязвимость инъекции, он получит и хеши, и их соли. Офлайн-взлом теперь может начаться.

Перец — это секретный ключ уровня приложения, подмешиваемый в хеш и хранящийся вне базы данных (например, в AWS Secrets Manager, Vault или переменных окружения).

[ Ввод пользователя ] + [ Соль (из БД) ] + [ Перец (из Env/Vault) ] ──▶ [ Хеш ]

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

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


Претенденты: выбираем алгоритм#

Вы создаёте совершенно новое приложение?
 ├── ДА ──▶ Используйте Argon2id (pip install argon2-cffi)
 └── НЕТ
      ├── Уже используете bcrypt? ──▶ Оставьте, но поднимите work factor до 12+
      └── Строгие государственные/FIPS-требования? ──▶ PBKDF2-HMAC-SHA256 (600 000+ раундов)

1. Argon2id: бесспорный король#

Argon2 победил в строгом многолетнем Password Hashing Competition в 2015 году и стандартизирован в RFC 9106.

Его суперспособность — устойчивость к памяти. GPU быстры, потому что у них тысячи крошечных ядер, выполняющих простую математику параллельно. Argon2id заставляет железо выделять значительный кусок RAM (например, 64 МиБ) для каждого потока хеширования. У высококлассной GPU с 24 ГБ видеопамяти внезапно заканчивается место уже после нескольких сотен одновременных потоков, что нейтрализует аппаратное преимущество злоумышленника.

pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError

# Настраиваем production-параметры стоимости
ph = PasswordHasher(
    time_cost=3,        # 3 итерации
    memory_cost=65536,  # 64 МиБ использования RAM
    parallelism=4,      # 4 параллельных потока CPU
    hash_len=32,
    salt_len=16,
)

raw_password = "correct horse battery staple"

# 1. Хеширование: генерация соли и кодирование параметров происходят автоматически
hashed = ph.hash(raw_password)
print(hashed)
# Вывод выглядит так:
# $argon2id$v=19$m=65536,t=3,p=4$q7tW4j...$O1WkZ9...
# [алгоритм] [версия] [память, время, потоки] [соль] [хеш]

# 2. Проверка
try:
    ph.verify(hashed, raw_password)
    print("✅ Доступ разрешён.")
except VerifyMismatchError:
    print("❌ Неверный пароль.")
except (VerificationError, InvalidHashError):
    print("⚠️ Некорректная запись хеша.")

# 3. Прозрачное обновление параметров
# Если через 2 года вы обновите сервер и увеличите memory_cost,
# этот метод проверит, нуждается ли существующий хеш в тихом пересчёте.
if ph.check_needs_rehash(hashed):
    new_hash = ph.hash(raw_password)
    # db.save_password_hash(user_id, new_hash)
🐳 Production-подводный камень: OOM-убийства в Docker и Kubernetes

Если ваш сервис работает в контейнере со строгим лимитом памяти (например, 512 МиБ) и использует memory_cost 64 МиБ (65,536 КиБ), десять одновременных запросов на вход потребуют более 640 МиБ RAM мгновенно. Ядро Linux вызовет OOM Killer и уронит контейнер приложения.

Если вы ограничены по памяти:

  1. Ограничьте скорость или поставьте в очередь попытки входа на обратном прокси (Nginx, Traefik).
  2. Или уменьшите memory_cost до 19–32 МиБ и слегка увеличьте time_cost, чтобы сохранить общую длительность работы.

2. bcrypt: надёжный боевой топор#

Выпущенный в 1999 году, bcrypt защищает production-нагрузки уже более четверти века без каких-либо критических теоретических взломов.

bcrypt использует логарифмический коэффициент стоимости. Увеличение стоимости на +1 удваивает время выполнения:

  • Стоимость 10 = 210 = 1,024 итерации
  • Стоимость 12 = 212 = 4,096 итераций (рекомендуемый базовый уровень)
  • Стоимость 14 = 214 = 16,384 итерации
pip install bcrypt
import bcrypt

password = b"my-super-secure-passphrase"

# Хеш: rounds=12 занимает примерно 250-350 мс на современных CPU
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
print(hashed)  # b'$2b$12$e8y...U5j.'

# Проверка: bcrypt.checkpw выполняет проверку за постоянное время под капотом
if bcrypt.checkpw(password, hashed):
    print("✅ Аутентификация пройдена!")
else:
    print("❌ Неверные учётные данные.")
⚠️ Ловушка 72 байт (и как обрабатывать длинные парольные фразы)

У bcrypt есть жёсткое архитектурное ограничение, унаследованное от шифра Blowfish: он молча игнорирует всё после 72-го байта. В стандартном bcrypt "a" * 72 + "secret1" и "a" * 72 + "secret2" дают одинаковые хеши. (Примечание: недавние версии Python-библиотеки bcrypt выбрасывают ValueError вместо молчаливого усечения).

Если ваши пользователи генерируют 100-символьные парольные фразы, сначала хешируйте их через SHA-256 и кодируйте результат в Base64, прежде чем передавать в bcrypt:

import base64
import hashlib
import bcrypt

def hash_long_password(password_bytes: bytes) -> bytes:
    # 1. Сжимаем бесконечную длину до безопасных фиксированных 32 байт
    digest = hashlib.sha256(password_bytes).digest()
    # 2. Кодируем в Base64, чтобы убрать нулевые байты (которые завершают C-строки)
    safe_token = base64.b64encode(digest)
    # 3. Безопасно хешируем через bcrypt (всегда <= 44 символов)
    return bcrypt.hashpw(safe_token, bcrypt.gensalt(rounds=12))

3. PBKDF2: галочка для соответствия требованиям#

PBKDF2 не является memory-hard, поэтому его значительно легче взломать на специализированных ASIC/GPU-стендах, чем Argon2 или bcrypt. Однако у него есть один явный козырь: сертификация FIPS-140.

Если вы работаете в сфере федеральных контрактов США, обороны или некоторых банковских секторах, требования соответствия часто отвергают Argon2 и требуют PBKDF2.

Python предоставляет PBKDF2 из коробки в стандартной библиотеке:

import hashlib
import hmac
import os
import secrets

password = "enterprise-compliant-password".encode()
salt = secrets.token_bytes(16)

# OWASP сейчас рекомендует >= 600 000 итераций для HMAC-SHA256
iterations = 600_000

key = hashlib.pbkdf2_hmac(
    hash_name="sha256",
    password=password,
    salt=salt,
    iterations=iterations,
    dklen=32,
)

# Проверка ОБЯЗАНА использовать hmac.compare_digest для предотвращения атак по времени!
candidate_input = "enterprise-compliant-password".encode()
test_key = hashlib.pbkdf2_hmac("sha256", candidate_input, salt, iterations, dklen=32)

if hmac.compare_digest(key, test_key):
    print("✅ Совпадение")

Ошибки, которые будут преследовать вашу production-систему#

1. Наивные сравнения строк (==)#

# ❌ ОПАСНО: прерывается на первом несовпавшем байте!
# Злоумышленники измеряют время ответа в микросекундах, чтобы восстановить хеш.
if submitted_hash == stored_hash:
    ...

# ✅ БЕЗОПАСНО: сравнение за постоянное время
import hmac
if hmac.compare_digest(submitted_hash, stored_hash):
    ...

2. Логирование тела запроса при неудачных входах#

Когда клиент опечатывается и вводит пароль в поле имени пользователя, отладочные логгеры, настроенные на запись сырого payload, запишут пароли в открытом виде прямо в Datadog, CloudWatch или Splunk. Очищайте auth-payload перед логированием.

3. Изобретение собственных схем солей#

Вам не нужно писать salt = str(uuid4()) и создавать отдельный SQL-столбец. Форматы вроде $argon2id$v=19$... и $2b$12$... — это стандартизированные строки Modular Crypt Format (MCF), содержащие соль, параметры и дайджест. Храните эту одну строку в одном столбце.


Миграция без простоя: обновление устаревших хешей#

Что, если вы только что унаследовали старое Django- или Flask-приложение, хранящее несо́ленные MD5- или SHA-256-хеши?

Вы не можете декодировать старые пароли — в этом и смысл хеша. Но вы можете выполнить ленивое перехеширование: тихо обновлять хеш каждого пользователя в фоне ровно в момент его входа.

import hashlib
import hmac
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)

def check_password_and_upgrade(stored_record: str, submitted_pw: str) -> tuple[bool, str | None]:
    """
    Проверяет пароль пользователя.
    Возвращает: (is_authenticated, new_hash_to_persist_or_None)
    """
    pw_bytes = submitted_pw.encode("utf-8")

    # Сценарий A: современный Argon2id
    if stored_record.startswith("$argon2id$"):
        try:
            ph.verify(stored_record, submitted_pw)
        except (VerifyMismatchError, VerificationError, InvalidHashError):
            return False, None
        
        # Проверяем, был ли хеш создан со старыми, более слабыми параметрами стоимости
        rehashed = ph.hash(submitted_pw) if ph.check_needs_rehash(stored_record) else None
        return True, rehashed

    # Сценарий B: устаревший SHA-256 (формат: "sha256:<salt_hex>:<hash_hex>")
    if stored_record.startswith("sha256:"):
        _, salt_hex, digest_hex = stored_record.split(":")
        salt = bytes.fromhex(salt_hex)
        expected = bytes.fromhex(digest_hex)
        
        candidate = hashlib.sha256(salt + pw_bytes).digest()
        if not hmac.compare_digest(candidate, expected):
            return False, None
        
        # Успех! Прозрачно обновляем пользователя до Argon2id
        print("🔄 Обновляем устаревшего пользователя до Argon2id...")
        return True, ph.hash(submitted_pw)

    # Неизвестный формат хеша
    return False, None
# Безопасный маршрут входа
def handle_login(username: str, candidate_pw: str) -> bool:
    user = db.find_user(username)

    if not user:
        # СМЯГЧЕНИЕ: предотвращаем перечисление пользователей через атаки по времени
        # Если пользователя не существует, всё равно вычисляем фиктивный хеш,
        # чтобы время ответа совпадало с легитимной попыткой аутентификации (~300 мс).
        ph.hash(candidate_pw)
        return False

    is_valid, new_hash = check_password_and_upgrade(user.password_hash, candidate_pw)

    if is_valid:
        if new_hash:
            db.update_user_hash(user.id, new_hash)
        return True

    return False

Проверьте сами: интерактивная площадка для хешей#

Хотите поэкспериментировать с этими алгоритмами без написания шаблонного кода? Попробуйте интерактивную площадку для хеширования паролей в браузере.

Всё выполняется на 100% локально в вашем браузере через WebAssembly (никакие пароли или учётные данные никогда не передаются по сети). Вы можете:

  • Генерировать и изучать сырые строки Argon2id vs. bcrypt.
  • Менять затраты памяти и количество итераций, чтобы увидеть, как они влияют на время выполнения.
  • Проверять форматы хешей на соответствие выводу вашего Python-бэкенда.

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

Открыт для проектов

Нужна помощь с проектом?

Проектирую надёжные и аккуратные бэкенды — FastAPI, PostgreSQL, MongoDB и self-hosted инфраструктура. От разовой консультации до полной разработки.

  • FastAPI
  • PostgreSQL
  • MongoDB
  • K8S
  • Self-hosting