Bouncy Castle Java PGP加解密

Posted

技术标签:

【中文标题】Bouncy Castle Java PGP加解密【英文标题】:Bouncy Castle Java PGP encryption & decryption 【发布时间】:2021-11-04 12:01:29 【问题描述】:

我正在尝试使用 Bouncy Castle 在 Java 中实现 PGP 加密,还使用了他们提供的一些示例。

但是,如果我尝试解密我刚刚加密的消息,它就不起作用。密钥和解密方法好像还可以,因为我可以使用外部工具(链接)加密,然后在这里成功解密。

    public static void main(String[] args) throws PGPException, IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException 
    Security.addProvider(new BouncyCastleProvider());

    byte[] msg = ("Test blabla BLA").getBytes();

    byte[] encData = encrypt(msg, publicKey);
    System.out.println(new String(encData));

    System.err.println("DECRYPTED");
    System.out.println(decrypt(IOUtils.toInputStream(new String(encData), StandardCharsets.UTF_8), privateKey, passphrase));

    System.err.println("DECRYPTED external");
    String testEncryptedMsg = "-----BEGIN PGP MESSAGE-----\n" +
            "Version: BCPG C# v1.6.1.0\n" +
            "\n" +
            "hQIMA4p9BgtBOg+BARAAiLdmGZBupsSkrji1qdC/y6KACuaWytcjVg3LWudWRlH8\n" +
            "Ye1U0bz+HYc6mLt21RivAZ3p+lPXPzh/h0GVLW/aI2ngJracL+iEA7eam5/H3NQV\n" +
            "2o0CR1mjw4xOf0XwDxaLrnivdeuWniXiZyFN7Ud7lmNG+EmQ0d5ZfR7KEnTJNjSe\n" +
            "FpzwRffBG5lKB4CxF49jBp+Iupdv4A5JzHuDVBkypPMNhS/UEbuPOt5cfmddYVvW\n" +
            "gtDM3rX+ePBhg7d/mVkiDT2KM0Cr7GSrdZ8q83fF4sat3Xp8OHMq61GWriNK4gwb\n" +
            "ViT/vmEsY0n55TVgN5VagMZJJlEThkqWE2wpEjq4HM8pNxR4PvP5inqP5oipn8Os\n" +
            "Nq2a5f77BS7GB9NQ+hnaLSBsywJkZVKqygN81MeHBnNrVXFQ/Hqg24BLwk49bOuH\n" +
            "jTUzorudCnjj/p8/WiyR+PuESkQDLKpo1z9KGEE/rKjTqAwH/fBKJ73XpmAWwd4U\n" +
            "1hv/EdF+XDZv7XwxeVjSIK2inAjdNz+way1WNY3qouo7G+gjKf1gbL14q2H***d/\n" +
            "7mfiW3uWCQ7GhXlHtd1E0WeK2296TdBuSGAvLlVxI4xepsiuVw+zC4BxvAkTXB/a\n" +
            "HyJZhKPksHMRCt8ZxitMfhdvMNGjc4MDhv1MLqKijpagKhgrmDTiJRMb3ng6QLvJ\n" +
            "L2GICyuS4aIsz6XiCQvZG8ebfb0J4uogbb5HxS2fGFT+Y/VgpCnwhwj6LnLBy1Q3\n" +
            "=D/1d\n" +
            "-----END PGP MESSAGE-----\n";
    System.out.println(decrypt(IOUtils.toInputStream(testEncryptedMsg, StandardCharsets.UTF_8), privateKey, passphrase));

我认为问题出在加密方面,但我无法弄清楚它到底是什么。有人有什么想法吗?

public static byte[] encrypt(byte[] bytes, String publicKey) throws IOException 

    ByteArrayOutputStream encOut = new ByteArrayOutputStream();

    try 
        PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(
                new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128)
                        .setSecureRandom(new SecureRandom())
                        .setWithIntegrityPacket(true)
                        .setProvider("BC"));

        PGPPublicKey encryptionKey = readPublicKey(publicKey);
        encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(encryptionKey).setProvider("BC"));

        ArmoredOutputStream aOut = new ArmoredOutputStream(encOut);
        OutputStream cOut = encGen.open(aOut, bytes.length);
        // write out the literal data
        PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
        OutputStream pOut = lData.open(aOut, PGPLiteralData.UTF8, PGPLiteralData.CONSOLE, bytes.length, new Date());
        pOut.write(bytes);
        pOut.close();

        // finish the encryption
        cOut.close();
        aOut.close();

     catch (PGPException e) 
        log.error("Exception encountered while encoding data.", e);
    
    return encOut.toByteArray();


