Привет, Хабр! Представляю вашему вниманию перевод статьи "Java Cryptography" автора Jakob Jenkov.
Данная публикация является переводом первой статьи Java Cryptography из серии статей для начинающих, желающих освоить основы криптографии в Java.
Оглавление:
- Java Cryptography
- Java Cipher
- Java MessageDigest
- Java Mac
- Java Signature
- Java KeyPair
- Java KeyGenerator
- Java KeyPairGenerator
- Java KeyStore
- Java Keytool
- Java Certificate
- Java CertificateFactory
- Java CertPath
Java Cryptography
Java Cryptography API предоставляют возможность зашифровывать и расшифровывать данные в java, а также управлять ключами, подписями и осуществлять аутентификацию (проверка подлинности) сообщений, вычислять криптографические хэши и многое другое.
В этой статье объясняются основы того, как пользоваться Java Cryptography API для выполнения различных задач в которых требуется безопасное шифрование.
В этой статье не объясняются основы криптографической теории. Вам придется посмотреть эту информацию еще где нибудь.
Расширение криптографии Java
Java cryptography API предоставляется так называемым расширением Java Сryptography Extension(JCE). JCE уже давно является частью платформы Java. Изначально JCE был отделен от Java из-за того, что в США действовали экспортные ограничения на технологии шифрования. Поэтому самые стойкие алгоритмы шифрования не были включены в стандартную платформу Java. Эти более надежные алгоритмы шифрования можно применять, если ваша компания находится в США, но в остальных случаях придется применять более слабые алгоритмы или реализовывать свои собственные алгоритмы шифрования и подключать их к JCE.
С 2017 года правила экспорта алгоритмов шифрования в США были значительно ослаблены и в большей части мира можно пользоваться международными стандартами шифрования через Java JCE.
Архитектура криптографии Java
Java Cryptography Architecture (JCA) — название внутреннего дизайна API криптографии в Java. JCA структурирован вокруг нескольких основных классов и интерфейсов общего назначения. Реальная функциональность этих интерфейсов обеспечивается поставщиками. Таким образом, можно использовать класс Cipher (Шифр) для шифрования и расшифровки некоторых данных, но конкретная реализация шифра (алгоритм шифрования) зависит от конкретного используемого поставщика.
Также можно реализовать и подключить свои собственные провайдеры, но вы должны быть осторожны с этим. Правильно реализовать шифрование без дыр в безопасности сложно! Если вы не знаете, что делаете, вам, вероятно, лучше использовать встроенный поставщик Java или использовать надежного поставщика, такого как Bouncy Castle.
Основные классы и интерфейсы
API криптографии Java состоит из следующих пакетов Java:
- java.security
- java.security.cert
- java.security.spec
- java.security.interfaces
- javax.crypto
- javax.crypto.spec
- javax.crypto.interfaces
Основные классы и интерфейсы этих пакетов:
- Provider
- SecureRandom
- Cipher
- MessageDigest
- Signature
- Mac
- AlgorithmParameters
- AlgorithmParameterGenerator
- KeyFactory
- SecretKeyFactory
- KeyPairGenerator
- KeyGenerator
- KeyAgreement
- KeyStore
- CertificateFactory
- CertPathBuilder
- CertPathValidator
- CertStore
Provider (Поставщик криптографии)
Класс Provider (java.security.Provider) является центральным классом в Java crypto API. Для того чтобы использовать Java crypto API, вам нужно установить поставщика криптографии. Java SDK поставляется с собственным поставщиком криптографии. Если вы явно не установите поставщик криптографии, то будет использоваться поставщик по умолчанию. Однако этот поставщик криптографии может не поддерживать алгоритмы шифрования, которые вы хотите использовать. Поэтому вам, возможно, придется установить свой собственный поставщик криптографии.
Один из самых популярных поставщиков криптографии для Java crypto API называется Bouncy Castle. Вот пример, где в качестве поставщика криптографии устанавливается BouncyCastleProvider:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class ProviderExample {
public static void main(String[] args) {
Security.addProvider(new BouncyCastleProvider());
}
}
Cipher (Шифр)
Класс Cipher (javax.crypto.Cipher) представляет криптографический алгоритм. Шифр может использоваться как для шифрования, так и для расшифровки данных. Класс Cipher объясняется более подробно в следующих разделах, ниже будет его краткое описание.
Создание экземпляра класса шифр, который использует алгоритм шифрования AES для внутреннего использования:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Метод Cipher.getInstance(...) принимает строку, определяющую, какой алгоритм шифрования использовать, а также некоторые другие параметры алгоритма.
В приведенном выше примере:
- AES — алгоритм шифрования
- CBC — это режим, в котором может работать алгоритм AES.
- PKCS5Padding — это то, как алгоритм AES должен обрабатывать последние байты данных для шифрования. Что именно это означает, ищите в руководстве по криптографии в целом, а не в этой статье.
Инициализация шифра
Перед использованием экземпляра шифра его необходимо инициализировать. Экземпляр шифра инициализируется вызывом метода init(). Метод init() принимает два параметра:
- Режим — Шифрование / Расшифровка
- Ключ
Первый параметр указывает, режим работы экземпляра шифр: шифровать или расшифровывать данные. Второй параметр указывает, какой ключ они используют для шифрования или расшифровки данных.
Пример:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
Обратите внимание, что способ создания ключа в этом примере небезопасен и не должен использоваться на практике. В этой статье в следующих разделах будет рассказано, как создавать ключи более безопасно.
Чтобы инициализировать экземпляр шифра для расшифровки данных, вы должны использовать Cipher.DECRYPT_MODE, например:
cipher.init(Cipher.DECRYPT_MODE, key);
Шифрование или дешифрование данных
После инициализации шифра вы можете начать шифрование или расшифровку данных вызовом методов update() или doFinal(). Метод update() используется, если вы шифруете или расшифровываете фрагмент данных. Метод doFinal() вызывается, когда вы шифруете последний фрагмент данных или если блок данных, который вы передаете в doFinal(), является единичным набором данных данных для шифрования.
Пример шифрования данных с помощью метода doFinal():
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);
Чтобы расшифровать данные, нужно передать зашифрованный текст(данные) в метод doFinal() или doUpdate().
Keys (Ключи)
Для шифрования или дешифрования данных вам нужен ключ. Существует два типа ключей в зависимости от того, какой тип алгоритма шифрования используется:
- Симметричные ключи
- Асимметричные ключи
Симметричные ключи используются для симметричных алгоритмов шифрования. Алгоритмы симметричного шифрования используют один и тот же ключ для шифрования и расшифровки.
Асимметричные ключи используются для алгоритмов асимметричного шифрования. Алгоритмы асимметричного шифрования используют один ключ для шифрования, а другой для дешифрования. Алгоритмы шифрования с открытым и закрытым ключом — примеры асимметричных алгоритмов шифрования.
Каким-то образом сторона, которая должна расшифровать данные, должна знать ключ, необходимый для дешифрования данных. Если дешифрующая сторона не является стороной шифрующей данные, эти две стороны должны договориться о ключе или обменяться ключом. Это называется обменом ключами.
Безопасность ключа
Ключи должны быть трудно угадываемые, чтобы злоумышленник не мог легко подобрать ключ шифрования. В примере из предыдущего раздела о классе Шифр(Cipher) использовался очень простой, жестко закодированный ключ. На практике так делать не стоит. Если ключ сторон легко угадать, злоумышленнику будет легко расшифровать зашифрованные данные и, возможно, создать поддельные сообщения самостоятельно. Важно сделать ключ, который трудно угадать. Таким образом, ключ должен состоять из случайных байтов. Чем больше случайных байтов тем сложнее угадать, потому что существует больше возможных комбинаций.
Генерация ключа
Чтобы сгенерировать случайные ключи шифрования вы можете использовать класс Java KeyGenerator. KeyGenerator будет более подробно описан в следующих главах, вот небольшой пример его использования здесь:
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = new SecureRandom();
int keyBitSize = 256;
keyGenerator.init(keyBitSize, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
Полученный экземпляр SecretKey можно передать в метод Cipher.init(), например так:
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Генерация пары ключей
Алгоритмы асимметричного шифрования используют пару ключей, состоящую из открытого ключа и закрытого ключа, для шифрования и дешифрования данных. Для создания асимметричной пары ключей вы можете использовать KeyPairGenerator (java.security.KeyPairGenerator). KeyPairGenerator будет более подробно описан в следующих главах, ниже простой пример использования Java KeyPairGenerator:
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Хранилище ключей (Key Store)
Java KeyStore — это база данных, которая может содержать ключи. Java KeyStore представлен классом KeyStore (java.security.KeyStore). Хранилище ключей может содержать ключи следующих типов:
- Закрытые ключи (Private keys)
- Открытые ключи и сертификаты(Public keys + certificates)
- Секретные ключи (Secret keys)
Закрытый и открытый ключи используются в асимметричном шифровании. Открытый ключ может иметь связанный сертификат. Сертификат — это документ, удостоверяющий личность человека, организации или устройства, претендующего на владение открытым ключом.
Сертификат обычно имеет цифровую подпись проверяющей стороны в качестве доказательства.
Секретные ключи используются в симметричном шифровании.Класс KeyStore довольно сложный, поэтому он описан более подробно далее в отдельной главе по Java KeyStore.
Инструмент управления ключами (Keytool)
Java Keytool — это инструмент командной строки, который может работать с файлами Java KeyStore. Keytool может генерировать пары ключей в файл KeyStore, экспортировать сертификаты и импортировать сертификаты в KeyStore и некоторые другие функции. Keytool поставляется с установкой Java. Keytool более подробно описан далее в отдельной главе по Java Keytool.
Дайджест сообщения (MessageDigest)
Когда вы получаете зашифрованные данные от другой стороны, можете ли вы быть уверенными что никто не изменил зашифрованные данные по пути к вам?
Обычно решение состоит в том, чтобы вычислить дайджест сообщения из данных до его шифрования, а затем зашифровать как данные, так и дайджест сообщения, и отправить его по сети. Дайджест сообщения — это хеш-значение, рассчитанное на основе данных сообщения. Если в зашифрованных данных изменяется хоть один байт, изменится и дайджест сообщения, рассчитанный по данным.
При получении зашифрованных данных вы расшифровываете их, вычисляете из них дайджест сообщения и сравниваете вычисленный дайджест сообщения с дайджестом сообщения, отправленного вместе с зашифрованными данными. Если два дайджеста сообщения одинаковы, существует высокая вероятность (но не 100%) того, что данные не были изменены.
Java MessageDigest (java.security.MessageDigest) можно использовать для вычисления дайджестов сообщений. Для создания экземпляра MessageDigest вызывается метод MessageDigest.getInstance(). Существует несколько различных алгоритмов дайджеста сообщений. Вам нужно указать, какой алгоритм вы хотите использовать при создании экземпляра MessageDigest. Работа с MessageDigest будет более подробно описана в главе посвященной Java MessageDigest.
Краткое введение в класс MessageDigest:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
В этом примере создается экземпляр MessageDigest, который использует внутренний алгоритм криптографического хэширования SHA-256 для вычисления дайджестов сообщений.
Чтобы вычислить дайджест сообщения некоторых данных, вы вызываете метод update() или digest(). Метод update() может вызываться несколько раз, а дайджест сообщения обновляется внутри объекта. Когда вы передали все данные, которые вы хотите включить в дайджест сообщения, вы вызываете digest() и извлекаете итоговые данные дайджеста сообщения.
Пример вызова update() несколько раз с последующим вызовом digest():
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
messageDigest.update(data1);
messageDigest.update(data2);
byte[] digest = messageDigest.digest();
Вы также можете вызвать digest() один раз, передав все данные, чтобы вычислить дайджест сообщения. Пример:
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] data1 = "0123456789".getBytes("UTF-8");
byte[] digest = messageDigest.digest(data1);
Код аутентификации сообщения (MAC)
Класс Java Mac используется для создания MAC(Message Authentication Code) из сообщения. MAC похож на дайджест сообщения, но использует дополнительный ключ для шифрования дайджеста сообщения. Только имея как исходные данные, так и ключ, вы можете проверить MAC. Таким образом, MAC является более безопасным способом защиты блока данных от модификации, чем дайджест сообщения. Класс Mac более подробно описан в главе по Java Mac, ниже приведено краткое введение.
Экземпляр Java Mac создается вызовом метода Mac.getInstance(), передавая в качестве параметра имя используемого алгоритма. Вот как это выглядит:
Mac mac = Mac.getInstance("HmacSHA256");
Прежде чем создать MAC из данных, вы должны инициализировать экземпляр Mac ключом. Вот пример инициализации экземпляра Mac ключом:
byte[] keyBytes = new byte[]{0,1,2,3,4,5,6,7,8 ,9,10,11,12,13,14,15};
String algorithm = "RawBytes";
SecretKeySpec key = new SecretKeySpec(keyBytes, algorithm);
mac.init(key);
После инициализации экземпляра Mac вы можете вычислить MAC из данных, вызвав методы update() и doFinal(). Если у вас есть все данные для расчета MAC, вы можете сразу вызвать метод doFinal(). Вот как это выглядит:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
byte[] data2 = "0123456789".getBytes("UTF-8");
mac.update(data);
mac.update(data2);
byte[] macBytes = mac.doFinal();
Подпись (Signature)
Класс Signature (java.security.Signature) используется для цифровой подписи данных. Когда данные подписаны, цифровая подпись создается из этих данных. Таким образом, подпись отделена от данных.
Цифровая подпись создается путем создания дайджеста сообщения (хеша) из данных и шифрования этого дайджеста сообщения с помощью закрытого ключа устройства, лица или организации, которая должна подписать данные. Дайджест зашифрованного сообщения называется цифровой подписью.
Для создания экземпляра Signature, вызывается метод Signature.getInstance (...):
Signature signature = Signature.getInstance("SHA256WithDSA");
Подпись данных
Чтобы подписать данные, вы должны инициализировать экземпляр подписи в режиме подписи вызывая метод initSign(...), передавая закрытый ключ для подписи данных. Пример инициализации экземпляра подписи в режиме подписи:
signature.initSign(keyPair.getPrivate(), secureRandom);
После инициализации экземпляра подписи, его можно использовать для подписи данных. Это делается вызовом метода update(), передавая данные для подписи в качестве параметра. Можно вызывать метод update() несколько раз, чтобы дополнить данные для создании подписи. После передачи всех данных в метод update(), вызывается метод sign() для получения цифровой подписи. Вот как это выглядит:
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Проверка подписи
Чтобы проверить подпись, нужно инициализировать экземпляр подписи в режиме проверки путем вызова метода initVerify(...), передавая в качестве параметра открытый ключ, который используется для проверки подписи. Пример инициализации экземпляра подписи в режиме проверки выглядит:
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initVerify(keyPair.getPublic());
После инициализации в режиме проверки в метод update() передаются данные, которые подписаны подписью. Вызов метода verify(), возвращает значение true или false в зависимости от того, можно ли проверить подпись или нет. Вот как выглядит проверка подписи:
byte[] data2 = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature2.update(data2);
boolean verified = signature2.verify(digitalSignature);
Полный пример подписи и проверки
SecureRandom secureRandom = new SecureRandom();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Signature signature = Signature.getInstance("SHA256WithDSA");
signature.initSign(keyPair.getPrivate(), secureRandom);
byte[] data = "abcdefghijklmnopqrstuvxyz".getBytes("UTF-8");
signature.update(data);
byte[] digitalSignature = signature.sign();
Signature signature2 = Signature.getInstance("SHA256WithDSA");
signature2.initVerify(keyPair.getPublic());
signature2.update(data);
boolean verified = signature2.verify(digitalSignature);
System.out.println("verified = " + verified);