Перейти к основному содержимому

Создание безопасного ключа AES в Java

· 8 мин. чтения

1. Обзор

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

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

2. АЭС

Расширенный стандарт шифрования (AES) является преемником стандарта шифрования данных (DES), опубликованного в 2001 году Национальным институтом стандартов и технологий (NIST). Он классифицируется как симметричный блочный шифр.

Симметричный шифр использует один и тот же секретный ключ как для шифрования, так и для дешифрования. Блочный шифр означает, что он работает с 128-битными блоками исходного открытого текста:

./c520ffdb6ea315729e174ea30440a771.png

2.1. Варианты AES

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

   | Размер ключа    | Размер блока    | # раундов   | 
| 128 | 128 | 10 |
| 192 | 128 | 12 |
| 256 | 128 | 14 |

2.2. Насколько безопасен AES?

Алгоритм AES является общедоступной информацией — это ключ AES, который является секретным и должен быть известен для успешного расшифровки. Итак, все сводится к взлому ключей AES. Предполагая, что ключ надежно сохранен, злоумышленнику придется попытаться угадать ключ.

Давайте посмотрим, как работает метод грубой силы при угадывании ключа.

Ключи AES-128 имеют длину 128 бит, что означает 2^128 возможных значений . Чтобы найти это , потребуется огромное и невообразимое количество времени и денег . Следовательно, AES практически невозможно взломать методом грубой силы.

Было несколько подходов без грубой силы, но они могли уменьшить возможное пространство для поиска ключа только на пару бит.

Все это означает, что при нулевых знаниях о ключе AES практически невозможно взломать .

3. Свойства хорошего ключа

Давайте теперь рассмотрим некоторые важные рекомендации, которым необходимо следовать при создании ключа AES.

3.1. Размер ключа

Поскольку AES поддерживает три размера ключа, мы должны выбрать правильный размер ключа для варианта использования. AES-128 является наиболее распространенным выбором в коммерческих приложениях. Он предлагает баланс между безопасностью и скоростью. Национальные правительства обычно используют AES-192 и AES-256 для обеспечения максимальной безопасности. Мы можем использовать AES-256, если хотим иметь дополнительный уровень безопасности.

Квантовые компьютеры действительно представляют угрозу, поскольку могут уменьшить объем вычислений, необходимых для больших пространств ключей. Следовательно, наличие ключа AES-256 было бы более перспективным, хотя на данный момент они недоступны для любых злоумышленников коммерческих приложений.

3.2. Энтропия

Энтропия относится к случайности в ключе. Если сгенерированный ключ не является достаточно случайным и имеет некоторую корреляцию с зависимостью от времени, машины или словарного слова, например, он становится уязвимым. Злоумышленник сможет сузить область поиска ключей, лишив возможности AES. Следовательно, крайне важно, чтобы ключи были действительно случайными .

4. Генерация ключей AES

Теперь, вооружившись рекомендациями по созданию ключа AES, давайте рассмотрим различные подходы к их созданию.

Для всех фрагментов кода мы определяем наш шифр как:

private static final String CIPHER = "AES";

4.1. Случайный

Давайте используем класс Random в Java для генерации ключа:

private static Key getRandomKey(String cipher, int keySize) {
byte[] randomKeyBytes = new byte[keySize / 8];
Random random = new Random();
random.nextBytes(randomKeyBytes);
return new SecretKeySpec(randomKeyBytes, cipher);
}

Мы создаем массив байтов с желаемым размером ключа и заполняем его случайными байтами, полученными из random.nextBytes() . Затем случайный массив байтов используется для создания SecretKeySpec .

Класс Java Random представляет собой генератор псевдослучайных чисел (PRNG), также известный как детерминированный генератор случайных чисел (DRNG). Это означает, что это не совсем случайно. Последовательность случайных чисел в PRNG может быть полностью определена на основе его начального числа. Java не рекомендует использовать Random для криптографических приложений.

При этом НИКОГДА не используйте Random для генерации ключей .

4.2. SecureRandom

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

private static Key getSecureRandomKey(String cipher, int keySize) {
byte[] secureRandomKeyBytes = new byte[keySize / 8];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(secureRandomKeyBytes);
return new SecretKeySpec(secureRandomKeyBytes, cipher);
}

Как и в предыдущем примере, мы создаем массив байтов с желаемым размером ключа. Теперь вместо использования Random мы используем SecureRandom для генерации случайных байтов для нашего массива байтов. SecureRandom рекомендуется Java для генерации случайных чисел для криптографических приложений. Минимально соответствует FIPS 140-2, Требования безопасности для криптографических модулей .

Понятно, что в Java SecureRandom является стандартом де-факто для получения случайности . Но лучший ли это способ генерации ключей? Переходим к следующему подходу.

4.3. Генератор ключей

Далее сгенерируем ключ с помощью класса KeyGenerator :

private static Key getKeyFromKeyGenerator(String cipher, int keySize) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(cipher);
keyGenerator.init(keySize);
return keyGenerator.generateKey();
}

