Грабли реализации криптографии

Большинство криптопровалов случается не из-за слабых алгоритмов, а из-за того, как их применили.

Правило №1 прикладной криптографии: «не изобретай своё крипто». Почти все реальные взломы — это ошибки реализации поверх стойких примитивов, а не взлом самих примитивов.

Этот урок собирает воедино типичные грабли из всего раздела и формулирует набор безопасных дефолтов. Цель — не запомнить список запретов, а выработать инженерную привычку: использовать проверенные инструменты правильным образом.

Зачем это знать защитнику

Алгоритмы вроде AES, SHA-256, ChaCha20 десятилетиями выдерживают анализ всего мирового сообщества. Шанс, что вы найдёте в них дыру, ничтожен — а шанс ошибиться в обвязке (режим, nonce, сравнение, хранение ключа) огромен. Поэтому работа защитника смещается с «придумать шифр» на «правильно собрать систему из готовых, проверенных блоков и не наступить на известные грабли».

Грабля 1. Самописная криптография

Свой шифр, своя хеш-функция, свой протокол обмена ключами почти всегда оказываются уязвимыми: нет ни peer-review, ни анализа side-channel, ни проверки граничных случаев. Сюда же — «улучшения» стандартов (двойной XOR, своя схема паддинга, «секретный» алгоритм). Безопасность через неясность (security by obscurity) не работает: стойкость должна держаться на ключе, а не на тайне алгоритма.

Защита: используйте высокоуровневые библиотеки, которые делают правильные вещи по умолчанию — cryptography (Python), libsodium/PyNaCl, Tink, age. Они дают AEAD, безопасную генерацию ключей и разумные дефолты «из коробки».

Грабля 2. Повтор nonce / IV

Nonce («number used once») обязан быть уникальным на каждый ключ. Для поточных и GCM-режимов повтор nonce катастрофичен: keystream повторяется, и связь между двумя открытыми текстами утекает, а в GCM повтор ещё и компрометирует ключ аутентификации. Покажем суть на XOR-потоке: при повторе nonce из двух шифртекстов вычитается keystream, обнажая p1 XOR p2.

def xor_hex(a: bytes, b: bytes) -> str:
    return (bytes(x ^ y for x, y in zip(a, b))).hex()

keystream = b"\x10\x20\x30\x40\x50\x60"  # повторился (nonce тот же)
p1, p2 = b"HELLO!", b"WORLD?"
c1 = bytes(x ^ y for x, y in zip(p1, keystream))
c2 = bytes(x ^ y for x, y in zip(p2, keystream))

print("c1 XOR c2 (видит атакующий):", xor_hex(c1, c2))
print("p1 XOR p2 (секрет):         ", xor_hex(p1, p2))
print("Совпали -> keystream исчез: ", xor_hex(c1, c2) == xor_hex(p1, p2))

Вывод:

c1 XOR c2 (видит атакующий): 1f0a1e000b1e
p1 XOR p2 (секрет):          1f0a1e000b1e
Совпали -> keystream исчез:  True

Атакующий не знал keystream, но получил соотношение между открытыми текстами — а дальше анализ языка часто доводит до полного восстановления.

Защита: генерируйте nonce из CSPRNG (secrets.token_bytes) или ведите строгий счётчик, никогда не переиспользуйте пару (ключ, nonce). Меняйте ключ задолго до исчерпания пространства nonce. Лучше всего — режимы, где это сложнее испортить (XChaCha20-Poly1305 с большим 192-битным nonce, misuse-resistant AES-GCM-SIV).

Грабля 3. Сравнение не constant-time

Сравнение MAC, токенов, хешей через обычный == открывает тайминг-канал (см. урок про side-channel). Это особенно опасно на серверах, отвечающих в сеть.

Защита: сравнивайте секреты только constant-time-функциями — secrets.compare_digest / hmac.compare_digest (Python), crypto.timingSafeEqual (Node.js), hmac.Equal (Go).

Грабля 4. Опасные дефолты и хранение ключей

  • Хеширование паролей быстрым хешем. md5/sha256 для паролей — ошибка: их легко перебирать на GPU. Нужны медленные KDF с солью: Argon2id, scrypt, bcrypt.
  • Устаревшие примитивы. MD5 и SHA-1 нестойки к коллизиям; DES/3DES, RC4, режим ECB — устарели. Используйте SHA-256/SHA-3, AES-GCM, ChaCha20-Poly1305.
  • Ключи в коде и репозитории. Хардкод ключей и секретов — частая утечка. Храните их в секрет-менеджере (Vault, KMS, переменные окружения вне VCS), ротируйте.
  • Слишком короткие параметры. RSA < 2048 бит, симметричные ключи < 128 бит — недостаточно. Берите ≥2048 (лучше 3072) для RSA и ≥256 бит для эллиптических кривых.

Как это работает под капотом

Все эти грабли объединяет одно: примитив остаётся стойким, но нарушается контракт его безопасного использования — уникальность nonce, аутентификация до расшифровки, неразличимость по времени, секретность и достаточная длина ключа. Криптобиблиотеки высокого уровня тем и ценны, что зашивают эти контракты внутрь API: вы вызываете encrypt/decrypt, а уникальность nonce, AEAD-аутентификация и constant-time-проверки уже сделаны за вас правильно.

Как защититься: безопасные дефолты

  • Не пишите крипто сами — берите cryptography, libsodium/PyNaCl, Tink, age.
  • Шифрование — только AEAD (AES-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305): целостность включена.
  • Nonce — уникален всегда: CSPRNG или строгий счётчик, никогда не повторять на одном ключе.
  • Сравнение секретов — constant-time (compare_digest и аналоги).
  • Случайность — из CSPRNG (secrets / os.urandom), ≥128 бит для ключей.
  • Пароли — через Argon2id/scrypt/bcrypt с солью, не быстрым хешем.
  • Ключи — в секрет-менеджере, не в коде; ротация и достаточная длина.
  • Современные примитивы: SHA-256/SHA-3, AES, без MD5/SHA-1/DES/RC4/ECB.

Правовое напоминание: отрабатывать атаки на реализацию допустимо только на своих системах или в разрешённой лаборатории (DVWA, juice-shop, локальная ВМ, CTF). Несанкционированные действия наказуемы (УК РФ ст. 272–273).

Итоги

  • Большинство криптопровалов — ошибки обвязки, а не взлом алгоритмов; правило №1 — не изобретать своё крипто.
  • Повтор nonce/IV раскрывает связь между открытыми текстами и подрывает AEAD — nonce обязан быть уникальным.
  • Сравнивайте секреты constant-time, генерируйте их CSPRNG, храните ключи вне кода и используйте современные примитивы.
  • Лучший дефолт — проверенная высокоуровневая библиотека с AEAD: она зашивает безопасные контракты внутрь API.
Проверьте себя
1. Почему повтор nonce в поточном или GCM-режиме на одном ключе опасен?
AШифртекст становится длиннее ожидаемого
BПовторяется keystream, из-за чего из двух шифртекстов утекает связь между открытыми текстами (а в GCM компрометируется и аутентификация)
CРасшифровка начинает работать медленнее
DМеняется длина ключа
2. Какой принцип лучше всего описывает безопасный подход к прикладной криптографии?
AПисать собственный шифр, чтобы алгоритм был секретным (security by obscurity)
BИспользовать MD5 для паролей ради скорости
CНе изобретать своё крипто и применять проверенные библиотеки с AEAD и безопасными дефолтами
DХранить ключи прямо в коде для удобства