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

Руководство по классу шифров

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

1. Обзор

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

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

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

2. Класс шифрования

Расширение криптографии Java (JCE) является частью архитектуры криптографии Java (JCA) , которая предоставляет приложению криптографические шифры для шифрования и дешифрования данных, а также для хеширования частных данных.

Класс Cipher , расположенный в пакете javax.crypto , формирует ядро среды JCE, предоставляя функциональные возможности для шифрования и дешифрования.

2.1. Создание экземпляра шифра

Чтобы создать экземпляр объекта Cipher , мы вызываем статический метод getInstance , передавая имя запрошенного преобразования . При желании можно указать имя провайдера.

Давайте напишем пример класса, иллюстрирующий создание экземпляра Cipher :

public class Encryptor {

public byte[] encryptMessage(byte[] message, byte[] keyBytes)
throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
//...
}
}

Преобразование AES/ECB/PKCS5Padding указывает методу getInstance создать экземпляр объекта Cipher как шифра AES с режимом работы ECB и схемой заполнения PKCS5 .

Мы также можем создать экземпляр объекта Cipher , указав только алгоритм преобразования:

Cipher cipher = Cipher.getInstance("AES");

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

Обратите внимание, что getInstance вызовет исключение NoSuchAlgorithmException , если преобразование имеет значение null , пусто или имеет недопустимый формат, или если поставщик его не поддерживает.

Это вызовет исключение NoSuchPaddingException , если преобразование содержит неподдерживаемую схему заполнения.

2.2. Потокобезопасность

Класс Cipher является классом с отслеживанием состояния без какой-либо формы внутренней синхронизации. На самом деле такие методы, как init() или update() , изменяют внутреннее состояние конкретного экземпляра Cipher .

Поэтому класс Cipher не является потокобезопасным. Таким образом, мы должны создать один экземпляр Cipher для каждой потребности в шифровании/дешифровании.

2.3. Ключи

Интерфейс Key представляет ключи для криптографических операций. Ключи — это непрозрачные контейнеры, содержащие закодированный ключ, формат кодирования ключа и его криптографический алгоритм.

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

Давайте создадим симметричный ключ из предоставленных байтов ключа:

SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");

2.4. Инициализация шифра

Мы вызываем метод init() для инициализации объекта C ipher ключом или сертификатом и режимом операции , указывающим режим работы шифра. `` ``

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

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

Вот доступные режимы работы шифра:

  • ENCRYPT_MODE : инициализировать объект шифрования в режиме шифрования .
  • DECRYPT_MODE : инициализировать объект шифрования в режиме дешифрования .
  • WRAP_MODE : инициализировать объект шифрования в режиме переноса ключей .
  • UNWRAP_MODE : инициализировать объект шифрования в режиме распаковки ключа .

Давайте инициализируем объект Cipher :

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// ...

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

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

Давайте рассмотрим пример с использованием сертификата :

public byte[] encryptMessage(byte[] message, Certificate certificate) 
throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, certificate);
// ...
}

Объект Cipher получает открытый ключ для шифрования данных из сертификата, вызывая метод getPublicKey .

2.5. Шифрование и дешифрование

После инициализации объекта Cipher мы вызываем метод doFinal() для выполнения операции шифрования или дешифрования. Этот метод возвращает массив байтов, содержащий зашифрованное или расшифрованное сообщение.

Метод doFinal() также сбрасывает объект Cipher в состояние, в котором он находился при предыдущей инициализации с помощью вызова метода init() , делая объект Cipher доступным для шифрования или расшифровки дополнительных сообщений.

Давайте вызовем doFinal в нашем методе encryptMessage :

public byte[] encryptMessage(byte[] message, byte[] keyBytes)
throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException,
BadPaddingException, IllegalBlockSizeException {

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(message);
}

Чтобы выполнить операцию расшифровки, мы меняем opmode на DECRYPT_MODE :

public byte[] decryptMessage(byte[] encryptedMessage, byte[] keyBytes) 
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedMessage);
}

2.6. Провайдеры

Разработанный для использования архитектуры на основе провайдеров , JCE позволяет подключать квалифицированные криптографические библиотеки, такие как BouncyCastle , в качестве провайдеров безопасности и беспрепятственно добавлять новые алгоритмы .

Теперь давайте добавим BouncyCastle в качестве поставщика безопасности. Мы можем добавить поставщика безопасности статически или динамически.

Чтобы добавить BouncyCastle статически, мы модифицируем файл java.security , расположенный в папке <JAVA_HOME>/jre/lib/security .

Добавляем строку в конец списка:

...
security.provider.4=com.sun.net.ssl.internal.ssl.Provider
security.provider.5=com.sun.crypto.provider.SunJCE
security.provider.6=sun.security.jgss.SunProvider
security.provider.7=org.bouncycastle.jce.provider.BouncyCastleProvider

При добавлении свойства провайдера ключ свойства имеет формат security.provider.N , где число N на единицу больше, чем последнее в списке.

Мы также можем добавить поставщика безопасности BouncyCastle динамически , не изменяя файл безопасности:

Security.addProvider(new BouncyCastleProvider());

Теперь мы можем указать провайдера во время инициализации шифра:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "BC");

BC указывает BouncyCastle в качестве провайдера. Мы можем получить список зарегистрированных провайдеров с помощью метода Security.getProviders() .

3. Тестирование шифрования и дешифрования

Давайте напишем пример теста, чтобы проиллюстрировать шифрование и дешифрование сообщений.

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

@Test
public void whenIsEncryptedAndDecrypted_thenDecryptedEqualsOriginal()
throws Exception {

String encryptionKeyString = "thisisa128bitkey";
String originalMessage = "This is a secret message";
byte[] encryptionKeyBytes = encryptionKeyString.getBytes();

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKey secretKey = new SecretKeySpec(encryptionKeyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);

byte[] encryptedMessageBytes = cipher.doFinal(message.getBytes());

cipher.init(Cipher.DECRYPT_MODE, secretKey);

byte[] decryptedMessageBytes = cipher.doFinal(encryptedMessageBytes);
assertThat(originalMessage).isEqualTo(new String(decryptedMessageBytes));
}

4. Вывод

В этой статье мы обсудили класс Cipher и представили примеры использования. Дополнительные сведения о классе Cipher и JCE Framework можно найти в документации по классу и в Справочном руководстве по архитектуре криптографии Java (JCA) .

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub . Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.