public static String decrypt(InputStream in, String secretKey, String passphrase) throws IOException 
    String content = null;

    try 
        in = PGPUtil.getDecoderStream(in);
        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();
        

        // find the secret key
        Iterator<PGPEncryptedData> it = enc.getEncryptedDataObjects();
        PGPPrivateKey sKey = null;
        PGPPublicKeyEncryptedData pbe = null;
        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(new ByteArrayInputStream(secretKey.getBytes())), new JcaKeyFingerprintCalculator());

        while (sKey == null && it.hasNext()) 
            pbe = (PGPPublicKeyEncryptedData) it.next();
            sKey = findSecretKey(pgpSec, pbe.getKeyID(), passphrase.toCharArray());
        
        if (sKey == null) 
            log.error("Secret key for message not found.");
            throw new IllegalArgumentException("secret key for message not found.");
        

        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;
            content = new String(ld.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
         else if (message instanceof PGPOnePassSignatureList) 
            throw new PGPException("encrypted message contains a signed message - not literal data.");
         else 
            throw new PGPException("message is not a simple encrypted file - type unknown.");
        

        if (pbe.isIntegrityProtected()) 
            if (!pbe.verify()) 
                log.error("message failed integrity check");
             else 
                log.info("message integrity check passed");
            
         else 
            log.warn("no message integrity check");
        
     catch (PGPException e) 
        log.error("Exception encountered while decrypting file.", e);
    
    return content;


private static PGPPrivateKey findSecretKey(PGPSecretKeyRingCollection pgpSec, long keyID, char[] pass) throws PGPException 
    PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
    if (pgpSecKey == null) 
        return null;
    

    return pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(pass));


private static PGPPublicKey readPublicKey(String publicKey) throws IOException, PGPException 
    InputStream input = IOUtils.toInputStream(publicKey, StandardCharsets.UTF_8);

    PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());

    Iterator<PGPPublicKeyRing> keyRingIter = pgpPub.getKeyRings();
    while (keyRingIter.hasNext()) 
        PGPPublicKeyRing keyRing = keyRingIter.next();

        Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
        while (keyIter.hasNext()) 
            PGPPublicKey key = keyIter.next();

            if (key.isEncryptionKey()) 
                return key;
            
        
    

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

【问题讨论】:

首先引起我注意的是流的关闭。您应该始终只关闭最顶层的流,而不是先关闭底层流。尝试谷歌搜索“try-with-resources”。 我不确定您是否应该在打开流时输入bytes.length 作为长度值。我对 1 【参考方案1】:

您的 encrypt() 方法代码中有一些错误:

首先,在这一行中,您将直接在装甲输出流上打开文字数据流,而您应该在加密流上打开它:

OutputStream pOut = lData.open(aOut, PGPLiteralData.UTF8, PGPLiteralData.CONSOLE, bytes.length, new Date());
// replace with
OutputStream pOut = lData.open(cOut, PGPLiteralData.UTF8, PGPLiteralData.CONSOLE, bytes.length, new Date());

此外,您应该替换该行

OutputStream cOut = encGen.open(aOut, bytes.length);
// with
OutputStream cOut = encGen.open(aOut, new byte[1<<9]);

否则你会遇到编码问题。 这些更改应该可以让您的代码运行。

不过,我建议使用诸如 PGPainless 之类的库(免责声明:此处为作者),它会为您完成所有低级流包装和检查。最重要的是,它会进行正确的签名验证,这是大多数其他基于 Bouncycastle 的库所缺乏的。

【讨论】:

【参考方案2】:

这是最终对我有用的加密方法。 它与 BCFipsIn100 书中提供的示例一起帮助调试。

public byte[] encrypt(byte[] bytes, String publicKey) throws IOException 

    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) 
        byte[] compressedData = compress(bytes);

        PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(
                new JcePGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
                        .setWithIntegrityPacket(true)
                        .setSecureRandom(new SecureRandom())
                        .setProvider(PROVIDER));

        PGPPublicKey encryptionKey = readPublicKey(publicKey);
        encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(encryptionKey).setProvider(PROVIDER));

        ArmoredOutputStream aOut = new ArmoredOutputStream(byteArrayOutputStream);
        OutputStream cOut = encGen.open(aOut, compressedData.length);
        cOut.write(compressedData);
        cOut.close();
        aOut.close();

        return byteArrayOutputStream.toByteArray();
     catch (PGPException e) 
        log.error("Exception encountered while encoding data.", e);
    

    return new byte[0];


private static byte[] compress(byte[] clearData) throws IOException 
    try (ByteArrayOutputStream bOut = new ByteArrayOutputStream()) 
        PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
        OutputStream cos = comData.open(bOut);

        PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
        OutputStream pOut = lData.open(cos,
                PGPLiteralData.BINARY,
                PGPLiteralData.CONSOLE,
                clearData.length,
                new Date());

        pOut.write(clearData);
        pOut.close();

        comData.close();

        return bOut.toByteArray();
    

【讨论】:

以上是关于Bouncy Castle Java PGP加解密的主要内容,如果未能解决你的问题,请参考以下文章

Bouncy Castle 从公钥加密会话数据包中提取 PGP 会话密钥

为 Bouncy Castle C# API 使用多个键

如何使用Bouncy Castle Crypto API来加密和解密数据

在 C# 中使用 Bouncy Castle 加密/解密

使用 RSA Bouncy Castle 加密/解密无法正常工作

如何使用Bouncy Castle解密AES / CCM加密密文?