Введение
Качество случайных чисел критично для криптографии — слабый или предсказуемый генератор полностью компрометирует безопасность любой системы, использующей его для генерации ключей, IV (статья 373), солей (статья 372), или иных секретных значений, независимо от того, насколько стойки сами криптографические алгоритмы. В статье разберём разницу между обычным, «быстрым» генератором случайных чисел и криптографически безопасным, и где какой уместен.
Концепция
Обычный псевдослучайный генератор (PRNG, как классический rand()/большинство случаев использования QRandomGenerator::global()) детерминирован — зная внутреннее состояние (seed) и алгоритм, можно точно предсказать всю последующую последовательность чисел, что совершенно неприемлемо для криптографических целей, но вполне приемлемо для большинства неcекьюрити-применений (случайное число в игре, случайная задержка для повторной попытки соединения). Криптографически безопасный генератор случайных чисел (CSPRNG) спроектирован так, чтобы предсказание следующего значения было вычислительно невозможным даже при знании всех предыдущих сгенерированных значений — QRandomGenerator::system() в Qt и RAND_bytes() в OpenSSL (уже встречавшийся в статьях 373-374) оба относятся к этой, криптографически безопасной категории.
Пример кода: Генерация криптографически безопасных
// Сравнение: обычный генератор (НЕ для криптографии) против криптографически безопасного
#include <QRandomGenerator>
void demonstrateRandomGeneratorTypes()
{
// QRandomGenerator::global() — подходит для игр, случайных задержек, НЕ для криптографии
quint32 gameRandomValue = QRandomGenerator::global()->bounded(100);
// QRandomGenerator::system() — криптографически безопасный, подходит для ключей/IV/солей
QByteArray cryptoKey(32, 0);
for (int i = 0; i < cryptoKey.size(); ++i) {
cryptoKey[i] = static_cast<char>(QRandomGenerator::system()->bounded(256));
}
}
// Более эффективный способ генерации блока криптографически безопасных случайных байт —
// через generate(), а не побайтовый цикл bounded()
QByteArray generateSecureRandomBytes(int length)
{
QByteArray result(length, 0);
QRandomGenerator::system()->fillRange(
reinterpret_cast<quint32 *>(result.data()), length / sizeof(quint32));
return result;
}
// Эквивалент через OpenSSL — RAND_bytes(), используемый напрямую в статьях 373-374
#include <openssl/rand.h>
QByteArray generateSecureRandomBytesOpenSsl(int length)
{
QByteArray result(length, 0);
if (RAND_bytes(reinterpret_cast<unsigned char *>(result.data()), length) != 1) {
throw std::runtime_error("Не удалось сгенерировать криптографически безопасные случайные данные");
}
return result;
}
Пояснения к коду
Сравнение QRandomGenerator::global() и QRandomGenerator::system() в первом примере явно демонстрирует ключевое архитектурное решение Qt — оба интерфейса доступны через один и тот же класс с похожим API, но используют принципиально разные источники энтропии внутри: global() оптимизирован для скорости и общего использования, а system() обращается к системному источнику энтропии операционной системы (/dev/urandom на Linux, CryptGenRandom/её современный аналог на Windows), специально предназначенному для криптографически чувствительных применений. generateSecureRandomBytes() показывает более эффективный способ генерации блока случайных данных через fillRange() целиком, а не побайтовым циклом bounded(), что особенно важно для больших объёмов случайных данных (хотя для криптографических ключей разумного размера разница на практике обычно незначительна).
Подводные камни
- Использование
QRandomGenerator::global()(или, что хуже, классическогоrand()/std::rand()) для генерации криптографических ключей, IV, солей или других секретных значений — это фундаментальная ошибка, полностью компрометирующая безопасность всей системы независимо от того, насколько стойки сами используемые алгоритмы шифрования; для любого значения, используемого в криптографическом контексте, должен использоваться исключительно криптографически безопасный источник (QRandomGenerator::system()илиRAND_bytes()OpenSSL), без исключений, даже для «не самых критичных» частей системы. - Предположение, что криптографически безопасный генератор всегда доступен и работает мгновенно, без учёта того, что в редких случаях (особенно на встраиваемых системах сразу после загрузки, до накопления достаточной энтропии операционной системой) обращение к системному источнику энтропии теоретически может временно блокироваться в ожидании достаточного количества накопленной случайности — для большинства десктопных/серверных систем эта проблема практически не проявляется на современных ОС, но для специфичных embedded-сценариев (Raspberry Pi/STM32-циклы) стоит знать о потенциальной возможности такой задержки сразу после холодного старта системы.
- Повторное использование одного и того же сгенерированного случайного значения для нескольких разных криптографических целей, например использование одного и того же случайного значения и как ключа, и как IV для того же шифрования — каждое криптографическое назначение (ключ, IV, соль, nonce) требует собственного, независимо сгенерированного случайного значения, и переиспользование значений между разными ролями нарушает математические предположения, на которых основана безопасность используемых алгоритмов.
- Недостаточная длина генерируемого случайного значения для конкретной криптографической цели (например, генерация соли длиной всего 4 байта, что значительно меньше рекомендуемых для современных задач 16+ байт) — длина случайного значения должна соответствовать рекомендациям конкретного алгоритма/назначения (32 байта для ключа AES-256, статья 373; 12 байт для IV режима GCM; 16+ байт для соли паролей, статья 372), а не выбираться произвольно без учёта этих рекомендаций.