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

Хеширование: SHA-256/MD5 через QCryptographicHash, соль и медленные хеши для паролей (bcrypt/Argon2)

Введение

Хеширование — основа множества задач: проверка целостности файлов, идентификация дублирующихся данных, и (с важными оговорками, разбираемыми ниже) хранение паролей. В статье разберём QCryptographicHash для общего хеширования и принципиально другой класс алгоритмов — медленные, специально разработанные для паролей хеш-функции (bcrypt, Argon2), не входящие в стандартный набор Qt.

Концепция

Обычные криптографические хеш-функции (SHA-256, и тем более устаревший, криптографически скомпрометированный MD5) спроектированы быть быстрыми — это хорошо для проверки целостности файлов, но плохо для хранения паролей, поскольку та же скорость позволяет атакующему быстро перебирать (брутфорсить) огромное количество вариантов паролей против похищенной базы хешей. Bcrypt и Argon2 — специально замедленные (по конфигурируемому параметру «стоимости» вычисления) хеш-функции, разработанные именно для хранения паролей, делающие массовый перебор вариантов паролей вычислительно намного дороже для атакующего при сохранении приемлемой скорости для легитимной, единичной проверки пароля пользователя.

Пример кода: Хеширование

// QCryptographicHash — подходит для проверки целостности, НЕ для хранения паролей
#include <QCryptographicHash>
#include <QFile>

QByteArray computeFileSha256(const QString &filePath)
{
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return QByteArray();
    }

    QCryptographicHash hash(QCryptographicHash::Sha256);
    // Потоковое добавление данных — для больших файлов, не загружаемых целиком в память
    if (hash.addData(&file)) {
        return hash.result();
    }
    return QByteArray();
}
// Хранение паролей через Argon2 (через библиотеку libargon2, НЕ через QCryptographicHash) —
// демонстрация концепции; в реальном проекте используется готовая обёртка над libargon2
#include <argon2.h>

QByteArray hashPasswordArgon2(const QString &password, const QByteArray &salt)
{
    const uint32_t timeCost = 3;        // количество итераций — параметр "стоимости" по времени
    const uint32_t memoryCost = 65536;  // память в килобайтах (64 МБ) — затрудняет атаки через GPU
    const uint32_t parallelism = 1;

    QByteArray hashOutput(32, 0);
    QByteArray passwordBytes = password.toUtf8();

    int result = argon2id_hash_raw(
        timeCost, memoryCost, parallelism,
        passwordBytes.constData(), passwordBytes.size(),
        salt.constData(), salt.size(),
        hashOutput.data(), hashOutput.size()
    );

    return (result == ARGON2_OK) ? hashOutput : QByteArray();
}

QByteArray generateRandomSalt()
{
    // QRandomGenerator::system() — криптографически подходящий источник, статья 378
    QByteArray salt(16, 0);
    for (int i = 0; i < salt.size(); ++i) {
        salt[i] = static_cast<char>(QRandomGenerator::system()->bounded(256));
    }
    return salt;
}
// Проверка пароля при входе — повторное вычисление хеша с той же солью и сравнение
bool verifyPassword(const QString &enteredPassword, const QByteArray &storedHash, const QByteArray &storedSalt)
{
    QByteArray computedHash = hashPasswordArgon2(enteredPassword, storedSalt);
    return computedHash == storedHash; // сравнение байт массивов — см. подводные камни про timing attack
}

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

hash.addData(&file) показывает потоковое хеширование непосредственно из файла, не требующее предварительной загрузки всего содержимого в QByteArray — критично важно для больших файлов, где загрузка целиком могла бы исчерпать доступную память. argon2id_hash_raw() с параметрами timeCost/memoryCost/parallelism показывает настраиваемую «стоимость» вычисления Argon2 — эти параметры явно подбираются разработчиком как компромисс между безопасностью (выше стоимость — труднее перебор) и приемлемым временем ожидания для легитимного пользователя при входе (обычно целятся в десятки-сотни миллисекунд на современном оборудовании). generateRandomSalt() демонстрирует обязательное использование уникальной, случайной «соли» для каждого пароля — без соли одинаковые пароли разных пользователей дали бы одинаковый хеш, что упрощает атаку через предварительно вычисленные таблицы (rainbow tables).

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

  • Использование QCryptographicHash с SHA-256 (или тем более MD5) для хранения паролей вместо специально разработанной медленной функции (bcrypt/Argon2) — это одна из самых распространённых и серьёзных ошибок в практической безопасности приложений: быстрые хеш-функции, даже криптографически стойкие как SHA-256, не дают защиты от массового перебора похищенной базы хешей паролей, поскольку именно их скорость (желательная для проверки целостности файлов) делает перебор миллиардов вариантов паролей в секунду вычислительно доступным на современном оборудовании, особенно с использованием GPU.
  • Хранение паролей без соли вовсе, или с одной и той же, общей для всех пользователей солью — оба варианта существенно облегчают атаку: отсутствие соли позволяет использовать предварительно вычисленные таблицы хешей популярных паролей, а общая соль для всех пользователей сводит атаку фактически к той же ситуации, что и полное отсутствие соли, поскольку таблицу можно предварительно вычислить именно под эту конкретную, известную атакующему соль.
  • Сравнение хешей паролей через обычный, «быстрый выход» оператор сравнения (== для строк/массивов байт) в коде, чувствительном к атакам по времени (timing attack) — хотя для большинства веб/desktop-приложений с сетевой задержкой это не самая критичная угроза по сравнению с другими, более серьёзными слабостями, для систем с особо высокими требованиями к безопасности (банковские, государственные системы) сравнение секретных значений рекомендуется выполнять через специальные, защищённые от такого анализа времени выполнения функции (constant-time comparison), а не обычный быстрый оператор сравнения, прекращающий проверку при первом несовпадающем байте.
  • Слишком низкие параметры «стоимости» вычисления (timeCost, memoryCost) ради ускорения работы приложения, без понимания, что это прямо снижает защиту от перебора — параметры стоимости должны выбираться с учётом современных вычислительных мощностей потенциального атакующего (которые со временем растут, требуя периодического пересмотра выбранных значений для долгоживущих систем), а не исключительно ради минимизации времени ответа сервера или отзывчивости приложения.