1. Обзор
BouncyCastle — это библиотека Java, которая дополняет стандартное криптографическое расширение Java (JCE).
В этой вводной статье мы собираемся показать, как использовать BouncyCastle для выполнения криптографических операций, таких как шифрование и подпись.
2. Конфигурация Maven
Прежде чем мы начнем работать с библиотекой, нам нужно добавить необходимые зависимости в наш файл pom.xml
:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.58</version>
</dependency>
Обратите внимание, что мы всегда можем посмотреть последние версии зависимостей в центральном репозитории Maven .
3. Настройка файлов политик неограниченной силы юрисдикции
Стандартная установка Java ограничена с точки зрения стойкости криптографических функций, это связано с политиками, запрещающими использование ключа с размером, превышающим определенные значения, например 128 для AES.
Чтобы обойти это ограничение, нам необходимо настроить файлы политик неограниченной силы юрисдикции .
Для этого нам сначала нужно скачать пакет, перейдя по этой ссылке . После этого нам нужно извлечь заархивированный файл в каталог по нашему выбору, который содержит два файла jar:
local_policy.jar
US_export_policy.jar
Наконец, нам нужно найти папку {JAVA_HOME}/lib/security
и заменить существующие файлы политик теми, которые мы извлекли здесь.
Обратите внимание, что в Java 9 нам больше не нужно загружать пакет файлов политик , достаточно установить для свойства crypto.policy
значение неограниченное
:
Security.setProperty("crypto.policy", "unlimited");
После этого нам нужно проверить правильность работы конфигурации:
int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES");
System.out.println("Max Key Size for AES : " + maxKeySize);
Как результат:
Max Key Size for AES : 2147483647
Основываясь на максимальном размере ключа, возвращаемом методом getMaxAllowedKeyLength()
, мы можем с уверенностью сказать, что файлы политики неограниченной надежности были установлены правильно.
Если возвращаемое значение равно 128, нам нужно убедиться, что мы установили файлы в JVM, где мы запускаем код.
4. Криптографические операции
4.1. Подготовка сертификата и закрытого ключа
Прежде чем мы перейдем к реализации криптографических функций, нам сначала нужно создать сертификат и закрытый ключ.
В целях тестирования мы можем использовать эти ресурсы:
ForEach.cer
— это цифровой сертификат, использующий международный стандарт инфраструктуры открытых ключей X.509, а ForEach.p12
— защищенное паролем хранилище ключей PKCS12 , содержащее закрытый ключ.
Давайте посмотрим, как их можно загрузить в Java:
Security.addProvider(new BouncyCastleProvider());
CertificateFactory certFactory= CertificateFactory
.getInstance("X.509", "BC");
X509Certificate certificate = (X509Certificate) certFactory
.generateCertificate(new FileInputStream("ForEach.cer"));
char[] keystorePassword = "password".toCharArray();
char[] keyPassword = "password".toCharArray();
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(new FileInputStream("ForEach.p12"), keystorePassword);
PrivateKey key = (PrivateKey) keystore.getKey("foreach", keyPassword);
Во-первых, мы динамически добавили BouncyCastleProvider
в качестве поставщика безопасности с помощью метода addProvider()
.
Это также можно сделать статически, отредактировав файл {JAVA_HOME}/jre/lib/security/java.security
и добавив следующую строку:
security.provider.N = org.bouncycastle.jce.provider.BouncyCastleProvider
После правильной установки провайдера мы создали объект CertificateFactory
с помощью метода getInstance()
.
Метод getInstance()
принимает два аргумента; тип сертификата «X.509» и поставщик безопасности «BC».
Экземпляр certFactory
впоследствии используется для создания объекта X509Certificate с помощью метода
generateCertificate()
.
Точно так же мы создали объект хранилища ключей PKCS12, для которого вызывается метод load()
.
Метод getKey()
возвращает закрытый ключ, связанный с данным псевдонимом.
Обратите внимание, что хранилище ключей PKCS12 содержит набор закрытых ключей, каждый закрытый ключ может иметь определенный пароль, поэтому нам нужен глобальный пароль для открытия хранилища ключей и определенный пароль для получения закрытого ключа.
Сертификат и пара закрытых ключей в основном используются в асимметричных криптографических операциях:
- Шифрование
- Расшифровка
- Подпись
- Проверка
4.2. Шифрование и дешифрование CMS/PKCS7
В криптографии с асимметричным шифрованием для каждой связи требуется открытый сертификат и закрытый ключ.
Получатель привязан к сертификату, который общедоступен для всех отправителей.
Проще говоря, отправителю нужен сертификат получателя для шифрования сообщения, а получателю нужен соответствующий закрытый ключ, чтобы расшифровать его.
Давайте посмотрим, как реализовать функцию encryptData()
с использованием сертификата шифрования:
public static byte[] encryptData(byte[] data,
X509Certificate encryptionCertificate)
throws CertificateEncodingException, CMSException, IOException {
byte[] encryptedData = null;
if (null != data && null != encryptionCertificate) {
CMSEnvelopedDataGenerator cmsEnvelopedDataGenerator
= new CMSEnvelopedDataGenerator();
JceKeyTransRecipientInfoGenerator jceKey
= new JceKeyTransRecipientInfoGenerator(encryptionCertificate);
cmsEnvelopedDataGenerator.addRecipientInfoGenerator(transKeyGen);
CMSTypedData msg = new CMSProcessableByteArray(data);
OutputEncryptor encryptor
= new JceCMSContentEncryptorBuilder(CMSAlgorithm.AES128_CBC)
.setProvider("BC").build();
CMSEnvelopedData cmsEnvelopedData = cmsEnvelopedDataGenerator
.generate(msg,encryptor);
encryptedData = cmsEnvelopedData.getEncoded();
}
return encryptedData;
}
Мы создали объект JceKeyTransRecipientInfoGenerator
, используя сертификат получателя.
Затем мы создали новый объект CMSEnvelopedDataGenerator
и добавили в него генератор информации о получателях.
После этого мы использовали класс JceCMSContentEncryptorBuilder
для создания объекта OutputEncrytor
с использованием алгоритма AES CBC.
Шифровальщик используется позже для создания объекта CMSEnvelopedData
, который инкапсулирует зашифрованное сообщение.
Наконец, закодированное представление конверта возвращается в виде массива байтов.
Теперь давайте посмотрим, как выглядит реализация метода decryptData() :
public static byte[] decryptData(
byte[] encryptedData,
PrivateKey decryptionKey)
throws CMSException {
byte[] decryptedData = null;
if (null != encryptedData && null != decryptionKey) {
CMSEnvelopedData envelopedData = new CMSEnvelopedData(encryptedData);
Collection<RecipientInformation> recipients
= envelopedData.getRecipientInfos().getRecipients();
KeyTransRecipientInformation recipientInfo
= (KeyTransRecipientInformation) recipients.iterator().next();
JceKeyTransRecipient recipient
= new JceKeyTransEnvelopedRecipient(decryptionKey);
return recipientInfo.getContent(recipient);
}
return decryptedData;
}
Сначала мы инициализировали объект CMSEnvelopedData
, используя зашифрованный массив байтов данных, а затем получили всех предполагаемых получателей сообщения, используя метод getRecipients()
.
После этого мы создали новый объект JceKeyTransRecipient,
связанный с закрытым ключом получателя.
Экземпляр реципиентаInfo
содержит расшифрованное/инкапсулированное сообщение, но мы не можем получить его, если у нас нет соответствующего ключа получателя.
Наконец, учитывая ключ получателя в качестве аргумента, метод getContent()
возвращает необработанный массив байтов, извлеченный из EnvelopedData
, с которым связан этот получатель.
Давайте напишем простой тест, чтобы убедиться, что все работает именно так, как должно:
String secretMessage = "My password is 123456Seven";
System.out.println("Original Message : " + secretMessage);
byte[] stringToEncrypt = secretMessage.getBytes();
byte[] encryptedData = encryptData(stringToEncrypt, certificate);
System.out.println("Encrypted Message : " + new String(encryptedData));
byte[] rawData = decryptData(encryptedData, privateKey);
String decryptedMessage = new String(rawData);
System.out.println("Decrypted Message : " + decryptedMessage);
Как результат:
Original Message : My password is 123456Seven
Encrypted Message : 0�*�H��...
Decrypted Message : My password is 123456Seven
4.3. Подпись и проверка CMS/PKCS7
Подпись и проверка — это криптографические операции, которые подтверждают подлинность данных.
Давайте посмотрим, как подписать секретное сообщение с помощью цифрового сертификата:
public static byte[] signData(
byte[] data,
X509Certificate signingCertificate,
PrivateKey signingKey) throws Exception {
byte[] signedMessage = null;
List<X509Certificate> certList = new ArrayList<X509Certificate>();
CMSTypedData cmsData= new CMSProcessableByteArray(data);
certList.add(signingCertificate);
Store certs = new JcaCertStore(certList);
CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator();
ContentSigner contentSigner
= new JcaContentSignerBuilder("SHA256withRSA").build(signingKey);
cmsGenerator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider("BC")
.build()).build(contentSigner, signingCertificate));
cmsGenerator.addCertificates(certs);
CMSSignedData cms = cmsGenerator.generate(cmsData, true);
signedMessage = cms.getEncoded();
return signedMessage;
}
Сначала мы встроили входные данные в CMSTypedData
, а затем создали новый объект CMSSignedDataGenerator
.
Мы использовали SHA256
с RSA в качестве алгоритма подписи и наш ключ подписи для создания нового объекта ContentSigner
.
Экземпляр contentSigner
используется впоследствии вместе с сертификатом подписи для создания объекта SigningInfoGenerator
.
После добавления SignerInfoGenerator
и сертификата подписи в экземпляр CMSSignedDataGenerator
мы, наконец, используем метод generate()
для создания объекта подписанных данных CMS, который также содержит подпись CMS.
Теперь, когда мы увидели, как подписывать данные, давайте посмотрим, как проверить подписанные данные:
public static boolean verifSignedData(byte[] signedData)
throws Exception {
X509Certificate signCert = null;
ByteArrayInputStream inputStream
= new ByteArrayInputStream(signedData);
ASN1InputStream asnInputStream = new ASN1InputStream(inputStream);
CMSSignedData cmsSignedData = new CMSSignedData(
ContentInfo.getInstance(asnInputStream.readObject()));
SignerInformationStore signers
= cmsSignedData.getCertificates().getSignerInfos();
SignerInformation signer = signers.getSigners().iterator().next();
Collection<X509CertificateHolder> certCollection
= certs.getMatches(signer.getSID());
X509CertificateHolder certHolder = certCollection.iterator().next();
return signer
.verify(new JcaSimpleSignerInfoVerifierBuilder()
.build(certHolder));
}
Опять же, мы создали объект CMSSignedData
на основе нашего подписанного массива байтов данных, а затем получили всех подписывающих лиц, связанных с подписями, с помощью метода getSignerInfos()
.
В этом примере мы проверили только одну подписывающую сторону, но для общего использования необходимо выполнить итерацию по коллекции подписывающих сторон, возвращаемых методом getSigners()
, и проверить каждую из них отдельно.
Наконец, мы создали объект SignerInformationVerifier
с помощью метода build()
и передали его методу verify()
.
Метод verify() возвращает значение true
, если данный объект может успешно проверить подпись на объекте подписывающей стороны.
Вот простой пример:
byte[] signedData = signData(rawData, certificate, privateKey);
Boolean check = verifSignData(signedData);
System.out.println(check);
Как результат:
true
5. Вывод
В этой статье мы узнали, как использовать библиотеку BouncyCastle для выполнения основных криптографических операций, таких как шифрование и подпись.
В реальной ситуации мы часто хотим подписать, а затем зашифровать наши данные таким образом, что только получатель может расшифровать их с помощью закрытого ключа и проверить их подлинность на основе цифровой подписи.
Фрагменты кода, как всегда, можно найти на GitHub .