Contents

BouncyCastle 简介

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 安装在加密功能的强度方面受到限制,这是由于政策禁止使用大小超过某些值的密钥,例如 AES 为 128。

为了克服这个限制,我们需要配置无限强度管辖策略文件

为此,我们首先需要通过此链接 下载包。之后,我们需要将压缩文件解压缩到我们选择的目录中——其中包含两个 jar 文件:

  • local_policy.jar
  • US_export_policy.jar

最后,我们需要查找*{JAVA_HOME}/lib/security文件夹并将现有策略文件替换为我们在此处提取的文件。 请注意,在 Java 9 中,我们不再需要下载策略文件 package,将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. 准备证书和私钥

在我们进入加密功能的实现之前,我们首先需要创建一个证书和一个私钥。

出于测试目的,我们可以使用以下资源:

Blogdemo.cer是使用国际 X.509 公钥基础结构标准的数字证书,而Blogdemo.p12是包含私钥的受密码保护的PKCS12 密钥库。 让我们看看如何在 Java 中加载它们:

Security.addProvider(new BouncyCastleProvider());
CertificateFactory certFactory= CertificateFactory
  .getInstance("X.509", "BC");
 
X509Certificate certificate = (X509Certificate) certFactory
  .generateCertificate(new FileInputStream("Blogdemo.cer"));
 
char[] keystorePassword = "password".toCharArray();
char[] keyPassword = "password".toCharArray();
 
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(new FileInputStream("Blogdemo.p12"), keystorePassword);
PrivateKey key = (PrivateKey) keystore.getKey("blogdemo", keyPassword);

首先,我们使用addProvider()方法动态地将BouncyCastleProvider添加为安全提供程序。

这也可以通过编辑*{JAVA_HOME}/jre/lib/security/java.security*文件并添加以下行来静态完成:

security.provider.N = org.bouncycastle.jce.provider.BouncyCastleProvider

正确安装提供程序后,我们使用getInstance()方法创建了CertificateFactory对象。

*getInstance()*方法有两个参数;证书类型“X.509”和安全提供者“BC”。

certFactory实例随后用于通过generateCertificate()方法生成X509Certificate对象。

同样,我们创建了一个 PKCS12 Keystore 对象,在该对象上调用*load()*方法。

*getKey()*方法返回与给定别名关联的私钥。

请注意,PKCS12 Keystore 包含一组私钥,每个私钥可以有一个特定的密码,这就是为什么我们需要一个全局密码来打开 Keystore,并且需要一个特定的密码来检索私钥。

证书和私钥对主要用于非对称加密操作:

  • 加密
  • 解密
  • 签名
  • 确认

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类使用 AES CBC 算法创建了一个OutputEncrytor对象。

加密器稍后用于生成封装加密消息的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.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()*方法返回的签名者集合并分别检查每个签名者。

最后,我们使用build()方法创建了一个SignerInformationVerifier对象并将其传递给verify()方法。 如果给定对象可以成功验证签名者对象上的签名,则verify() 方法返回true

这是一个简单的例子:

byte[] signedData = signData(rawData, certificate, privateKey);
Boolean check = verifSignData(signedData);
System.out.println(check);

因此:

true