1. 概述

在软件应用中,安全性至关重要,对敏感和个人数据进行加密和解密是基本要求。这些加密API中,一部分作为JCA/JCE的一部分包含在JDK中,另一部分则来自第三方库,如BouncyCastle

在本教程中,我们将学习PGP的基础知识以及如何生成PGP密钥对。此外,我们还将学习如何使用BouncyCastle API在Java中实现PGP加密和解密。

2. 使用BouncyCastle进行PGP加密

PGP(Pretty Good Privacy)加密是一种保护数据机密性的方式,目前可用的OpenPGP Java实现并不多,如BouncyCastle、IPWorks、OpenPGP和OpenKeychain API。如今,当我们谈论PGP时,几乎总是指OpenPGP。

PGP使用两种密钥:

  • 收件人的公钥用于加密消息
  • 收件人的私钥用于解密消息

简而言之,涉及两个参与者:发送方(A)和接收方(B)。

如果A想向B发送加密消息,那么A使用B的公钥通过BouncyCastle PGP加密消息并发送给B。之后,B使用其私钥解密并读取消息。

BouncyCastle是一个实现PGP加密的Java库。

3. 项目设置和依赖

在开始加密和解密之前,让我们先设置Java项目并添加必要的依赖,同时创建后续所需的PGP密钥对。

3.1. BouncyCastle的Maven依赖

首先,我们创建一个简单的Java Maven项目,并添加BouncyCastle依赖。

我们将添加*bcprov-jdk15on,它包含了JDK 1.5及以上版本的BouncyCastle加密API的JCE提供者和轻量级API。此外,我们还会添加bcpg-jdk15on*,这是用于处理OpenPGP协议的BouncyCastle Java API,同样适用于JDK 1.5及以上版本:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.68</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpg-jdk15on</artifactId>
    <version>1.68</version>
</dependency>

3.2. 安装GPG工具

我们将使用GnuPG (GPG)工具来生成ASCII格式(*.asc*)的PGP密钥对。

首先,如果系统中尚未安装GPG,请先安装:

$ sudo apt install gnupg

3.3. 生成PGP密钥对

在深入加密和解密之前,我们先创建一个PGP密钥对。

运行以下命令生成密钥对:

$ gpg --full-generate-key

接下来,根据提示选择密钥类型、密钥大小和过期时间。

例如,我们选择RSA作为密钥类型,2048位作为密钥大小,过期时间设为2年。

然后,输入姓名和邮箱地址:

Real name: baeldung
Email address: baeldung@example.com
Comment: test keys
You selected this USER-ID:
    "baeldung (test keys) <baeldung@example.com>"

我们需要设置一个密码短语来保护密钥,确保其强度和唯一性。虽然PGP加密并不强制要求使用密码短语,但出于安全考虑强烈推荐使用。在生成PGP密钥对时,我们可以选择设置密码短语来保护私钥,增加一层额外的安全保障。

如果攻击者获取了我们的私钥,设置强密码短语可以确保攻击者在不知道密码短语的情况下无法使用该私钥。

当GPG工具提示时,我们创建密码短语。在本例中,我们选择baeldung作为密码短语。

3.4. 导出ASCII格式的密钥

密钥生成后,我们使用以下命令将其导出为ASCII格式:

$ gpg --armor --export baeldung@example.com > public_key.asc

这将创建一个名为public_key.asc的文件,包含我们的公钥。

同样,导出私钥:

$ gpg --armor --export-secret-key baeldung@example.com > private_key.asc

现在,我们得到了ASCII格式的PGP密钥对,包括公钥public_key.asc和私钥private_key.asc

4. PGP加密

在我们的示例中,将有一个包含明文消息的文件。我们将使用PGP公钥加密此文件,并创建一个包含加密消息的文件。

我们参考了BouncyCastle的示例来实现PGP。

首先,创建一个简单的Java类并添加*encrypt()*方法:

public static void encryptFile(String outputFileName, String inputFileName, String pubKeyFileName, boolean armor, boolean withIntegrityCheck) 
  throws IOException, NoSuchProviderException, PGPException {
    // ...
}

这里,outputFileName是输出文件名,文件内容为加密后的消息。

inputFileName是输入文件名,包含明文消息,pubKeyFileName是公钥文件名。

如果armor设置为true我们将使用ArmoredOutputStream,它采用类似Base64的编码,将二进制不可打印字节转换为文本友好的格式。

此外,withIntegrityCheck指定生成的加密数据是否通过完整性包进行保护。

接下来,为输出文件打开流:

OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFileName));
if (armor) {
    out = new ArmoredOutputStream(out);
}

现在,读取公钥:

InputStream publicKeyInputStream = new BufferedInputStream(new FileInputStream(pubKeyFileName));

然后,使用PGPPublicKeyRingCollection类来管理和利用PGP应用中的公钥环,允许我们加载、搜索和使用公钥进行加密。

PGP中的公钥环是一组公钥,每个公钥都与一个用户ID(如邮箱地址)关联。一个公钥环可以包含多个公钥,使用户可以拥有多个身份或密钥对。

我们打开一个密钥环文件,加载第一个可用于加密的密钥:

PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(publicKeyInputStream), new JcaKeyFingerprintCalculator());
PGPPublicKey pgpPublicKey = null;
Iterator keyRingIter = pgpPub.getKeyRings();
while (keyRingIter.hasNext()) {
    PGPPublicKeyRing keyRing = (PGPPublicKeyRing) keyRingIter.next();
    Iterator keyIter = keyRing.getPublicKeys();
    while (keyIter.hasNext()) {
        PGPPublicKey key = (PGPPublicKey) keyIter.next();
        if (key.isEncryptionKey()) {
            pgpPublicKey = key;
            break;
        }
    }
}

