PGP 加解密及 Java/Python 代码演示
说起 PGP(Pretty Good Privacy) 的工作原理,类似早期的 TLS/HTTPS 的 RSA 密钥交换,只不过它不依赖于第三方的 CA,而是直接在两端以信任的方式直接交换了公钥和私钥。 它是基于以下几点进行工作的
- 用工具(如 PGPTool 或 GnuPG) 生成公钥和私钥对,公钥发给加密方,私钥保存在解密方
- 公钥加密的数据只能用相应的私钥解密,这是非对称加密
- 由于非对称加密解密的计算量较大,通常用一个较小随机 Key 对数据进行对称加密,而只对 Key 用公钥进行非对称加密,再把对称加密的数据与非对称加密的 Key 发给解密方,在解密方用私钥解密出 Key, 再对数据进行解密。-- 这称之为混合加密,也是目前 HTTPS 的工作方式
- 另外,在只持有公钥的一方可以对使用私钥加密的数据进行签名验证,验证数据是否是由持有私钥的一方签发的,这就是数字签名的工作原理
- PGP 也会对生成的私钥用一个口令对其进行加密,防止私钥被盗时进一步保护
- 只用私钥可以做加解密与签名验证,如加密的数据能用相同的私钥解密,私钥签名再私钥验证。但交换私钥是不安全的,所以才有了可公开的公钥,非对称加密
下面用 GnuPG 和 Java 的 Bouncy Castle 包 进行演示加解密的过程。
在 mac OS 下安装 pgp 命令
brew install gnupg
用 pgp 命令生成公私钥对
用 gpg --full-generate-key 命令,提示选择时选择默认选项,如
- ECS (sign and encrypt) default
- Curve 25519 default
- 有效期输入 1m
- 输入备注,名称和邮箱
- 最后输入保护私钥的口令,在弹出密码输入窗口这里输入
password123
1gpg --full-generate-key
2gpg (GnuPG) 2.5.21; Copyright (C) 2026 g10 Code GmbH
3This is free software: you are free to change and redistribute it.
4There is NO WARRANTY, to the extent permitted by law.
5
6Please select what kind of key you want:
7 (1) RSA and RSA
8 (2) DSA and Elgamal
9 (3) DSA (sign only)
10 (4) RSA (sign only)
11 (9) ECC (sign and encrypt) *default*
12 (10) ECC (sign only)
13 (14) Existing key from card
14 (16) ECC and Kyber
15Your selection?
16Please select which elliptic curve you want:
17 (1) Curve 25519 *default*
18 (4) NIST P-384
19 (6) Brainpool P-256
20Your selection?
21Please specify how long the key should be valid.
22 0 = key does not expire
23 <n> = key expires in n days
24 <n>w = key expires in n weeks
25 <n>m = key expires in n months
26 <n>y = key expires in n years
27Key is valid for? (0) 1m
28Key expires at Sat Aug 29 11:13:11 2026 CDT
29Is this correct? (y/N) y
30
31GnuPG needs to construct a user ID to identify your key.
32
33Real name: Yanbin
34Email address: yabqiu@gmail.com
35Comment:
36You selected this USER-ID:
37 "Yanbin <yabqiu@gmail.com>"
38
39Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
40We need to generate a lot of random bytes. It is a good idea to perform
41some other action (type on the keyboard, move the mouse, utilize the
42disks) during the prime generation; this gives the random number
43generator a better chance to gain enough entropy.
44We need to generate a lot of random bytes. It is a good idea to perform
45some other action (type on the keyboard, move the mouse, utilize the
46disks) during the prime generation; this gives the random number
47generator a better chance to gain enough entropy.
48gpg: revocation certificate stored as '/Users/yanbin/.gnupg/openpgp-revocs.d/F5209BA90BC24513339DCE0341DF8CD698924E33.rev'
49public and secret key created and signed.
50
51pub ed25519 2026-07-30 [SC] [expires: 2026-08-29]
52 F5209BA90BC24513339DCE0341DF8CD698924E33
53uid Yanbin <yabqiu@gmail.com>
54sub cv25519 2026-07-30 [E] [expires: 2026-08-29]
55 2FAEEA94A20F85C9BACB97F14929F90C8FFBB71C
生成的私钥保存在 ./.gnupg/ 目录下,查看公钥和私钥的命令分别为
1gpg --list-keys
2gpg --list-secret-keys
要在别处用的话需要导公钥和私钥,导出公钥命令为
1gpg --armor --export yabqiu@gmail.com > yanbin_public.asc
导出私钥的命令为
1gpg --armor --export-secret-keys yabqiu@gmail.com > yanbin_private.asc
导出私钥时会提示输入相同的口令,这里是 password123.
完后,查看公私钥文件
1ls -l
2-rw-r--r--@ 1 yanbin root 955 Jul 30 11:22 yanbin_private.asc
3-rw-r--r--@ 1 yanbin root 725 Jul 30 11:22 yanbin_public.asc
注意导出公私钥时邮箱要与生成密钥时的邮箱一致,否则什么也不能导出。
继续用 gpg 命令的话,完全可由它完成加密,解密,签名,验证签名的所有用工作,但我们下面要转入到 Java 代码中来。
Java 使用 PGP 加密
首先创建一个 Maven 项目,并加入 org.bouncycastle 的两个依赖
1<dependency>
2 <groupId>org.bouncycastle</groupId>
3 <artifactId>bcprov-jdk18on</artifactId>
4 <version>1.85</version>
5</dependency>
6<dependency>
7 <groupId>org.bouncycastle</groupId>
8 <artifactId>bcpg-jdk18on</artifactId>
9 <version>1.85</version>
10</dependency>
bcprov 是核心加密库,bcpg 是 OpenPGP 实现模块。
把前面导出的公私钥文件拷贝到 Maven 项目的 src/main/resources/ 目录中,以便后面用 ClassLoader 来读取。完整加密代码如下
1package blog.yanbin;
2
3import org.bouncycastle.bcpg.ArmoredOutputStream;
4import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
5import org.bouncycastle.jce.provider.BouncyCastleProvider;
6import org.bouncycastle.openpgp.*;
7import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
8import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
9import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
10
11import java.io.*;
12import java.security.Security;
13import java.util.Date;
14
15public class PgpEncryptor {
16
17 static {
18 Security.addProvider(new BouncyCastleProvider()); // 不注册的话会得到执行错误:No such provider: BC
19 }
20
21 static byte[] encrypt(byte[] data) throws Exception {
22 InputStream publicKeyInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("yanbin_public.asc");
23 PGPPublicKeyRingCollection pgpPublicKeyRings = new PGPPublicKeyRingCollection(
24 PGPUtil.getDecoderStream(publicKeyInputStream), new JcaKeyFingerprintCalculator());
25 PGPPublicKey publicKey = null;
26 for (PGPPublicKeyRing ring : pgpPublicKeyRings) {
27 for (PGPPublicKey key : ring) {
28 if (key.isEncryptionKey()) {
29 publicKey = key;
30 break;
31 }
32 }
33 if (publicKey != null) {
34 break;
35 }
36 }
37 if (publicKey == null) {
38 throw new IllegalArgumentException("Can't find encryption key in key ring.");
39 }
40 JcePGPDataEncryptorBuilder encryptorBuilder =
41 new JcePGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256)
42 .setWithIntegrityPacket(true)
43 .setSecureRandom(new java.security.SecureRandom())
44 .setProvider("BC");
45 PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(encryptorBuilder);
46 encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider("BC"));
47
48 ByteArrayOutputStream baos = new ByteArrayOutputStream();
49 try (OutputStream out = new ArmoredOutputStream(baos);
50 OutputStream encOut = encGen.open(out, new byte[1024 * 64]);
51 OutputStream literalOut = new PGPLiteralDataGenerator().open(
52 encOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date())) {
53 literalOut.write(data);
54 }
55 return baos.toByteArray();
56 }
57
58 public static void main(String[] args) throws Exception {
59 byte[] encrypted = encrypt("Hello, World!".getBytes());
60 System.out.println(new String(encrypted));
61 }
62}
执行后输出为
1-----BEGIN PGP MESSAGE-----
2Version: BCPG v1.85
3
4wV4DSSn5DI/7txwSAQdAmN8CNPzTl/4Uz++29w7ler6LhHb7NeZuqGWdcJMoiHMw
5kmYb304gNAx7GKQvHp0UXEWb7Fn2i6yYDG4HNTfdQEQaEiBVQ7pKuZIT0Mmo8nIh
60kYB3irP9q1dxIMoxEl9rLQ4GjuKzeG4ezqylLgZNmW5YYqVM4ZD9R1DzgI1EmtY
7xHFhpmi2S8XHlpp+GbxrA+aW2lxM9edE
8=jm4E
9-----END PGP MESSAGE-----
这就是 Hello, World! 加密后的 PGP 密文,包含用随机 Key 对数据进行的对称加密,以及用公钥加密的随机 Key。由于是随机的 Key, 所以每次加密后的数据是不一样的。
Java 使用 PGP 解密
直接解密上面生成的密文,完整代码如下
1package blog.yanbin;
2
3import org.bouncycastle.jce.provider.BouncyCastleProvider;
4import org.bouncycastle.openpgp.*;
5import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
6import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
7import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
8import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
9
10import java.io.ByteArrayInputStream;
11import java.io.ByteArrayOutputStream;
12import java.io.InputStream;
13import java.security.Security;
14import java.util.Iterator;
15
16public class PgpDecryptor {
17
18 static {
19 Security.addProvider(new BouncyCastleProvider()); // 不注册的话会得到执行错误:No such provider: BC
20 }
21
22 private static byte[] decrypt(byte[] data, String passphrase) throws Exception {
23 InputStream privateKeyInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("yanbin_private.asc");
24 PGPSecretKeyRingCollection secretKeyRings = new PGPSecretKeyRingCollection(
25 PGPUtil.getDecoderStream(privateKeyInputStream),
26 new JcaKeyFingerprintCalculator());
27
28 InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(data));
29 PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(decoderStream);
30 Object o = pgpObjectFactory.nextObject();
31 PGPEncryptedDataList enc = (o instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) o :
32 (PGPEncryptedDataList) pgpObjectFactory.nextObject();
33 Iterator<PGPEncryptedData> encryptedDataObjects = enc.getEncryptedDataObjects();
34
35 PGPPrivateKey privateKey = null;
36 PGPPublicKeyEncryptedData encData = null;
37 while (privateKey == null && encryptedDataObjects.hasNext()) {
38 Object next = encryptedDataObjects.next();
39 if (!(next instanceof PGPPublicKeyEncryptedData)) continue;
40 encData = (PGPPublicKeyEncryptedData) next;
41
42 PGPSecretKey secretKey = secretKeyRings.getSecretKey(encData.getKeyID());
43 if (secretKey != null) {
44 privateKey = secretKey.extractPrivateKey(
45 new JcePBESecretKeyDecryptorBuilder()
46 .setProvider("BC")
47 .build(passphrase.toCharArray()));
48 }
49 }
50
51 InputStream clear = encData.getDataStream(
52 new JcePublicKeyDataDecryptorFactoryBuilder().setProvider("BC").build(privateKey));
53
54 PGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);
55 Object message = plainFact.nextObject();
56
57 if (message instanceof PGPCompressedData cData) {
58 PGPObjectFactory compFact = new JcaPGPObjectFactory(cData.getDataStream());
59 message = compFact.nextObject();
60 }
61
62 if (message instanceof PGPLiteralData ld) {
63 try (InputStream unc = ld.getInputStream();
64 ByteArrayOutputStream out = new ByteArrayOutputStream()) {
65 unc.transferTo(out);
66 return out.toByteArray();
67 }
68 } else {
69 throw new PGPException("unknown message type: " + message.getClass());
70 }
71 }
72
73 public static void main(String[] args) throws Exception {
74 byte[] encrypted = """
75 -----BEGIN PGP MESSAGE-----
76 Version: BCPG v1.85
77
78 wV4DSSn5DI/7txwSAQdAmN8CNPzTl/4Uz++29w7ler6LhHb7NeZuqGWdcJMoiHMw
79 kmYb304gNAx7GKQvHp0UXEWb7Fn2i6yYDG4HNTfdQEQaEiBVQ7pKuZIT0Mmo8nIh
80 0kYB3irP9q1dxIMoxEl9rLQ4GjuKzeG4ezqylLgZNmW5YYqVM4ZD9R1DzgI1EmtY
81 xHFhpmi2S8XHlpp+GbxrA+aW2lxM9edE
82 =jm4E
83 -----END PGP MESSAGE-----
84 """.getBytes();
85 byte[] decrypted = decrypt(encrypted, "password123");
86 System.out.println(new String(decrypted));
87 }
88}
成功解密,输出为
Hello, World!
解密的过程就是从加密数据中分离出两部分
- 对称加密的数据
- 非对称加密的 Key
然后用私钥解密出 Key, 然后使用该 Key 对数据进行解密。
注意到解密代码中有个判断是否为 PGPCompressedData
1 if (message instanceof PGPCompressedData cData) {
2 PGPObjectFactory compFact = new JcaPGPObjectFactory(cData.getDataStream());
3 message = compFact.nextObject();
4 }
由于加密时没有使用数据压缩,所以这里的 message 不会是 PGPCompressedData 类型。
在加密时使用数据压缩
只需要在 PgpEncryptor 类中,把
1 try (OutputStream out = new ArmoredOutputStream(baos);
2 OutputStream encOut = encGen.open(out, new byte[1024 * 64]);
3 OutputStream literalOut = new PGPLiteralDataGenerator().open(
4 encOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date())) {
5 literalOut.write(data);
6 }
改为
1 try (OutputStream out = new ArmoredOutputStream(baos);
2 OutputStream encOut = encGen.open(out, new byte[1024 * 64]);
3 OutputStream compOut = new PGPCompressedDataGenerator(PGPCompressedDataGenerator.ZIP).open(encOut);
4 OutputStream literalOut = new PGPLiteralDataGenerator().open(
5 compOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date())) {
6 literalOut.write(data);
7 }
这时候加密前就会对数据进行压缩,解密时就会进入到 PGPCompressedData 的判断分支。对于小数据 Hello World! 启用压缩不会有什么改观,
甚至大小还会略大。
在使用了压缩后 PgpEncryptor 得到的结果是
1-----BEGIN PGP MESSAGE-----
2Version: BCPG v1.85
3
4wV4DSSn5DI/7txwSAQdAopwN3hVpbGkJ4MmDHBEYQiD0HzM4b7ecy+hQTzqt8W8w
5jPgJnyQ70kVLurDz7Ccd10ncN+KTFBzGSayHnfAZeE2tdbqQFFQE6B1/0fSyNJQv
60koBb7Eb9mhLrVMIK/1Txn0NEomAm8NAOm+6z0soft0RVbILZQPdDLkG9xvd/yOz
7El0AA85YvBE6Hf7etQWrVSBaxpzC9Auf5DTXkA==
8=FltD
9-----END PGP MESSAGE-----
其实我们在使用 PGP 加密之前使用 JDK 的 API 对输入数据自行压缩就用不着 Bouncy Castle 的 PGPCompressedDataGenerator 了。
加密时不使用 ArmoredOutputStream
ArmoredOutputStream 的功能是让生成的密文形式为 Base64 文本格式,像前面的
1-----BEGIN PGP MESSAGE-----
2Version: BCPG v1.85
3
4<base64 text>
5-----END PGP MESSAGE-----
我们也可以去掉 ArmoredOutputStream 这一层,类似的,
1 try (OutputStream out = new ArmoredOutputStream(baos);
2 OutputStream encOut = encGen.open(out, new byte[1024 * 64]);
3 OutputStream literalOut = new PGPLiteralDataGenerator().open(
4 encOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date())) {
5 literalOut.write(data);
6 }
改为
1 try (OutputStream encOut = encGen.open(baos, new byte[1024 * 64]);
2 OutputStream compOut = new PGPCompressedDataGenerator(PGPCompressedDataGenerator.ZIP).open(encOut);
3 OutputStream literalOut = new PGPLiteralDataGenerator().open(
4 compOut, PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, data.length, new Date())) {
5 literalOut.write(data);
6 }
这时候生成密文转成 String 就无法阅读了
1System.out.print(encrypt("Hello, World!".getBytes()));
�^I)� ���@*���{�+���;��fRw�\-d7A��f�%L0�n�I��k��Eu�n���61��h��ŗv;�H��ųƿ��˅��}�JC{$�T��h���?�����GO��m�N}e������g�>!�fL�Ά6��������5�WO�$���F�F�
但把密文字节传递给解密代码是没问题的
1 byte[] encrypted = encrypt("Hello World!".getBytes());
2 byte[] decrypted = decrypt(encrypted, "password123");
3 System.out.println(new String(decrypted));
同样能还原出
Hello World!
另外,使用私钥签名,公钥验证签名的代码这里不予涉及。
观察用来加解密的 Key
从 PGP 的加密过程,我们知道 PGP 在加密时随机生成一个密钥,用该密钥对数据进行加密,然后用公钥对该密钥加密后发送给解密方(持有私钥, 可用私钥对非对称加密的密钥进行解密),那我们能看看以上代码生成的密钥。
在 PgpEncryptor 类中适当位置加入代码
1encGen.setSessionKeyExtractionCallback(sessionKey ->
2 System.out.printf("Encryption - Algorithm: %s, Session key: %s%n", sessionKey.getAlgorithm(), Arrays.toString(sessionKey.getKey())));
同时在 PgpDecryptor 类中加入代码
1PGPSessionKey sessionKey = encData.getSessionKey(decryptorFactory);
2System.out.printf("Decryption - Algorithm: %s, Session key: %s%n", sessionKey.getAlgorithm(), Arrays.toString(sessionKey.getKey()));
而后我们执行 PgpDecrptor 就会一同输出
1Encryption - Algorithm: 9, Session key: [-110, -37, -73, 93, -29, 56, -97, -38, 37, -98, 32, 13, 123, -78, 9, 19, -8, -4, -55, -71, 97, 122, -111, 12, 94, 94, -118, -18, 78, -38, 48, 27]
2Decryption - Algorithm: 9, Session key: [-110, -37, -73, 93, -29, 56, -97, -38, 37, -98, 32, 13, 123, -78, 9, 19, -8, -4, -55, -71, 97, 122, -111, 12, 94, 94, -118, -18, 78, -38, 48, 27]
可以发现在解密时正确的还原出了用于加密的密钥,以及对数据加密时相应的算法。
用 Python 还是简单的多
用 Java 代码进行 PGP 加解密的过程比预想的明显示要复杂,我们不妨看看隔壁 Python 的类似实现
如使用库 python-gnupg, 解密的代码如下
1import gnupg
2
3gpg = gnupg.GPG()
4
5with open("yanbin_private.asc", "r") as f:
6 gpg.import_keys(f.read())
7
8decrypted = gpg.decrypt("""
9-----BEGIN PGP MESSAGE-----
10Version: BCPG v1.85
11
12wV4DSSn5DI/7txwSAQdAopwN3hVpbGkJ4MmDHBEYQiD0HzM4b7ecy+hQTzqt8W8w
13jPgJnyQ70kVLurDz7Ccd10ncN+KTFBzGSayHnfAZeE2tdbqQFFQE6B1/0fSyNJQv
140koBb7Eb9mhLrVMIK/1Txn0NEomAm8NAOm+6z0soft0RVbILZQPdDLkG9xvd/yOz
15El0AA85YvBE6Hf7etQWrVSBaxpzC9Auf5DTXkA==
16=FltD
17-----END PGP MESSAGE-----
18""", passphrase="password123")
19
20print(decrypted)
输出
Hello, World!
加密的代码也很简单
1with open("yanbin_public.asc", "r") as f:
2 import_result = gpg.import_keys(f.read())
3
4status = gpg.encrypt(
5 "Hello World!",
6 recipients=import_result.fingerprints[0:],
7 always_trust=True
8)
9encrypted = status.data
10print(encrypted.decode())
输出为
1-----BEGIN PGP MESSAGE-----
2
3hF4DSSn5DI/7txwSAQdAMeC8iDHl5Es82WCmRsZL/tP74KFZY12/E/fXeWJWdS8w
4yeADHLp+KNBo2dt+LFzjSsfWPvTIwzVp2mA8gPxBYm0NSuf0rk0YbxpeLNSjcMzP
51FEBCQIQwV3r9xxi35qxm1r+n6qpMPX9wSEPVccBt1SgbjByAKfsZRlBDkY3QSxy
6J/h5JtKNeHhM9xSz3LHcN0yCbAdA2Ci4NzdS/5LDk3tV/Pk=
7=Paj8
8-----END PGP MESSAGE-----
能用 Python 还是选择 Python 吧。
永久链接 https://yanbin.blog/pgp-encrypt-decrypt-java-python-example/, 来自 隔叶黄莺 Yanbin's Blog[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。