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