无法 gpg 解密 BouncyCastlePGP 加密的消息

Fail to gpg-decrypt BouncyCastlePGP-encrypted message

当我尝试使用通过 BouncyCastle 加密的 GnuPG 解密消息时,我收到两条 gpg: [don't know]: invalid packet (ctb=xx) 消息并且解密失败。

我正在使用 BouncyCastle 1.54gpg (GnuPG) 2.0.30 on OSX

详情

1) PGP 密钥是使用 gpg 生成的,如下所示:

$ gpg --gen-key
gpg (GnuPG) 2.0.30; Copyright (C) 2015 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048)
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Foo Bar
Email address: foo@bar.com
Comment: Test Key
You selected this USER-ID:
    "Foo Bar (Test Key) <foo@bar.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
You need a Passphrase to protect your secret key.

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
public and secret key created and signed.

pub   2048R/79CC322A 2017-08-17
      Key fingerprint = 93B9 0D06 08D2 EB84 9F83  4CD3 A470 748E 79CC 322A
uid       [ unknown] Foo Bar (Test Key) <foo@bar.com>
sub   2048R/21B41E21 2017-08-17
$ 

2) 以 asc 格式导出的 PGP public 密钥如下所示:

-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2

mQENBFmWE88BCADB+esy+D2Zobru86ztUTp7hWyDy5w9B2iHlyORLI6mQ0JH+ya6
cbaO9nWT7WC68l1ocWaeak0t/9hx0CDWv6zdAXfEUPu8qB53M4m1NACDJp3UDeQS
UytmpB/kutViUO+yiRhtezYsFP70PHOO7o9Tgze5H/qF/hgAVmk1/eN4oSTH4hqo
VIvlsvTNZYLt+a2wYN0RPPryfXnvrKOSXdE5roQd/TMaMwwd2Mbhhh31IB1ROw2/
I+tvJfrM4zLseuo0ndYmWL7ZfjPslsFUezUQf5BDmFdIi9M3/UWHtHmSR6GGG1ko
uo0XjABiIjwfj1mQSA3txI1yO+RLHYrI5UL1ABEBAAG0IEZvbyBCYXIgKFRlc3Qg
S2V5KSA8Zm9vQGJhci5jb20+iQE5BBMBCAAjBQJZlhPPAhsDBwsJCAcDAgEGFQgC
CQoLBBYCAwECHgECF4AACgkQpHB0jnnMMipk+Qf/cKLSdv+aYHKteS/rtAXQbRWG
5nEGKk8vU5HseESr9tDbzkMpruYXtGG99GjPjZO2NqoYF+3NzD6suiQzP95dHyH0
g3i2AHLoyd7W9VvZEieH7vGRrUjYYP2N/qifUepdiu3gKQLHDBE14tXRHEfTN5WV
BxClZ4MwmMvHzsg6NB2RXJb7t5e9apgQ/+0O9l5LwGsSSkCQr53OmoMSUnQP0lrz
gqMxzbC6sI/FYaVaoMGnAUMFQ+8l7nI/Tv6R/sCN5d4egcIC1AzVcDE+zGkwRrmX
4lcKen0XvolJfpUNx9D2CIAQttz0nvygxjBAByXT4oFcbPw9GZCNmxKo/eUp4LkB
DQRZlhPPAQgAz4tebylTFXZCj4xwzy7wyrw4J+vUkMrOh5tOVgISPMaEiDBzFyhf
oqs0uAamImUyF2HGVGXEGappZncjrympdYFpDXG6jb0oYap03hheli1R6h+56PNq
zNyVzlfq85BqgVa5Qed1VnDbEz29JMjLwSvCEY3V0SIOgVZxE5GfMjvAkUhPoE+o
T5uVuRUlnfdKXMYgNRh5gbNEsvx+PMGihC3pSrWWYbBtU6otNXCVCYQryORaHWAf
pvB99P6YzP0nc6Dbu2ZSiGsDAQwa3ZCow+E997upf22WMfowwkNFtarnrr1fVLkl
exR28nJdeAbh0R3WpgQJqeCGmH9fYQEBjwARAQABiQEfBBgBCAAJBQJZlhPPAhsM
AAoJEKRwdI55zDIqgAAH/39teAauUrB+xEVr/Q+McXa0PQSrErB2P4jaVIBuZH2/
6EeRycp6bIwc5R/gpkIVcPg8DmDYtobRbj2YDc5o+4tPVgSKJUgOB2l9CryP+aCm
lx8R0nCN8q53vqtmm7LYc+W1K6tXQJZi8VbCC9sLGUxH0HuDP2ldcBnrialV2cLZ
zsxRgoFF3f8u/We97z2qwzRQXshvw4GVL3wSs511mTcOZW8LnO1YMt3m5ABIsZN0
P/avr7zEqsvr0iJOQ7WbJLbgHkxKu03SfcN0XfVDX4VzqykLvn9THHVtofU6h4LE
LoIRKGg57DXFrsc93a1GWzN9z764sXQ7JgWqnW6a72g=
=n8OZ
-----END PGP PUBLIC KEY BLOCK-----

