Различные статьи по Qt и c++

Распространённые ошибки криптографии в приложениях: типичные уязвимости и как их избежать

Введение

В этой предпоследней статье цикла собираем воедино и систематизируем распространённые ошибки, многие из которых уже упоминались по отдельности в подводных камнях предыдущих статей — рассматриваем их как единый, целостный чек-лист практической криптографической гигиены для разработчика Qt/C++ приложений.

Концепция

Большинство реальных криптографических уязвимостей в прикладном коде возникают не из слабости самих алгоритмов (AES, RSA, SHA-256 — все криптографически стойки при правильном использовании), а из ошибок их применения — неправильного управления ключами, повторного использования значений, которые должны быть уникальными, и неполной/некорректной проверки результатов криптографических операций. Систематический подход к избежанию этих ошибок важнее глубокого понимания математики самих алгоритмов для большинства практических Qt-приложений.

Пример кода: Распространённые ошибки криптографии

Чек-лист распространённых ошибок (обобщение подводных камней статей 371-378):

1. [ ] Используется ли проверенная библиотека (OpenSSL) вместо самостоятельной реализации алгоритмов?
2. [ ] Используется ли медленная хеш-функция (Argon2/bcrypt) для паролей, а не SHA-256/MD5?
3. [ ] Уникальна ли соль для каждого пароля (не общая для всех пользователей)?
4. [ ] Уникален ли IV для каждой отдельной операции AES-GCM шифрования?
5. [ ] Проверяется ли результат расшифровки/проверки подписи перед использованием данных?
6. [ ] Используется ли OAEP-padding для RSA-шифрования (а не устаревший PKCS1v15)?
7. [ ] Отсутствуют ли в коде вызовы ignoreSslErrors() без явных, обоснованных причин?
8. [ ] Хранятся ли секреты в системном хранилище, а не в открытом конфигурационном файле?
9. [ ] Используется ли криптографически безопасный генератор (system(), не global()) для ключей?
10. [ ] Зафиксирована ли минимально допустимая версия TLS (1.2+) явно в конфигурации?
// Пример типичной композитной ошибки — несколько проблем одновременно в одном фрагменте кода
// (НЕ повторяйте этот код — приведён исключительно для иллюстрации проблем)
QByteArray badPracticeExample(const QString &password, const QByteArray &data)
{
    // Проблема 1: быстрый хеш для пароле-производного ключа вместо PBKDF2/Argon2 (статья 372)
    QByteArray key = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);

    // Проблема 2: фиксированный, не случайный IV — нарушение требования уникальности (статья 373)
    QByteArray fixedIv = QByteArray::fromHex("000000000000000000000000");

    // ... шифрование с этим ключом и IV ...
    return key; // в реальном коде здесь было бы шифрование с обеими указанными проблемами
}

// Корректная версия, устраняющая обе проблемы
QByteArray goodPracticeExample(const QString &password, const QByteArray &salt)
{
    // PBKDF2 или Argon2 (статья 372) вместо прямого SHA-256 для производного от пароля ключа
    QByteArray key(32, 0);
    PKCS5_PBKDF2_HMAC(password.toUtf8().constData(), password.toUtf8().size(),
                      reinterpret_cast<const unsigned char *>(salt.constData()), salt.size(),
                      100000, EVP_sha256(), key.size(), reinterpret_cast<unsigned char *>(key.data()));

    // Случайный, уникальный IV для каждого использования (статья 378)
    QByteArray iv = generateSecureRandomBytesOpenSsl(12);

    return key; // в реальном коде далее следовало бы корректное AES-GCM шифрование с этими key/iv
}

Пояснения к коду

Чек-лист собирает в одном месте основные критерии, разбросанные по подводным камням предыдущих восьми статей цикла — практическая ценность такого систематизированного списка в том, что он может применяться как часть код-ревью для любого кода, работающего с криптографией, не требуя каждый раз заново вспоминать все детали отдельных статей. Сравнение badPracticeExample()/goodPracticeExample() явно демонстрирует две конкретные, очень частые в реальном коде проблемы рядом друг с другом — PKCS5_PBKDF2_HMAC() (альтернатива Argon2 из статьи 372, чуть менее современная, но широко распространённая и доступная непосредственно в OpenSSL без дополнительных зависимостей) с явным количеством итераций (100000 — параметр стоимости, аналогичный timeCost Argon2) против простого, быстрого хеша, и явно сгенерированный случайный IV против зафиксированного, повторяющегося значения.

Подводные камни

  • Применение чек-листа формально, «для галочки», без понимания, почему каждый конкретный пункт важен — механическая проверка пунктов списка без реального понимания контекста может пропустить специфичные для конкретного приложения нюансы, не охваченные общим списком; чек-лист — полезный инструмент для систематизации уже понятых принципов, а не замена реального понимания криптографии, разобранного в предыдущих статьях цикла.
  • Самоуверенность после прохождения базового чек-листа, создающая ложное ощущение полной защищённости приложения — реальная безопасность системы зависит от значительно большего числа факторов, чем только корректное использование криптографических примитивов (безопасность остальной кодовой базы, инфраструктуры, процессов разработки, человеческого фактора), и криптография — лишь один, хотя и важный, компонент общей безопасности приложения, не гарантирующий безопасность системы в целом сам по себе.
  • Откладывание привлечения специализированного аудита безопасности для систем с действительно высокими ставками (финансовые приложения, медицинские данные, системы с критичными требованиями к конфиденциальности) в надежде, что собственное соблюдение общих рекомендаций цикла полностью достаточно — для систем с серьёзными последствиями возможной компрометации профессиональный, независимый аудит безопасности (включающий специалистов с глубокой криптографической экспертизой) остаётся ценным дополнением к соблюдению общих практических рекомендаций, разобранных в этом учебном цикле статей.
  • Применение всех разобранных техник изолированно, без понимания общей архитектуры безопасности приложения как единой системы — отдельно корректное хеширование паролей, отдельно корректное TLS-соединение, и отдельно корректное хранение секретов не гарантируют общую безопасность приложения, если, например, эти отдельно корректные компоненты взаимодействуют друг с другом неправильным, открывающим уязвимость способом; безопасность стоит рассматривать как свойство системы в целом, а не сумму независимо корректных частей.