Учимся правильно работать с классом SecureRandom

Само название класса подразумевает криптостойкость генерируемых случайных чисел (то есть устойчивость к взлому, невозможность предсказать следующее число по предыдущим за разумное время). Но так ли это?

Сперва разберёмся с тем, что же для нас умеет генерировать SecureRandom.

Сам класс наследуется от всем известного класса Random и добавляет некоторые свои методы. В частности перегружает метод setSeed для установки в качестве порождающего элемента (англ. seed) массива байт. Как мы увидим в дальнейшем, “случайность” (или секретность) seed является краеугольным камнем для обеспечения криптографической стойкости генераторов ПСЧ.

Ну а мы попробуем сразу использовать его, не разбираясь в деталях. Один из примеров использования SecureRandom для обеспечения web-безопасности, это генерация симметричного ключа шифрования. В интернете можно найти примеры типа такого кода:

SecureRandom secureRandom = new SecureRandom();

KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
int keyBitSize = 256;
keyGenerator.init(keyBitSize, secureRandom);

И… этот ключ может оказаться ненадёжным.

Что же определяет надёжность этого ключа, его криптостойкость? Конечно, объект класса SecureRandom. И что там под капотом? Что за числа он нам нагенерирует?

В отличии от своего родителя, SecureRandom имеет в арсенале несколько алгоритмов генерации ПСЧ. Причём набор алгоритмов может быть разным в зависимости от версии JDK и платформы. Вот что говорит официальная документация Oracle для JDK 11 по этому поводу:

OS Algorithm Name Provider Name
Solaris1. PKCS11SunPKCS11
2. NativePRNGSUN
3. DRBGSUN
4. SHA1PRNGSUN
5. NativePRNGBlockingSUN
6. NativePRNGNonBlockingSUN
Linux1. NativePRNGSUN
2. DRBGSUN
3. SHA1PRNGSUN
4. NativePRNGBlockingSUN
5. NativePRNGNonBlockingSUN
macOS1. NativePRNGSUN
2. DRBGSUN
3. SHA1PRNGSUN
4. NativePRNGBlockingSUN
5. NativePRNGNonBlockingSUN
Windows1. DRBGSUN
2. SHA1PRNGSUN
3. Windows-PRNGSunMSCAPI

Выясним, какие алгоритмы используются в Linux и Windows по умолчанию:

SecureRandom secureRandom = new SecureRandom();
System.out.println("Алгоритм по умолчанию: " + secureRandom.getAlgorithm());
System.out.println("Провайдер: " + secureRandom.getProvider());

System.out.println(Security.getAlgorithms("SecureRandom"));

Linux (OpenJDK 11):

Алгоритм по умолчанию: NativePRNG
Провайдер: SUN version 11
[DRBG, SHA1PRNG, NATIVEPRNGBLOCKING, NATIVEPRNGNONBLOCKING, NATIVEPRNG]

Windows (Oracle JDK 11):

Алгоритм по умолчанию: DRBG
Провайдер: SUN version 11
[DRBG, WINDOWS-PRNG, SHA1PRNG]

Linux и Windows по умолчанию используют разные алгоритмы. Причём алгоритм NativePRNG есть во всех ОС, кроме Windows, а алгоритм DRBG (на самом деле это не один алгоритм, а целое семейство алгоритмов) есть во всех ОС.

Также во всех ОС есть алгоритм SHA1PRNG, считающийся устаревшим, однако DRBG появился лишь в JDK 9. А последней 32-разрядной была JDK 8 — и в ней, конечно, присутствовал алгоритм SHA1PRNG (тоже во всех ОС). Так что уделим внимание и этому алгоритму, наравне с DRBG и NativePRNG.

Кстати, постоянно встречающаяся в именах алгоритмов аббревиатура PRNG обозначает не что иное как ГПСЧ (pseudorandom number generator). Так что NativePRNG можно перевести как “родной” ГПСЧ, а родной он потому что использует устройства самой ОС (/dev/urandom или /dev/random).

