使用 Bouncy Castle 進行 PGP 加密和解密
1. 概述
在軟體應用程式中,安全性至關重要,其中敏感資料和個人資料的加密和解密是基本要求。所有這些加密 API 都作為 JCA/JCE 的一部分包含在 JDK 中,其他 API 來自第三方庫,例如 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 使用其私鑰解密並讀取訊息。
BouncyCastle是一個實作PGP加密的Java函式庫。
3. 專案設定與依賴關係
在開始加密和解密過程之前,讓我們使用必要的依賴項來設定 Java 項目,並建立稍後需要的 PGP 金鑰對。
3.1. BouncyCastle 的 Maven 依賴項
首先,讓我們建立一個簡單的 Java Maven 專案並新增 BouncyCastle 相依性。
我們將新增[bcprov-jdk15on,](https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on)
其中包含 JCE 提供者和適用於 JDK 1.5 及更高版本的 BouncyCastle Cryptography API 的輕量級 API。另外,我們將新增[bcpg-jdk15on](https://mvnrepository.com/artifact/org.bouncycastle/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>
3.2.安裝GPG工具
我們將使用GnuPG (GPG) 工具產生 ASCII ( .asc
) 格式的 PGP 金鑰對。
如果我們還沒有安裝 GPG,那麼首先在我們的系統上安裝 GPG:
$ sudo apt install gnupg
3.3.產生 PGP 金鑰對
在我們開始加密和解密之前,我們先建立一個 PGP 金鑰對。
首先,我們將運行命令來產生金鑰對:
$ gpg --full-generate-key
接下來,我們需要按照提示選擇密鑰類型、密鑰大小和到期日期。
例如,我們選擇 RSA 作為金鑰類型,2048 作為金鑰大小,到期日為2
年。
接下來,我們將輸入我們的姓名和電子郵件地址:
Real name: baeldung
Email address: [email protected]
Comment: test keys
You selected this USER-ID:
"baeldung (test keys) <[email protected]>"
我們需要設定一個密碼來保護金鑰並確保其強大且唯一。對於 PGP 加密來說,使用密碼並不是嚴格強制的,但出於安全原因,強烈建議您使用密碼。在產生 PGP 金鑰對時,我們可以選擇設定密碼來保護我們的私鑰,從而增加額外的安全層。
如果攻擊者掌握了我們的私鑰,設定強密碼可確保攻擊者在不知道密碼的情況下無法使用它。
讓我們在 GPG 工具提示後建立密碼。對於我們的範例,我們選擇baeldung
作為密碼。
3.4.以 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
組成。
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
是包含純文字訊息的輸入檔的名稱, 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-----
5.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.
六、結論
在本文中,我們學習如何使用 BouncyCastle 函式庫在 Java 中進行 PGP 加密和解密。
首先,我們了解了PGP金鑰對。其次,也是最重要的,我們了解了使用 BouncyCastle PGP 實作對檔案進行加密和解密。
與往常一樣,本文的完整範例程式碼可在 GitHub 上取得。