3) PGP public 密钥以其 "PUBLIC KEY BLOCK" 形式传入并解析如下: (pgpKeyBytes 是密钥的字符串表示的 UTF-8 字节编码)

private PGPPublicKey resolvePgpPublicKey(byte[] pgpKeyBytes) throws IOException, PGPException {
  PGPPublicKeyRingCollection keyRingCollection;
  try (InputStream in = PGPUtil.getDecoderStream(new ByteArrayInputStream(pgpKeyBytes))) {
    keyRingCollection = new PGPPublicKeyRingCollection(
        PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());
  }

  Iterator<?> keyRingIterator = keyRingCollection.getKeyRings();
  while (keyRingIterator.hasNext()) {
    PGPPublicKeyRing keyRing = (PGPPublicKeyRing) keyRingIterator.next();

    Iterator<?> keyIterator = keyRing.getPublicKeys();
    while (keyIterator.hasNext()) {
      PGPPublicKey key = (PGPPublicKey) keyIterator.next();
      if (key.isMasterKey()) {
        continue;
      }
      if (key.isEncryptionKey()) {
        return key;
      }
    }
  }
  throw new ServiceRequestException("Cannot resolve PGPPublicKey");
}

4) 使 PGP 加密的是 16 字节数组的十六进制字符串表示(作为 keyBytes)如下:

private byte[] encryptKeyBytes(byte[] keyBytes, byte[] pgpKey) throws GeneralSecurityException {
  ByteArrayOutputStream encKeyBytes = new ByteArrayOutputStream(keyBytes.length);

  try (Handle<SecureRandom> randomHandle = RngSupport.getRandom()) {
    JcePGPDataEncryptorBuilder encryptorBuilder =
        new JcePGPDataEncryptorBuilder(PGPEncryptedDataGenerator.AES_256);
    encryptorBuilder.setWithIntegrityPacket(true);
    encryptorBuilder.setSecureRandom(randomHandle.getObject());
    encryptorBuilder.setProvider("BC");

    PGPEncryptedDataGenerator encryptor = new PGPEncryptedDataGenerator(encryptorBuilder);
    try {
      JcePublicKeyKeyEncryptionMethodGenerator keyEncryptionMethodGenerator =
          new JcePublicKeyKeyEncryptionMethodGenerator(resolvePgpPublicKey(pgpKey));
      keyEncryptionMethodGenerator.setProvider("BC");
      encryptor.addMethod(keyEncryptionMethodGenerator);
      try (
          OutputStream ao = new ArmoredOutputStream(encKeyBytes);
          OutputStream eo = encryptor.open(ao, keyBytes.length)) {
        eo.write(BytesSupport.encodeHex(keyBytes).getBytes(StandardCharsets.UTF_8));
      }
    } catch (ServiceRequestException e) {
      throw e;
    } catch (Exception e) {
      throw new GeneralSecurityException("Cannot perform PGP encryption", e);
    }
  }

  return encKeyBytes.toByteArray();
}

5) 此加密的示例结果如下所示:

-----BEGIN PGP MESSAGE-----
Version: BCPG v@RELEASE_NAME@

