Введение в BouncyCastle с Java

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 Central Repository .

3. Настройка файлов политики неограниченной юрисдикции

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

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

Чтобы сделать это, нам сначала нужно загрузить пакет, следуя этому link .

После этого нам нужно извлечь zip-файл в каталог по нашему выбору, который содержит два jar-файла:

  • local policy.jar__

  • US export policy.jar

Наконец, нам нужно найти папку \ {JAVA HOME}/lib/security__ и заменить существующие файлы политик теми, которые мы извлекли здесь.

Обратите внимание, что в Java 9 нам больше не нужно загружать пакет файлов политики , достаточно установить для свойства crypto.policy значение unlimited :

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 - это цифровой сертификат, который использует международный стандарт инфраструктуры открытых ключей 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 Keystore, для которого вызывается метод 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 , связанный с личным ключом получателя.

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

  • Наконец, учитывая ключ получателя в качестве аргумента, метод 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.2 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 для выполнения основных криптографических операций, таких как шифрование и подпись.

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

Фрагменты кода, как всегда, можно найти по адресу over на GitHub .