使用 PGP Bouncy Castle 依赖项创建 CipherOutputStream

问题描述

我想从另一个 OutputStream 创建一个 OutputStream,其中新的 OutputStream自动加密我写入该 OutputStream内容。我想使用 Bouncy Castle,因为我已经将该依赖项用于其他功能

我在互联网上看到了如何使用 Bouncy Castle 加密数据的各种问题,但答案要么加密给定的 File(我不使用文件,我使用 OutputStreams)或有一个我需要复制粘贴的大量代码。我不敢相信这一定有那么难。

这是我的设置:

  1. 我正在使用这个 Bouncy Castle 依赖项 (V1.68)
  2. 我使用的是 Java 8
  3. 我有一个https://pgpkeygen.com/ 生成的公钥和私钥。算法为 RSA,密钥大小为 1024。
  4. 我将公钥和私钥作为文件保存在我的机器上
  5. 我想确保下面的测试通过

我注释掉了一些代码,Cipher 上的 init 函数代码可以编译,但测试失败)。我不知道应该在 init 函数中放入什么作为第二个参数。 读取函数来自:https://github.com/jordanbaucke/PGP-Sign-and-Encrypt/blob/472d8932df303d6861ec494a3e942ea268eaf25f/src/SignAndEncrypt.java#L272。只有 testEncryptDecryptWithoutSigning 是我写的。

代码

@Test
void testEncryptDecryptWithoutSigning() throws Exception {
    // The data will be written to this property
    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    Security.addProvider(new BouncyCastleProvider());

    PGPSecretKey privateKey = readSecretKey(pathToFile("privatekey0"));
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    //cipher.init(Cipher.ENCRYPT_MODE,privateKey);

    CipherOutputStream os = new CipherOutputStream(baos,cipher);
    // I also need to use a PrintWriter
    PrintWriter printWriter =
            new PrintWriter(new BufferedWriter(new OutputStreamWriter(
                    os,StandardCharsets.UTF_8.name())));

    // This is an example of super secret data to write
    String data = "Some very sensitive data";

    printWriter.print(data);
    printWriter.close();

    // At this point,the data is 'inside' the byte array property
    // Assert the text is encrypted
    if (baos.toString(StandardCharsets.UTF_8.name()).equals(data)) {
        throw new RuntimeException("baos not encrypted");
    }

    PGPSecretKey publicKey = readSecretKey(pathToFile("publickey0"));
    //cipher.init(Cipher.DECRYPT_MODE,publicKey);

    ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray());
    ByteArrayOutputStream decrypted = new ByteArrayOutputStream();

    // Decrypt the stream,but how?

    if (!decrypted.toString(StandardCharsets.UTF_8.name()).equals(data)) {
        throw new RuntimeException("Not successfully decrypted");
    }
}

static PGPSecretKey readSecretKey(InputStream input) throws IOException,PGPException
{
    PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
            PGPUtil.getDecoderStream(input),new JcaKeyFingerprintCalculator());

    //
    // we just loop through the collection till we find a key suitable for encryption,in the real
    // world you would probably want to be a bit smarter about this.
    //

    Iterator keyRingIter = pgpSec.getKeyRings();
    while (keyRingIter.hasNext())
    {
        PGPSecretKeyRing keyRing = (PGPSecretKeyRing)keyRingIter.next();

        Iterator keyIter = keyRing.getSecretKeys();
        while (keyIter.hasNext())
        {
            PGPSecretKey key = (PGPSecretKey)keyIter.next();

            if (key.isSigningKey())
            {
                return key;
            }
        }
    }

    throw new IllegalArgumentException("Can't find signing key in key ring.");
}

static PGPSecretKey readSecretKey(String fileName) throws IOException,PGPException
{
    InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));
    PGPSecretKey secKey = readSecretKey(keyIn);
    keyIn.close();
    return secKey;
}

static PGPPublicKey readPublicKey(String fileName) throws IOException,PGPException
{
    InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));
    PGPPublicKey pubKey = readPublicKey(keyIn);
    keyIn.close();
    return pubKey;
}