Начнём с семейства алгоритмов DRBG (детерминированных генераторов случайных бит)

По двум причинам:

  • это относительно свежие технологии, предложенные в стандарте  NIST SP 800-90A Revision 1 и способные обеспечить (пока обратное не доказано) высочайшую степень криптостойкости;
  • в Java они реализованы для всех платформ.

Реализация DRBG в Java включает на данный момент три механизма генерации криптографически стойких ПСЧ:

  • Hash_DRBG (на основе хеш-функций),
  • HMAC_DRBG (на основе хеш-кода аутентификации сообщений),
  • CTR_DRBG (на основе блочных шифров в режиме счетчика).

Рекомендуется использовать первые два, поскольку для них существуют доказательства безопасности (криптографической стойкости), а для третьего, CTR_DRBG, при определённых условиях, существует риск взлома. Механизм Hash_DRBG обеспечивает более быструю генерацию чем HMAC_DRBG.

Но это ещё не всё. Каждый из механизмов умеет работать с разными, но специфичными для этих механизмов алгоритмами.

Таким образом, казалось бы, простая задача по указанию алгоритма для объекта SecureRandom превратилась в такую:

  1. указать алгоритм (DRBG);
  2. указать механизм (на выбор: Hash_DRBG, HMAC_DRBG, CTR_DRBG)
  3. указать алгоритм для выбранного механизма.

И в таком случае мы будем точно знать каким алгоритмом генерируются наши случайные числа и является ли эта генерация криптографически стойкой. В противном случае нам придётся полагаться на выбор конкретного JDK под конкретную платформу.

На практике это выглядит так:

Security.setProperty("securerandom.drbg.config", "Hash_DRBG, SHA-512");
SecureRandom secureRandom = SecureRandom.getInstance(
       "DRBG",
       DrbgParameters.instantiation(256, PR_AND_RESEED, null)
);

В этом примере мы указали, что следует использовать механизм Hash_DRBG на алгоритме SHA-512 с уровнем криптостойкости 256. Уровень криптостойкости N обозначает, что для для взлома алгоритма потребуется 2^N операций.

Аналогичные примеры для HMAC_DRBG и CTR_DRBG выглядят так:

Security.setProperty("securerandom.drbg.config", "HMAC_DRBG, SHA-384");
SecureRandom secureRandom = SecureRandom.getInstance(
       "DRBG",
       DrbgParameters.instantiation(128, RESEED_ONLY, null)
);

Здесь указано, что следует использовать механизм HMAC_DRBG на алгоритме SHA-384 с уровнем криптостойкости 128.

Security.setProperty("securerandom.drbg.config", "CTR_DRBG, AES-128");
SecureRandom secureRandom = SecureRandom.getInstance(
       "DRBG",
       DrbgParameters.instantiation(112, NONE, null)
);

Здесь указано, что следует использовать механизм CTR_DRBG на алгоритме AES-128 с уровнем криптостойкости 112.

Во всех примерах использовалась статическая функция DrbgParameters.instantiation, которая позволяет конкретизировать параметры стойкости алгоритма. Кроме первого параметра, обозначающего уровень криптостойкости, в ней присутствуют ещё два параметра.

Первый — это enum DrbgParameters.Capability со значениями:

  • PR_AND_RESEED: повысить устойчивость к прогнозированию и изменять seed в процессе генерации элементов,
  • RESEED_ONLY: изменять seed в процессе генерации элементов,
  • NONE: не делать ничего из вышеперечисленного.

Однако значения эти являются минимальным требованием, т.е. если установлено требование NONE, то реальное значение может оказаться и NONE, и RESEED_ONLY, и PR_AND_RESEED. А если установлено RESEED_ONLY, то реальные значения могут быть RESEED_ONLY или PR_AND_RESEED.

Например код:

SecureRandom secureRandom = SecureRandom.getInstance(
       "DRBG",
       DrbgParameters.instantiation(112, NONE, null)
);
System.out.println(secureRandom);

даёт такой результат:

Hash_DRBG,SHA-256,112,reseed_only

Вместо указанного NONE реализация алгоритма решила использовать возможность RESEED_ONLY.

Второй параметр — это «personalization string», а точнее массив байт, в котором указаны какие-то выбранные нами числа. Эта “строка персонализации” также усложняет взлом генератора.

Маленький экскурс в историю. NIST (Национальный институт стандартов и технологий) выпустил в 2006 году специальную публикацию NIST SP 800-90  — рекомендацию для генерации случайных чисел с использованием детерминированных генераторов случайных бит. И, кроме вышеперечисленных трёх механизмов (Hash_DRBG, HMAC_DRBG, CTR_DRBG), в ней присутствовал четвёртый: Dual_EC_DRBG.

Утверждалось, что все алгоритмы криптографически стойкие (поскольку обратного не было доказано). Однако с четвёртым вышел казус. Основным инициатором его стандартизации выступило АНБ, и, как позже было показано,

Dual_EC_DRBG содержал клептографический бэкдор.

Т.е. АНБ, вероятно, имело возможность легко взламывать данный механизм.

Так что в 2015 году вышла новая специальная публикация NIST SP 800-90A Revision 1 в которой осталось только три рекомендованных механизма, а Dual_EC_DRBG был исключён.

Впрочем, за остальные механизмы (особенно Hash_DRBG и HMAC_DRBG) можно не беспокоиться. Судя по всему они действительно могут обеспечить высшую степень криптографической стойкости. Кстати, одним из алгоритмов поддерживаемых Hash_DRBG является SHA-256, который используется для майнинга биткоина.

Пару слов об источнике энтропии.

Во всех примерах, которые были выше по тексту мы совершенно не заботились о seed. Откуда же брались числа для него?

Во всех ОС есть какой-то системный источник энтропии. Энтропия есть мера неопределённости. Поэтому источником энтропии при генерации ПСЧ называют источник, выдающий последовательность случайных чисел. Позвольте… но если есть такой источник, то зачем нам ГПСЧ?

Дело в том, что источники энтропии ОС привязаны к “железу” компьютера: мышь и клавиатура (действия людей считаются слабо предсказуемыми), процессор (состояние кэша), и т.д. И поток данных из этих источников не всегда способен удовлетворить потребности в большом количестве случайных чисел. Однако, отлично подходит для генерации seed. С одной последовательностью seed мы можем сгенерировать тем же Hash_DRBG с SHA256, скажем, 10 000 000 случайных байт, а потом сменить seed (для безопасности).

Если потенциальный хакер узнает seed, то взлом (предсказание следующих значений ГПСЧ по нескольким предыдущим) может оказаться чрезвычайно простым. Именно поэтому случайность, секретность и размер seed имеют большое значение для обеспечения криптостойкости нашего ГПСЧ. Кстати, смена seed во всех алгоритмах которые мы рассматриваем, обеспечивает криптостойкость следующих генерируемых чисел даже в случае компрометации ГПСЧ на предыдущем seed. Т.е. это дополнительная (и важная!) мера защиты.

Какие же есть источники энтропии у разных ОС

Все unix-подобные системы, в том числе macOS, Android имеют “устройства” для генерации СЧ (/dev/random) и ПСЧ (/dev/urandom). По своей реальной “секретности” они похожи, несмотря на многочисленные утверждения о предпочтительности /dev/random. На практике дело обстоит так. Для подавляющего большинства задач, требующих криптостойкости, предпочтительнее использовать /dev/urandom, поскольку /dev/random является блокирующим процесс. Т.е. не может выдать столько байт, сколько у него запрашивают. Впрочем, для генерации seed вполне подходит и /dev/random, а на основе /dev/urandom в этих ОС есть те самые “родные”, которые Native… ГПСЧ. Мы рассмотрим их ниже.