Мы получаем экземпляр KeyGenerator для шифра, с которым работаем. Затем мы инициализируем объект keyGenerator с желаемым размером ключа. Наконец, мы вызываем метод generateKey для генерации нашего секретного ключа. Итак, чем он отличается от подходов Random и SecureRandom ?

Стоит выделить два принципиальных отличия.

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

Использование SecureRandom с недопустимым keySize вызывает исключение, когда мы инициализируем шифр для шифрования:

encrypt(plainText, getSecureRandomKey(CIPHER, 111));
java.security.InvalidKeyException: Invalid AES key length: 13 bytes
at java.base/com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:90)
at java.base/com.sun.crypto.provider.GaloisCounterMode.init(GaloisCounterMode.java:321)
at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:592)
at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:470)
at java.base/com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:322)
at java.base/javax.crypto.Cipher.implInit(Cipher.java:867)
at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:929)
at java.base/javax.crypto.Cipher.init(Cipher.java:1299)
at java.base/javax.crypto.Cipher.init(Cipher.java:1236)
at com.foreach.secretkey.Main.encrypt(Main.java:59)
at com.foreach.secretkey.Main.main(Main.java:51)

С другой стороны, при использовании KeyGenerator происходит сбой во время самой генерации ключа, что позволяет нам обрабатывать его более надлежащим образом:

encrypt(plainText, getKeyFromKeyGenerator(CIPHER, 111));
java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256
at java.base/com.sun.crypto.provider.AESKeyGenerator.engineInit(AESKeyGenerator.java:93)
at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:539)
at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:516)
at com.foreach.secretkey.Main.getKeyFromKeyGenerator(Main.java:89)
at com.foreach.secretkey.Main.main(Main.java:58)

Другим ключевым отличием является использование SecureRandom по умолчанию . Класс KeyGenerator является частью криптопакета Java javax.crypto , который обеспечивает использование SecureRandom для случайности. Мы можем увидеть определение метода init в классе KeyGenerator :

public final void init(int keysize) {
init(keysize, JCAUtil.getSecureRandom());
}

Следовательно, использование KeyGenerator в качестве практики гарантирует, что мы никогда не используем объект класса Random для генерации ключей.

4.4. Ключ на основе пароля

До сих пор мы генерировали ключи из случайных и не очень удобных для человека байтовых массивов. Ключ на основе пароля (PBK) предлагает нам возможность генерировать секретный ключ на основе удобочитаемого пароля:

private static Key getPasswordBasedKey(String cipher, int keySize, char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] salt = new byte[100];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, keySize);
SecretKey pbeKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(pbeKeySpec);
return new SecretKeySpec(pbeKey.getEncoded(), cipher);
}

У нас здесь происходит довольно много дел. Давайте сломаем это.

Начнем с нашего удобочитаемого пароля. Это тайна и ее нужно охранять. Необходимо соблюдать рекомендации по паролю, такие как минимальная длина 8 символов, использование специальных символов, сочетание прописных и строчных букв, цифр и т. д. Кроме того, в рекомендациях OWASP предлагается проверять уже открытые пароли.

Удобный пароль не имеет достаточной энтропии. Следовательно, мы добавляем дополнительные случайно сгенерированные байты, называемые солью , чтобы было труднее угадать. Минимальная длина соли должна быть 128 бит . Мы использовали SecureRandom для генерации нашей соли. Соль не является секретом и хранится в виде открытого текста. Мы должны генерировать соль попарно с каждым паролем и не использовать одну и ту же соль глобально. Это защитит от атак Rainbow Table , которые используют поиск в предварительно вычисленной хеш-таблице для взлома паролей.

Количество итераций — это количество раз, когда алгоритм генерации секрета применяет функцию преобразования. Он должен быть настолько большим, насколько это возможно. Минимальное рекомендуемое количество итераций — 1000 . Более высокое количество итераций увеличивает сложность для злоумышленника при выполнении проверки методом грубой силы для всех возможных паролей.

Размер ключа такой же, как мы обсуждали ранее, он может быть 128, 192 или 256 для AES.

Мы обернули все четыре элемента, рассмотренные выше, в объект PBEKeySpec . Далее, используя SecretKeyFactory , мы получаем экземпляр алгоритма PBKDF2WithHmacSHA256 для генерации ключа.

Наконец, вызывая generateSecret с PBEKeySpec , мы генерируем SecretKey на основе удобочитаемого пароля.

5. Вывод

Есть две основные базы для генерации ключа. Это может быть случайный ключ или ключ на основе удобочитаемого пароля. Мы обсудили три подхода к генерации случайного ключа. Среди них KeyGenerator обеспечивает настоящую случайность, а также предлагает систему сдержек и противовесов. Следовательно, KeyGenerator — лучший вариант .

Для ключа, основанного на удобочитаемом пароле, мы можем использовать SecretKeyFactory вместе с солью, сгенерированной с помощью SecureRandom и большого количества итераций.

Как всегда, полный код доступен на GitHub .