/**
 * A simple routine that opens a key ring file and loads the first available key
 * suitable for encryption.
 *
 * @param input data stream containing the public key data
 * @return the first public key found.
 * @throws IOException
 * @throws PGPException
 */
static PGPPublicKey readPublicKey(InputStream input) throws IOException,PGPException
{
    PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
            PGPUtil.getDecoderStream(input),in the real
    // world you would probably want to be a bit smarter about this.
    //

    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())
            {
                return key;
            }
        }
    }

    throw new IllegalArgumentException("Can't find encryption key in key ring.");
}

解决方法

作为初步,该网站不会生成一个密钥对,而是三个。从历史上看,在 PGP 中,实际的加密密钥和密钥对以及 PGP 用户所谓的密钥之间一直存在一些歧义,因为给定用户(或实体或角色等)拥有一个“主”或“主”密钥是很常见的,并且一个或多个与该主密钥相关的子密钥。对于 DSA+ElG 密钥,技术上需要使用子密钥(而不是主密钥)进行加密;对于 RSA,这样做被认为是一种很好的做法,因为分开管理(例如可能撤销)这些密钥通常会更好。有些人还认为使用子密钥而不是主密钥来签署数据是一种很好的做法,并且仅使用主密钥来签署密钥(PGP 称之为认证 - C),但有些人不这样做。当 PGP 用户和文档谈论“密钥”时,他们通常指的是一组主密钥及其(所有)子密钥,他们说主密钥或子密钥(或加密子密钥或签名子密钥)表示特定的实际密钥。

当您选择 RSA 时,该网站会生成一个使用 SCEA 的主密钥(密钥对)——即所有用途——以及每个使用 SEA 的两个子密钥——所有用途都对一个子密钥有效。这是荒谬的;如果主密钥支持签名和加密,大多数 PGP 程序将永远不会使用任何子密钥,即使它没有或您覆盖它,子密钥之间也没有有意义的区别,也没有合理的方法来选择使用哪个。

而 BouncyCastle 通过更改术语加剧了这种情况:大多数 PGP 程序使用密钥作为实际密钥或一组主密钥加上子密钥,并且“公共”和“秘密”密钥指代每个密钥的一半 或 组,“keyring”指所有您存储的密钥组,通常在一个文件中,该文件可能用于许多不同的人或实体。然而,Bouncy 将主密钥及其子密钥(公共或秘密形式)的组称为 KeyRing,将可能包含多个组的文件称为 KeyRingCollection,它们都有 Public 和 Secret 变体。总之……

你的第一个问题是你把它倒过来了。public key cryptography中,我们用公钥(一半)加密,用私钥解密 密钥(一半),PGP(以及 BCPG)称之为 secret。此外,因为 PGP 中的私钥/秘密密钥是密码加密的,所以要使用它,我们必须先解密它。 (在 JKS 和 PKCS12 等“普通”JCA 密钥库中也是如此,但在其他密钥库中不一定如此。)

您的第二个问题是类型。虽然给定非对称算法的(特定)PGP 密钥在语义上只是该算法的密钥,加上一些元数据(身份、首选项和信任/签名信息),BCPG 中用于 PGP 密钥的 Java 对象(类)不在 Java 加密体系结构 (JCA) 中用于密钥的对象的类型层次结构中。简单来说,org.bouncycastle.openpgp.PGPPublicKey 不是 java.security.PublicKey 的子类。所以这些关键对象 must be converted 到 JCA 兼容的对象,以便与 JCA 一起使用。

通过这些更改和一些添加,以下代码有效(FSVO 有效):