В случае с Windows ситуация сложнее. Ибо в качестве источника энтропии используется CryptoApi. Сам CryptoApi в качестве источников использует криптопровайдеров, в том числе новую директиву RdRand процессора Intel. С другой стороны, известно, что seed для генерации этих данных прилежно пишется в реестр. А значит их “непредсказуемость” под вопросом. Кроме того, никаких гарантий того что Microsoft не сотрудничает с тем же АНБ или кем-то ещё — нет. Так что CryptoApi как источник для большинства случаев подойдёт, но для обеспечения реальной безопасности — нет.

С источниками энтропии на основе /dev/urandom и/dev/random ситуация схожая. Ведь сами источники — это файлы, открытые для всех имеющих права доступа к ним.

И что же делать если нам нужна действительно мощная защита, а не агенты АНБ или ФСБ? Один из выходов — это использование сервисов для сбора и накопления энтропии. Такие решения есть, например, для unix-подобных систем это haveged или EGD, а для Windows EGDW, позволяющий забирать информацию через порт. Есть и более сложные решения, типа аппаратных источников энтропии, или создания своего сетевого источника.

В остальных случаях, когда не требуется высшая степень защиты информации, вполне подойдут стандартные источники энтропии ОС.

В Java можно задать источник энтропии через свойство securerandom.source. Тогда SecureRandom будет устанавливать и переустанавливать seed самостоятельно. Делается это так:

Security.setProperty("securerandom.source", "file:/dev/urandom");

Посмотреть значение текущее значение этого свойства можно так:

System.out.println(Security.getProperty("securerandom.source"));

Результат:

file:/dev/urandom

Для Windows, если в securerandom.source установлено значение “file:/dev/random” или “file:/dev/urandom”, в качестве источника энтропии будет выбран CryptoApi.

Если же нам нужна высшая степень криптостойкости ГПСЧ, то формировать seed придётся самостоятельно, равно как и следить за его своевременным изменением.

Предположим, что вопрос с источником энтропии для нашей задачи мы решили. Следующий вопрос — размер seed. Вот что рекомендует NIST:

Таким образом, по алгоритмам для механизмов Hash_DRBG и HMAC_DRBG для части алгоритмов рекомендуется seed размером 440 бит — это массив из 55 байт, а для другой части 888 бит — это массив из 111 байт.

Следующий алгоритм, который мы рассмотрим, это SHA1PRNG

По рекомендации NIST размер seed для него должен быть 440 бит. Т.е. это массив из случайных 55 байт.

С ним всё просто:

SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(SecureRandom.getSeed(55));

Алгоритм хоть и старый, но достаточно надёжный и быстрый.

Инициализационное значение seed устанавливается автоматически: как комбинация системных атрибутов и данных из источника энтропии, указанного в свойстве securerandom.source. Однако, в таком случае  размер его определяется провайдером алгоритма, так что если хотим гарантий требуемой надёжности — устанавливаем самостоятельно.

Следить за своевременным изменением seed здесь также нужно самостоятельно.

И, наконец, семейство Native… алгоритмов. Это:

  • NativePRNG
  • NativePRNGBlocking
  • NativePRNGNonBlocking

Алгоритмы доступны только на unix-подобных системах. Опираются они на “устройства” /dev/random и /dev/urandom. Работать с ними также просто, как и с SHA1PRNG:

SecureRandom secureRandom =
    SecureRandom.getInstance("NativePRNGNonBlocking");

При этом необходимо учитывать следующие особенности этих алгоритмов.

  • NativePRNG автоматически инициализирует seed через /dev/random, данные получает через /dev/urandom
  • NativePRNGBlocking автоматически инициализирует seed и получает данные через /dev/random
  • NativePRNGNonBlocking автоматически инициализирует seed и получает данные через /dev/urandom

С особой аккуратностью следует использовать алгоритмы NativePRNG и NativePRNGBlocking, поскольку они опираются на блокирующее устройство /dev/random. А значит, если достаточная энтропия так и не будет получена (например, используется виртуальная машина), то процесс может зависнуть наглухо.

По скорости генерации это самые медленные алгоритмы из рассмотренных, при этом качество достаточно высокое.

Никита Корнеев