接下来,压缩文件并获取字节数组:

ByteArrayOutputStream bOut = new ByteArrayOutputStream();
PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
PGPUtil.writeFileToLiteralData(comData.open(bOut), PGPLiteralData.BINARY, new File(inputFileName));
comData.close();
byte[] bytes = bOut.toByteArray();

此外,创建一个BouncyCastle的PGPEncryptDataGenerator类,用于流式输出和写入数据:

PGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder(PGPEncryptedData.CAST5).setProvider("BC")
  .setSecureRandom(new SecureRandom())
  .setWithIntegrityPacket(withIntegrityCheck);
PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(encryptorBuilder);
encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(pgpPublicKey).setProvider("BC"));
OutputStream cOut = encGen.open(out, bytes.length);
cOut.write(bytes);

最后,运行程序,检查输出文件是否已创建,内容如下:

-----BEGIN PGP MESSAGE-----
Version: BCPG v1.68

hQEMA7Bgy/ctx2O2AQf8CXpfY0wfDc515kSWhdekXEhPGD50kwCrwGEZkf5MZY7K
2DXwUzlB5ORLxZ8KkWZe4O+PNN+cnNy/p6UYFpxRuHez5D+EXnXrI6dIUp1XmSPY
22l0v5ANwn7yveS/3PruRTcR0yv5tD0pQ+rZqH9itC47o9US+/WHTWHyuBLWeVMC
jTCd7nu3p2xtoKqLOMIh0pqQtexMwvLUxRJNjyQl4CTsO+WLkKkktQ+QhA5lirx2
rbp0aR7vIT6qhPjahKln0VX2kbIAJh8JC4rIZXhTGo+U/GDk5ph76u0F3UvhovHN
X++D1Ev6nNtjfKAsYUvRANT+6tHfWmXknsZ2DpH1sNJUAbEAYTBPcKhO3SFdovuN
6fbhoSnChNTBln63h67S9ZXNSt+Ip03wyy+OxV9H1HNGxSHCa+dtvkgZT6KMuEOq
4vBqPdL8vpRT+E60ZKxoOkDyxnKJ
=CYPG
-----END PGP MESSAGE-----

5. PGP解密

在解密部分,我们将使用接收方的私钥解密上一步创建的文件。

首先,创建一个*decrypt()*方法:

public static void decryptFile(String encryptedInputFileName, String privateKeyFileName, char[] passphrase, String defaultFileName) 
  throws IOException, NoSuchProviderException {
    // ...
}

这里,参数encryptedInputFileName是需要解密的文件名。

privateKeyFileName是私钥文件名,passphrase是生成密钥对时设置的密码短语。

defaultFileName是解密文件的默认名称。

为输入文件和私钥文件打开输入流:

InputStream in = new BufferedInputStream(new FileInputStream(encryptedInputFileName));
InputStream keyIn = new BufferedInputStream(new FileInputStream(privateKeyFileName));
in = PGPUtil.getDecoderStream(in);

然后,创建解密流,并使用BouncyCastle的PGPObjectFactory处理OutputStream

JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
PGPEncryptedDataList enc;
Object o = pgpF.nextObject();
// 第一个对象可能是PGP标记包。
if (o instanceof PGPEncryptedDataList) {
    enc = (PGPEncryptedDataList) o;
} else {
    enc = (PGPEncryptedDataList) pgpF.nextObject();
}

此外,我们将使用PGPSecretKeyRingCollection来加载、查找和利用私钥进行解密。 接下来,从文件加载私钥:

Iterator it = enc.getEncryptedDataObjects();
PGPPrivateKey sKey = null;
PGPPublicKeyEncryptedData pbe = null;
PGPSecretKeyRingCollection pgpSec = 
  new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyIn), new JcaKeyFingerprintCalculator());
while (sKey == null && it.hasNext()) {
    pbe = (PGPPublicKeyEncryptedData) it.next();
    PGPSecretKey pgpSecKey = pgpSec.getSecretKey(pbe.getKeyID());
    if(pgpSecKey == null) {
        sKey = null;
    } else {
        sKey = pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC")
          .build(passphrase));
    }
}

现在,一旦获取私钥,我们就用这个私钥解密加密数据或消息:

InputStream clear = pbe.getDataStream(new JcePublicKeyDataDecryptorFactoryBuilder().setProvider("BC")
  .build(sKey));
JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
Object message = plainFact.nextObject();
if (message instanceof PGPCompressedData) {
    PGPCompressedData cData = (PGPCompressedData) message;
    JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
    message = pgpFact.nextObject();
}
if (message instanceof PGPLiteralData) {
    PGPLiteralData ld = (PGPLiteralData) message;
    String outFileName = ld.getFileName();
    outFileName = defaultFileName;
    InputStream unc = ld.getInputStream();
    OutputStream fOut = new FileOutputStream(outFileName);
    Streams.pipeAll(unc, fOut);
    fOut.close();
}
keyIn.close();
in.close();

最后,使用PGPPublicKeyEncryptedData的*isIntegrityProtected()verify()*方法验证数据包的完整性:

if (pbe.isIntegrityProtected() && pbe.verify()) {
    // 成功消息
} else {
    // 完整性检查失败错误消息
}

之后,运行程序,检查输出文件是否已创建,内容为明文:

// 在我们的示例中,解密文件名为defaultFileName,消息为:
This is my message.

6. 总结

在本文中,我们学习了如何使用BouncyCastle库在Java中进行PGP加密和解密。

首先,我们了解了PGP密钥对。其次,也是最重要的,我们学习了使用BouncyCastle PGP实现加密和解密文件。

一如既往,本文的完整示例代码可在GitHub上获取。


原始标题:PGP Encryption and Decryption Using Bouncy Castle | Baeldung