Ошибка шифрования данных: стоит ли доверять стандартным библиотекам
В криптографии достаточно одного неверно обработанного nonce, чтобы сильный алгоритм превратился в декорацию для отчёта по безопасности.

AES с ключом 256 бит не спасает, если приложение повторно использует пару «ключ — nonce», принимает повреждённый шифротекст за валидный или хранит ключ прямо в репозитории. Математика может быть безупречной. Код вокруг неё — уже нет.
Стандартные библиотеки действительно надёжнее самописной криптографии: они прошли больше проверок, их API изучены, а типовые ошибки давно разобраны в документации и базе уязвимостей. Но библиотека не знает, что разработчик собирается сделать с ключом после вызова функции шифрования, проверит ли он тег аутентификации и не скопирует ли секрет в логи вместе с отладочным сообщением.
Поэтому ошибка шифрования данных редко выглядит как «AES сломан». Чаще ломается обвязка: режим работы, протокол передачи, жизненный цикл ключей, обработка исключений или представление команды о том, что именно делает криптографический примитив. В Web3 к этому добавляется неприятный множитель: если зашифрованные данные участвуют в подписании транзакций, работе моста, мультиподписи или хранении секретов валидатора, ошибка может масштабироваться быстрее, чем успеет появиться постмортем.
Стандартная библиотека — это двигатель, а не автомобиль
Самописная реализация шифрования обычно заканчивается плохо ещё до первой атаки. Разработчик берёт низкоуровневый примитив, самостоятельно придумывает формат сообщения, способ генерации nonce, схему хранения ключей, обработку ошибок и правила ротации, а затем удивляется, почему «шифротекст иногда не расшифровывается». Это не криптография. Это фарминг уязвимостей с нулевым TVL и гарантированным дампом доверия.
Стандартные библиотеки снимают часть нагрузки. В Libsodium для практического шифрования предусмотрены высокоуровневые API вроде crypto_secretbox, crypto_aead и crypto_secretstream: они не только преобразуют данные, но и добавляют тег аутентификации, позволяющий обнаружить изменение шифротекста. В OpenSSL для новых приложений предпочтение отдаётся интерфейсу EVP, а не низкоуровневым вызовам, привязанным к конкретному алгоритму.
Это правильная архитектурная ставка: чем меньше собственной криптографической логики в проекте, тем меньше мест, где можно случайно пропустить обязательный шаг. MITRE даже выделяет отдельный класс ошибок — Missing Cryptographic Step, то есть отсутствие необходимой стадии криптографической процедуры. Иногда защита рушится не из-за сложного эксплойта, а потому, что кто-то не вызвал нужную функцию и решил, что «ну там же AES».
Но библиотека не является готовой системой безопасности. Она не определяет:
- где генерировать ключи и кто имеет к ним доступ;
- как долго ключ считается допустимым;
- как отличать разные сообщения при использовании одного ключа;
- что делать при ошибке проверки аутентификации;
- как удалять секреты из памяти процесса;
- кто и как будет обновлять зависимость после публикации CVE;
- какие данные можно передавать в логи, трассировку и систему мониторинга;
- соответствует ли конкретная сборка требованиям регулятора.
Это разделение часто теряется в маркетинговом шуме. Команда пишет в документации «используем AES-GCM через OpenSSL», и на этом мысленно закрывает тикет безопасности. Но AES-GCM — только часть конструкции. Если nonce повторяется, тег игнорируется, ключ лежит в переменной окружения без контроля доступа, а версия криптографического модуля заморожена в Docker-образе на три года, то красивое название алгоритма не создаёт защитный слой.
Надёжная библиотека уменьшает вероятность ошибки. Она не отменяет ответственность за протокол, ключи и обработку отказов.
Именно здесь проходит граница между «мы используем стандарт» и «система действительно защищает данные». В первом случае достаточно показать зависимость в package.json или список пакетов в образе. Во втором придётся разбирать весь путь секрета — от генерации до уничтожения.
Главная ловушка: nonce нельзя использовать повторно
Одна из самых неприятных причин сбоев криптографических алгоритмов — повторное использование nonce с одним и тем же ключом. Nonce, или одноразовое значение, не обязан быть секретным, но обязан быть уникальным в рамках требований конкретного режима и ключа. Для Libsodium правило сформулировано жёстко: каждая пара «ключ — nonce» должна быть уникальной для каждого сообщения.
Если это условие нарушено, последствия зависят от примитива, но общий сценарий неприятен: конфиденциальность нескольких сообщений может быть разрушена, а атакующий получает возможность формировать дополнительные корректные шифротексты. Для потоковых конструкций повторное использование nonce может раскрывать связь между открытыми текстами. Для AEAD-режимов вроде GCM это уже не просто «два сообщения стали похожи»: может пострадать и механизм аутентификации.
Причины обычно банальны:
1. Nonce генерируется псевдослучайно, но его состояние сбрасывается. После перезапуска сервиса генератор начинает выдавать значения из прежней последовательности.
2. Nonce хранится в памяти одного процесса. В архитектуре с несколькими инстансами каждый экземпляр считает себя единственным и начинает нумерацию с нуля.
3. Nonce строится из timestamp. Два события в одной миллисекунде получают одинаковое значение, а при параллельной обработке эта вероятность перестаёт быть теоретической.
4. Nonce помещают в базу с ошибкой конкурентного доступа. Два воркера читают один номер, оба используют его и оба уверены, что всё прошло по плану.
5. Nonce считают секретом и не сохраняют рядом с шифротекстом. В итоге разработчик пытается «спрятать» его, усложняет восстановление и при миграции данных повторно применяет старое значение.
Уникальность не означает, что nonce нужно шифровать. В большинстве практических схем его можно хранить вместе с шифротекстом, если сам ключ остаётся защищённым. Критично не скрыть nonce, а не допустить его повторения для того же ключа. Но конкретный способ генерации и хранения должен соответствовать выбранному API, а не универсальному рецепту из случайного поста.
Для OpenSSL EVP при использовании AES-GCM длина nonce/IV по умолчанию составляет 12 байт. Это не магическое число, которое можно механически перенести в любую библиотеку и любой протокол. Проблема не в недостаточно длинной строке, а в нарушении требований режима: уникальность, корректная передача, согласованность между сторонами и отсутствие повторов.
Почему «случайный nonce» не всегда спасает
Фраза «мы генерируем nonce через криптографически стойкий генератор случайных чисел» звучит убедительно, пока не появляется несколько миллиардов сообщений, восстановление резервной копии или распределённая система с несколькими источниками энтропии. Случайность снижает риск совпадения, но не превращает ошибочную архитектуру в безопасную.
В ряде схем удобнее использовать счётчик, который гарантированно не повторяется при одном ключе, либо высокоуровневый потоковый API, где часть этой логики уже учтена. Но и здесь библиотека не видит весь жизненный цикл ключа: если ключ восстановили из старого бэкапа, а счётчик откатился, уникальность снова исчезла.
Для блокчейн-инфраструктуры это особенно чувствительно. Сервис может быть формально «некастодиальным», но при этом хранить зашифрованные ключи операторов, секреты релееров, данные приватных ордеров или материалы для генерации доказательств с нулевым разглашением. Повтор nonce в таком контуре не обязательно даст мгновенный взлом кошелька, но создаст канал для анализа сообщений и подделки данных там, где команда рассчитывала на AEAD как на последний рубеж.
Тег аутентификации: отказ — это не предупреждение
Шифрование отвечает за конфиденциальность, но для современных прикладных протоколов этого мало. Получатель должен понимать, что данные не изменили по дороге и что сообщение создано тем, кто владеет ключом. Поэтому используются аутентифицированные схемы: к шифротексту добавляется тег, а при расшифровке библиотека проверяет его корректность.
Ошибка возникает в тот момент, когда код воспринимает неуспешную проверку как незначительную проблему и продолжает работу с выходными данными. В OpenSSL при неудачной аутентификации расшифровку нужно считать неуспешной, а полученные данные не использовать: они могут быть повреждены или подделаны. Это не «расшифровка с низким качеством». Это отказ.
На практике встречаются такие варианты:
- функция возвращает код ошибки, но вызывающий код его игнорирует;
- исключение перехватывается, после чего система подставляет пустую строку или старый кэш;
- тег проверяется после передачи расшифрованных данных в бизнес-логику;
- данные сначала сохраняются во временный файл, а проверка выполняется потом;
- при ошибке аутентификации приложение повторяет операцию с другим ключом, выдавая атакующему удобный oracle.
Последний сценарий особенно неприятен: система начинает рассказывать внешнему наблюдателю, чем именно отличается неверный тег от неверного ключа, повреждённого формата или отсутствующего поля. Чем подробнее такие ответы, тем больше информации получает атакующая сторона. Криптография любит скучные и одинаковые отказы. Разные сообщения об ошибках — это уже лишняя телеметрия для противника.
Низкоуровневые функции, которые только генерируют поток байтов, нельзя автоматически считать готовым шифрованием. В Libsodium crypto_stream сама по себе не обеспечивает обнаружение изменения шифротекста. Если поверх неё не построена корректная схема аутентификации, приложение получает маскировку данных без гарантии целостности. Такой примитив может быть частью конструкции, но не заменяет конструкцию целиком.
| Слой | Что он решает | Где обычно ломается |
|---|---|---|
| Симметричный алгоритм | Скрывает содержимое при наличии ключа | Неподходящий режим, слабый ключ, неверная настройка |
| Nonce или IV | Разделяет сообщения в рамках режима | Повторное использование, сброс счётчика, коллизии |
| Тег аутентификации | Показывает, что данные не изменены | Игнорирование ошибки, поздняя проверка, подмена логики отказа |
| Формат протокола | Определяет, что именно зашифровано и как восстановить данные | Неясные границы полей, повторное шифрование, несовместимые версии |
| Управление ключами | Ограничивает доступ и срок жизни секрета | Ключи в коде, бэкапах, логах и общей переменной окружения |
| Обновление зависимостей | Закрывает известные дефекты библиотек | Замороженные версии, неподтверждённые сборки, пропущенные уведомления |
Ошибка шифрования данных часто находится не в одной строке, а на стыке этих слоёв. Именно поэтому замена OpenSSL на Libsodium, а Libsodium на другой стек редко является автоматическим решением. Если протокол продолжает повторно использовать nonce и игнорировать ошибки, новый бренд библиотеки просто получит старую проблему.
Ключи — место, где заканчивается презентация и начинается реальность
Можно выбрать AES с ключом не менее 128 бит, а предпочтительно 256 бит, использовать AEAD-режим и всё равно хранить секрет так, что любой компрометированный CI-агент заберёт его за пару минут. Криптография не может защитить ключ от того, кто уже получил к нему доступ. В этот момент алгоритм не взломан. Он честно выполнил свою работу — позволил владельцу ключа расшифровать данные.
OWASP прямо не рекомендует зашивать ключи в исходный код или помещать их в системы контроля версий. Но проблема шире, чем случайный коммит с .env:
- ключи попадают в дампы памяти и аварийные отчёты;
- секреты копируются в логи при отладке;
- один ключ используется для базы данных, резервных копий и внутренних API;
- доступ к ключу получает весь кластер, хотя расшифровка нужна одному сервису;
- ротация меняет значение в хранилище, но старые данные остаются без понятной схемы расшифровки;
- ключи и зашифрованные данные хранятся в одном контуре, поэтому компрометация базы сразу превращается в компрометацию секрета.
Для защиты ключевого материала применяют KMS, HSM, vault и другие специализированные механизмы. Это не делает архитектуру безопасной по умолчанию, но меняет экономику атаки: секрет можно выдавать по политике доступа, журналировать операции, ограничивать область применения и разделять права между сервисами.
Здесь полезно не путать три разные задачи:
- Шифрование скрывает содержимое.
- Хеширование строит одностороннее представление данных и не предназначено для обратного восстановления.
- Цифровая подпись подтверждает происхождение и целостность сообщения, но не скрывает его содержимое.
В блокчейн-системах эта путаница регулярно появляется на уровне продукта. Команда говорит о «зашифрованной транзакции», хотя на цепочке лежит хеш, подпись или публичный commitment. В другом случае приватные данные шифруются, но ключ расшифровки передаётся через тот же канал и тем же участникам, от которых якобы требовалась конфиденциальность. Получается не приватность, а дорогой ритуал вокруг публичной базы.
Срок жизни ключа не задаётся одной цифрой
Универсального правила вроде «ротировать ключ каждые 90 дней» нет. Подходящий cryptoperiod зависит от типа ключа, объёма зашифрованных данных, чувствительности информации и модели угроз. Для ключа, которым шифруется большой поток событий, и ключа резервного архива на десять лет нельзя использовать одну и ту же логику.
Ротация также не равна удалению старого ключа. Если после замены ключа приложение больше не может расшифровать легитимный архив, это не безопасность, а операционный инцидент. Нужна схема версионирования: по метаданным зашифрованного объекта система должна понимать, каким ключом и в каком формате его обрабатывать. При этом старые ключи нельзя оставлять доступными всем сервисам «на всякий случай».
Хороший дизайн обычно разделяет:
- ключ шифрования данных и ключ шифрования ключей;
- полномочия на расшифровку и полномочия на ротацию;
- рабочие секреты и резервные копии;
- публичные параметры протокола и приватный ключевой материал;
- данные разных арендаторов, сервисов или доменов риска.
Звучит менее эффектно, чем обещание «военная криптография». Зато именно такие детали переживают первый аудит.
FIPS-валидация не превращает приложение в сейф
В корпоративной и государственной инфраструктуре часто появляется слово FIPS, после которого разговор о конкретных ошибках резко прекращается. Это опасный переключатель мышления. FIPS 140-3 валидирует криптографический модуль, а не автоматически весь продукт, который этим модулем пользуется.
Разница принципиальная. Сертификат может относиться к определённой версии модуля, конкретной операционной среде и заявленной области применения. Если приложение использует другую сборку, меняет конфигурацию, вызывает неподходящий API или неправильно управляет ключами, наличие валидированного модуля не становится индульгенцией.
Не стоит называть «FIPS-сертифицированной» любую библиотеку, в которой есть AES или другой алгоритм из соответствующего набора. Нужно проверять конкретный номер сертификата, статус модуля, версию, операционную среду и границы валидации CMVP. Даже корректный сертификат не подтверждает отсутствие уязвимостей в бизнес-логике приложения, защиту памяти процесса или правильность протокола передачи данных.
Есть и временной аспект. Для новых федеральных систем США после 21 сентября 2026 года активные модули FIPS 140-2 переводятся в Historical List с соответствующими ограничениями; это не означает, что FIPS 140-2 «перестаёт действовать во всех сценариях» в один день. Но для проектов с регуляторными требованиями дата важна: миграцию нельзя откладывать до момента, когда закупка нового модуля внезапно превращается в кризис.
FIPS-статус имеет смысл рассматривать как один из критериев совместимости и контроля, а не как замену инженерной проверке. На аудите всё равно придётся выяснять:
1. Какой именно модуль используется в продакшене?
2. Совпадает ли его версия с указанной в сертификате?
3. Через какой API вызывается криптографическая операция?
4. Проверяется ли ошибка аутентификации до передачи данных в приложение?
5. Кто имеет доступ к ключам и журналам операций?
6. Как устроены обновление, отзыв и ротация секретов?
7. Не нарушает ли собственный код заявленную границу модуля?
Сертификат подтверждает ограниченное утверждение. Команды любят пересказывать его как универсальное. Это и есть регуляторный арбитраж, только вместо токеномики — документация.
FIPS подтверждает свойства криптографического модуля. Он не подписывает ваш протокол, Docker-образ и разработчика, который проглотил код возврата.
Постквантовый переход: ML-KEM не исправит старый код
13 августа 2024 года NIST опубликовал FIPS 203 — стандарт механизма инкапсуляции ключей ML-KEM. В документе определены три набора параметров: ML-KEM-512, ML-KEM-768 и ML-KEM-1024. Это важный шаг для перехода к постквантовой криптографии, но не кнопка «защитить всё от квантовых атак».
ML-KEM решает конкретную задачу — согласование или инкапсуляцию ключевого материала в условиях новой модели угроз. Он не устраняет повторное использование nonce в симметричном шифровании, не защищает ключи в памяти, не исправляет ошибку проверки подписи и не делает безопасным протокол с неправильной обработкой сообщений.
Есть и практический риск переходного периода. Организации начинают добавлять постквантовый алгоритм в презентацию, не определив:
- какие данные имеют длительный срок конфиденциальности;
- где требуется гибридная схема;
- как обновлять клиентов и серверы без потери совместимости;
- как хранить и ротировать новые типы ключей;
- какие протоколы используют устаревшие криптографические предположения;
- как проверять размер сообщений, производительность и отказоустойчивость.
Для блокчейна вопрос ещё сложнее. Публичные данные цепочки не станут приватными после замены одного алгоритма. Если конфиденциальность строится на шифровании off-chain, commitment-схемах, доказательствах с нулевым разглашением или защищённых каналах между участниками, менять нужно не только криптографический примитив, но и формат доказательств, управление ключами, маршрутизацию сообщений и процедуру восстановления.
Нельзя утверждать, что ML-KEM полностью защищает систему от квантовых атак. Он защищает определённый криптографический механизм при корректной реализации и использовании. Вокруг него остаются имплементационные ошибки, проблемы протокола, компрометация конечной точки и банальная кража ключа. Квантовый компьютер здесь не обязателен: иногда достаточно доступа к переменной окружения.
Как искать причину сбоя, не меняя стек наугад
Отладка криптографических модулей должна начинаться не с выбора нового алгоритма, а с фиксации инвариантов. Нужно понять, какое свойство должно сохраняться на каждом этапе: секретность, целостность, аутентичность, уникальность nonce, доступность расшифровки для легитимного получателя.
Практическая последовательность выглядит так:
1. Зафиксировать криптографическую задачу. Что требуется защитить — содержимое, происхождение сообщения, целостность, обмен ключами или всё сразу? Если задача сформулирована как «зашифровать строку», проект уже движется вслепую.
2. Инвентаризировать примитивы и API. Нужно увидеть не только название библиотеки, но и конкретные функции, режимы, параметры, версии и платформу сборки. Низкоуровневый вызов часто выглядит компактнее, но переносит критические решения в код приложения.
3. Проверить жизненный цикл nonce. Как он создаётся, где хранится, как передаётся, что происходит после рестарта, отката базы и горизонтального масштабирования? Ответ «он случайный» недостаточен.
4. Проверить путь ошибки аутентификации. При неверном теге данные должны быть отброшены. Не закэшированы, не переданы дальше, не записаны во временный файл и не возвращены вызывающей стороне как «частично восстановленные».
5. Разобрать управление ключами. Откуда берётся ключ, кто может его прочитать, сколько объектов им зашифровано, как выполняется ротация и что происходит со старыми версиями.
6. Проверить формат протокола. В шифротексте должны быть однозначно представлены необходимые метаданные: версия формата, идентификатор ключа, nonce и аутентифицированные дополнительные данные, если они используются. Формат не должен зависеть от догадок парсера.
7. Проверить обновление зависимостей. Надёжная библиотека с устаревшей версией — это не надёжная библиотека, а исторический артефакт с хорошей репутацией.
8. Смоделировать активного атакующего. Повреждение байта, перестановка сообщений, повторная отправка, подмена метаданных, откат ключа, восстановление старого бэкапа — это полезнее, чем очередной тест на успешное шифрование и расшифровку собственного сообщения.
Отдельно стоит проверять логи. Система должна фиксировать факт отказа, но не обязана раскрывать атакующему, на каком именно этапе он произошёл. Внутренний журнал может содержать технический идентификатор события, а внешний ответ — оставаться одинаковым для неверного ключа, повреждённого тега и некорректного формата, если это соответствует модели угроз.
Так стоит ли доверять стандартным библиотекам
Да — но только в правильном смысле.
Стандартной библиотеке стоит доверять больше, чем самописной реализации алгоритма. У неё выше шанс пройти профессиональное тестирование, получить исправления и использовать проверенные высокоуровневые конструкции. Для практического шифрования разумнее выбирать API, который сразу учитывает аутентификацию, чем собирать схему из разрозненных низкоуровневых деталей.
Но библиотеке нельзя доверять вместо архитектуры. Она не знает, что ключ попал в Git, что nonce откатился вместе с резервной копией, что ошибка проверки тега была проглочена, а расшифрованные данные ушли в публичный лог. Она не компенсирует неправильный протокол и не превращает FIPS-валидацию модуля в сертификацию всего приложения.
Мой практический вердикт такой: менять криптографический стек нужно только после доказательства, что проблема действительно в библиотеке или её конфигурации. В большинстве расследований первым подозреваемым окажется не AES, не OpenSSL и не Libsodium, а код, который неправильно связал примитив с жизненным циклом данных.
Сильная криптография — это не самый длинный ключ в презентации и не логотип стандарта в архитектурной схеме. Это скучная дисциплина, где nonce не повторяется, тег проверяется до использования данных, ключи живут по понятным правилам, ошибки не замалчиваются, а сертификаты не расширяют свою область действия силой маркетингового желания.
Остальное — экзит-ликвидность для аудиторов.