static void SO66155608BCPGPRawStream (String[] args) throws Exception {
    byte[] plain = "testdata".getBytes(StandardCharsets.UTF_8);
    
    PGPPublicKey p1 = null;
    FileInputStream is = new FileInputStream (args[0]);
    Iterator<PGPPublicKeyRing> i1 = new JcaPGPPublicKeyRingCollection (PGPUtil.getDecoderStream(is)).getKeyRings();
    for( Iterator<PGPPublicKey> j1 = i1.next().getPublicKeys(); j1.hasNext(); ){
        PGPPublicKey t1 = j1.next();
        if( t1.isEncryptionKey() ){ p1 = t1; break; }
    }
    is.close();
    if( p1 == null ) throw new Exception ("no encryption key");
    PublicKey k1 = new JcaPGPKeyConverter().getPublicKey(p1);
    
    Cipher c1 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    c1.init(Cipher.ENCRYPT_MODE,k1);
    ByteArrayOutputStream b1 = new ByteArrayOutputStream();
    CipherOutputStream s1 = new CipherOutputStream(b1,c1);
    s1.write(plain);
    s1.close();
    byte[] cipher = b1.toByteArray();
    long id = p1.getKeyID();
    System.out.println("keyid="+Long.toString(id,16)+" "+Arrays.toString(cipher));
    if( Arrays.equals(cipher,plain) ) throw new Exception ("didn't encrypt!");
    
    PGPSecretKey p2 = null;
    is = new FileInputStream (args[1]); 
    Iterator<PGPSecretKeyRing> i2 = new JcaPGPSecretKeyRingCollection (PGPUtil.getDecoderStream(is)).getKeyRings();
    for( Iterator<PGPSecretKey> j2 = i2.next().getSecretKeys(); j2.hasNext(); ){
        PGPSecretKey t2 = j2.next();
        if( t2.getKeyID() == id ){ p2 = t2; break; }
    }
    is.close();
    if( p2 == null ) throw new Exception ("no decryption key");
    PGPPrivateKey p3 = p2.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().build(args[2].toCharArray()));
    PrivateKey k2 = new JcaPGPKeyConverter().getPrivateKey(p3);
    
    Cipher c2 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    c2.init(Cipher.DECRYPT_MODE,k2);
    ByteArrayInputStream b2 = new ByteArrayInputStream(cipher);
    CipherInputStream s2 = new CipherInputStream(b2,c2);
    byte[] back = new byte[cipher.length]; // definitely more than needed
    int actual = s2.read(back);
    s2.close();
    System.out.println ("Result->" + new String(back,actual,StandardCharsets.UTF_8));
}

(我发现将代码放在执行顺序中的一个地方会更清晰,但您可以将其分解成碎片,而无需进行实质性更改。)

我保留了您的逻辑(来自 Bouncy 示例),即从第一组中选择第一个具有加密功能的公钥(无论是 master 还是 sub),其中一个以上的 Bouncy 错误地调用了 KeyRing;由于您使用的网站上面给出了主密钥 SCEA,因此这始终是主密钥。不可能根据是否允许加密来类似地选择秘密/私钥,并且在任何情况下都不能保证公钥文件始终处于相同的顺序,因此选择解密密钥的正确方法是从用于加密的密钥中匹配 keyid

此外,现代加密算法(像 RSA 这样的非对称和像 AES 或“3DES”这样的对称)产生的数据是任意位模式的,尤其是大多数无效的 UTF-8,因此将这些字节“解码”为 UTF-8与明文进行比较通常会破坏您的数据;如果你想要这个(不必要的)检查,你应该像我展示的那样比较字节数组。

最后,如果您不知道,非对称算法通常不用于加密大数据或可变大小的数据,而这正是您通常使用 Java 流的目的;这也在维基百科文章中进行了解释。这种方法直接使用 RSA PKCS1-v1_5,使用 1024 位密钥,只能处理 117 字节的数据(可能少于 117 个字符,具体取决于)。

如果您希望结果与任何真正的 PGP 实现兼容或可互操作,那绝对不是——这意味着从 PGP 密钥格式转换的工作被浪费了,因为您可以简单地生成 JCA 形式的密钥首先直接按照 Oracle 网站上的基本教程或 Stack 上的数百个示例进行操作。如果你想和 GPG 或类似的东西互操作,你需要使用 BCPG 类进行 PGP 格式的加密和解密,它可以在 plain 字节流上分层,但与 JCA 的 { {1}}。