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 是核心加密库,bcpgOpenPGP 实现模块。

把前面导出的公私钥文件拷贝到 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!

解密的过程就是从加密数据中分离出两部分

  1. 对称加密的数据
  2. 非对称加密的 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) 进行许可。