hQEMA0a1HkkhtB4hAQf+MfDa3ILJJivDYO+V9GwLDXMq1Oi8YFe/oNfScT2KT6aG
rKBIaCQvwTQPD95QS3lo9sRZYvD64C7+Y+PA2e4nSJYNiLmyEczqFVzSgoI8ibhD
LDG+trkAgEd3UiSltju8oF/d5SUPaubVrfH413xZ2xg5lbx7z78U4KtAZ1IMk/XN
DN2nCaOVIw/EbqzVt8YCdDpQRrnfh1ZB5lDmLYoRuJykQ08UCrxv9dyQN3wpOX/G
K7Nq3w6Q6+vT8LiP9iA7NEEu3BObNHAQ371VQ4uJQaZOysxPAH/RFHDHRtDa6R/J
F6ca8z28mliQdZxFpyrqKgPwjrsthZ4BApUnHZ6dm9I5ATnr4m4jPQefMeSgGLYD
I3Mk82COdct8ZNk108bOQjSAl+CPlARb53pItOZm21PL1lVwzcq1IXvUjJYjEOSA
6SccakqFhF8cgQ==
=A56Z
-----END PGP MESSAGE-----

6) 当我尝试解密消息时,我收到两条 "gpg: [don't know]: invalid packet" 消息和 解密失败:

$ gpg -vv --decrypt /tmp/ct.asc
gpg: armor: BEGIN PGP MESSAGE
Version: BCPG v@RELEASE_NAME@
:pubkey enc packet: version 3, algo 1, keyid 46B51E4921B41E21
    data: [2046 bits]
gpg: armor header:
gpg: public key is 21B41E21
gpg: using subkey 21B41E21 instead of primary key 79CC322A

You need a passphrase to unlock the secret key for
user: "Foo Bar (Test Key) <foo@bar.com>"
gpg: using subkey 21B41E21 instead of primary key 79CC322A
2048-bit RSA key, ID 21B41E21, created 2017-08-17 (main key ID 79CC322A)

gpg: no running gpg-agent - starting one
gpg: public key encrypted data: good DEK
:encrypted data packet:
    length: 57
    mdc_method: 2
gpg: encrypted with 2048-bit RSA key, ID 21B41E21, created 2017-08-17
      "Foo Bar (Test Key) <foo@bar.com>"
gpg: AES256 encrypted data
gpg: [don't know]: invalid packet (ctb=39)
gpg: decryption okay
gpg: [don't know]: invalid packet (ctb=44)
$

encryptKeyBytes 实现的问题,即构建加密 PGP 消息的地方,是它按原样写入纯文本消息八位字节,而不是 literal data,因此解密尝试期间的协议错误。

正确的实现如下:

private byte[] encryptKeyBytes(String keyName, byte[] keyBytes, byte[] pgpKey)
    throws GeneralSecurityException {
  ByteArrayOutputStream encKeyBytes = new ByteArrayOutputStream(keyBytes.length);

  try (Handle<SecureRandom> randomHandle = RngSupport.getRandom()) {
    JcePGPDataEncryptorBuilder encryptorBuilder =
        new JcePGPDataEncryptorBuilder(PGPEncryptedDataGenerator.AES_256);
    encryptorBuilder.setWithIntegrityPacket(true);
    encryptorBuilder.setSecureRandom(randomHandle.getObject());
    encryptorBuilder.setProvider("BC");

    PGPEncryptedDataGenerator encryptor = new PGPEncryptedDataGenerator(encryptorBuilder);
    try {
      JcePublicKeyKeyEncryptionMethodGenerator keyEncryptionMethodGenerator =
          new JcePublicKeyKeyEncryptionMethodGenerator(resolvePgpPublicKey(pgpKey));
      keyEncryptionMethodGenerator.setProvider("BC");
      encryptor.addMethod(keyEncryptionMethodGenerator);

      PGPLiteralDataGenerator dataGenerator = new PGPLiteralDataGenerator();
      byte[] data = BytesSupport.encodeHex(keyBytes).getBytes(StandardCharsets.UTF_8);
      try (
          OutputStream ao = new ArmoredOutputStream(encKeyBytes);
          OutputStream eo = encryptor.open(ao, keyBytes.length);
          OutputStream go = dataGenerator.open(
              eo, PGPLiteralData.UTF8, keyName, data.length, new Date())) {
        go.write(data);
      }
    } catch (ServiceRequestException e) {
      throw e;
    } catch (Exception e) {
      throw new GeneralSecurityException("Cannot perform PGP encryption on the content key", e);
    }
  }

  return encKeyBytes.toByteArray();
}

注意 PGPLiteralDataGenerator 的使用,它是提供写入消息字节的输出流的抽象。