«1. Обзор

BouncyCastle — это библиотека Java, дополняющая стандартное Java Cryptographic Extension (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. Подготовка сертификата и закрытого ключа

Прежде чем мы перейдем к реализации криптографических функций, нам сначала нужно создать сертификат и закрытый ключ.

В целях тестирования мы можем использовать эти ресурсы:

    Baeldung.cer Baeldung.p12 (пароль = «пароль»)

Baeldung.cer — это цифровой сертификат, использующий международную инфраструктуру открытого ключа X.509. стандарт, а Baeldung.p12 — это защищенное паролем хранилище ключей PKCS12, содержащее закрытый ключ.

Давайте посмотрим, как их можно загрузить в Java:

Security.addProvider(new BouncyCastleProvider());
CertificateFactory certFactory= CertificateFactory
  .getInstance("X.509", "BC");
 
X509Certificate certificate = (X509Certificate) certFactory
  .generateCertificate(new FileInputStream("Baeldung.cer"));
 
char[] keystorePassword = "password".toCharArray();
char[] keyPassword = "password".toCharArray();
 
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(new FileInputStream("Baeldung.p12"), keystorePassword);
PrivateKey key = (PrivateKey) keystore.getKey("baeldung", 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.

Мы использовали SHA256withRSA в качестве алгоритма подписи и наш ключ подписи для создания нового объекта 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.