使用 Bouncy Castle 进行 PGP 加密和解密

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

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

对于软件应用程序而言,安全性至关重要,因为加密和解密敏感数据和个人数据是基本要求。所有这些加密 API 都作为 JCA/JCE 的一部分包含在 JDK 中,其他 API 则来自第三方库,例如BouncyCastle

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

使用 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 库。

项目设置和依赖项
在开始加密和解密过程之前,让我们使用必要的依赖项设置我们的 Java 项目并创建我们稍后需要的 PGP 密钥对。

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

我们将添加bcprov-jdk15on,它包含 JCE 提供程序和适用于 JDK 1.5 及更高版本的 BouncyCastle 加密 API 的轻量级 API。此外,我们还将添加bcpg-jdk15on,它是用于处理 OpenPGP 协议的 BouncyCastle Java API,包含适用于 JDK 1.5 及更高版本的 OpenPGP API:

<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>

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

如果我们还没有在系统上安装 GPG,那么首先安装它:

$ sudo apt install gnupg

生成 PGP 密钥对
在我们进行加密和解密之前,让我们首先创建一个 PGP 密钥对。

首先,我们将运行命令来生成密钥对:

$ gpg --full-generate-key

接下来我们需要按照提示选择密钥类型、密钥大小以及有效期。

例如,我们选择 RSA 作为密钥类型、2048 作为密钥大小以及有效期为2 年。

接下来,我们输入我们的姓名和电子邮件地址:

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

我们需要设置密码来保护密钥,并确保其强大且唯一。使用密码对于 PGP 加密并非严格强制要求,但出于安全原因,强烈建议使用密码。生成 PGP 密钥对时,我们可以选择设置密码来保护我们的私钥,从而增加额外的安全层。

如果攻击者掌握了我们的私钥,设置强密码可确保攻击者在不知道密码的情况下无法使用它。

根据 GPG 工具的提示,我们来创建密码。在本例中,我们选择baeldung作为密码。

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

$ gpg --armor --export <our_email_address> > public_key.asc

这将创建一个名为public_key.asc的文件,其中包含 ASCII 格式的公钥。

以同样的方式,我们将导出私钥:

$ gpg --armor --export-secret-key <our_email_address> > private_key.asc


现在我们得到了一个 ASCII 格式的 PGP 密钥对,由一个公钥public_key.asc和一个私钥private_key.asc组成。

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是包含纯文本消息的输入文件的名称,publicKeyFileName是公钥文件名的名称。

在这里,如果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(encKey).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-----

PGP 解密
作为解密的一部分,我们将使用收件人的私钥解密上一步中创建的文件。

首先,我们将创建一个decrypt()方法:

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

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

接下来,privateKeyFileName是私钥的文件名,passphrase是在生成密钥对时选择的秘密密码。

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

让我们在输入文件和私钥文件上打开一个输入流:

InputStream in = new BufferedInputStream(new FileInputStream(inputFileName));
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();
// The first object might be a PGP marker packet.
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();
}
privateKeyInStream.close();
instream.close();

最后,我们将使用PGPPublicKeyEncryptedData的isIntegrityProtected()和verify()方法来验证数据包的完整性:

if (pbe.isIntegrityProtected() && pbe.verify()) {
    // success msg
} else {
   
// Error msg for failed integrity check
}

之后,让我们运行程序来查看输出文件是否以我们的文件名创建,以及内容是否是纯文本:

//In our example, decrypted file name is defaultFileName and the msg is:
This is my message.