Само название класса подразумевает криптостойкость генерируемых случайных чисел (то есть устойчивость к взлому, невозможность предсказать следующее число по предыдущим за разумное время). Но так ли это?
Сперва разберёмся с тем, что же для нас умеет генерировать 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 |
---|---|---|
Solaris | 1. PKCS11 | SunPKCS11 |
2. NativePRNG | SUN | |
3. DRBG | SUN | |
4. SHA1PRNG | SUN | |
5. NativePRNGBlocking | SUN | |
6. NativePRNGNonBlocking | SUN | |
Linux | 1. NativePRNG | SUN |
2. DRBG | SUN | |
3. SHA1PRNG | SUN | |
4. NativePRNGBlocking | SUN | |
5. NativePRNGNonBlocking | SUN | |
macOS | 1. NativePRNG | SUN |
2. DRBG | SUN | |
3. SHA1PRNG | SUN | |
4. NativePRNGBlocking | SUN | |
5. NativePRNGNonBlocking | SUN | |
Windows | 1. DRBG | SUN |
2. SHA1PRNG | SUN | |
3. Windows-PRNG | SunMSCAPI |
Выясним, какие алгоритмы используются в 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 превратилась в такую:
- указать алгоритм (DRBG);
- указать механизм (на выбор: Hash_DRBG, HMAC_DRBG, CTR_DRBG)
- указать алгоритм для выбранного механизма.
И в таком случае мы будем точно знать каким алгоритмом генерируются наши случайные числа и является ли эта генерация криптографически стойкой. В противном случае нам придётся полагаться на выбор конкретного 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. А значит, если достаточная энтропия так и не будет получена (например, используется виртуальная машина), то процесс может зависнуть наглухо.
По скорости генерации это самые медленные алгоритмы из рассмотренных, при этом качество достаточно высокое.