Грабли реализации криптографии
Большинство криптопровалов случается не из-за слабых алгоритмов, а из-за того, как их применили.
Правило №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.