【问题标题】:Getting BouncyCastle to decrypt a GPG-encrypted message让 BouncyCastle 解密 GPG 加密的消息
【发布时间】:2015-04-11 06:16:25
【问题描述】:

如何让 BouncyCastle 解密 GPG 加密的消息?

我在 CentOS 7 命令行中使用 gpg --gen-key 创建了 GPG 密钥对。我选择 RSA RSA 作为加密类型,并使用gpg --export-secret-key -a "User Name" > /home/username/username_private.keygpg --armor --export 66677FC6 > /home/username/username_pubkey.asc 导出密钥

我能够将username_pubkey.asc 导入另一个电子邮件帐户的远程Thunderbird 客户端,并成功将加密电子邮件发送到username@mydomain.com。但是当我在 mydomain.com 上运行的 Java/BouncyCastle 代码尝试解密 GPG 编码的数据时,会出现以下错误:

org.bouncycastle.openpgp.PGPException:  
Encrypted message contains a signed message - not literal data.

如果您查看下面的代码,您会看到这与 PGPUtils.decryptFile() 中声明 else if (message instanceof PGPOnePassSignatureList) {throw new PGPException("Encrypted message contains a signed message - not literal data."); 的行相对应

原始代码来自the blog entry at this link,尽管我做了一些小改动以使其在 Eclipse Luna 中使用 Java 7 进行编译。链接博客的用户报告了同样的错误,博客作者回复说它不适用于 GPG。 那么我该如何解决这个问题以使其与 GPG 一起使用?

当 GPG-encoded-file 和 GPG-secret-key 传入Tester.testDecrypt() 时,Java 解密代码开始,如下所示:

Tester.java 包含:

public InputStream testDecrypt(String input, String output, String passphrase, String skeyfile) throws Exception {
    PGPFileProcessor p = new PGPFileProcessor();
    p.setInputFileName(input);//this is GPG-encoded data sent from another email address using Thunderbird
    p.setOutputFileName(output);
    p.setPassphrase(passphrase);
    p.setSecretKeyFileName(skeyfile);//this is the GPG-generated key
    return p.decrypt();//this line throws the error
}

PGPFileProcessor.java 包括:

public InputStream decrypt() throws Exception {
    FileInputStream in = new FileInputStream(inputFileName);
    FileInputStream keyIn = new FileInputStream(secretKeyFileName);
    FileOutputStream out = new FileOutputStream(outputFileName);
    PGPUtils.decryptFile(in, out, keyIn, passphrase.toCharArray());//error thrown here
    in.close();
    out.close();
    keyIn.close();
    InputStream result = new FileInputStream(outputFileName);//I changed return type from boolean on 1/27/15
    Files.deleteIfExists(Paths.get(outputFileName));//I also added this to accommodate change of return type on 1/27/15
    return result;
}

PGPUtils.java 包括:

/**
 * decrypt the passed in message stream
 */
@SuppressWarnings("unchecked")
public static void decryptFile(InputStream in, OutputStream out, InputStream keyIn, char[] passwd)
    throws Exception
{
    Security.addProvider(new BouncyCastleProvider());

    in = org.bouncycastle.openpgp.PGPUtil.getDecoderStream(in);

    //1/26/15 added Jca prefix to avoid eclipse warning, also used https://www.bouncycastle.org/docs/pgdocs1.5on/index.html
    PGPObjectFactory 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<PGPPublicKeyEncryptedData> it = enc.getEncryptedDataObjects();
    PGPPrivateKey sKey = null;
    PGPPublicKeyEncryptedData pbe = null;

    while (sKey == null && it.hasNext()) {
        pbe = it.next(); 
        sKey = findPrivateKey(keyIn, pbe.getKeyID(), passwd);
    }

    if (sKey == null) {throw new IllegalArgumentException("Secret key for message not found.");}

    InputStream clear = pbe.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey));

    //1/26/15 added Jca prefix to avoid eclipse warning, also used https://www.bouncycastle.org/docs/pgdocs1.5on/index.html
    PGPObjectFactory plainFact = new JcaPGPObjectFactory(clear);

    Object message = plainFact.nextObject();

    if (message instanceof  PGPCompressedData) {
        PGPCompressedData cData = (PGPCompressedData) message;
        //1/26/15 added Jca prefix to avoid eclipse warning, also used https://www.bouncycastle.org/docs/pgdocs1.5on/index.html
        PGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); 
        message = pgpFact.nextObject();
    }

    if (message instanceof  PGPLiteralData) {
        PGPLiteralData ld = (PGPLiteralData) message;

        InputStream unc = ld.getInputStream();
        int ch;

        while ((ch = unc.read()) >= 0) {out.write(ch);}
    } 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()) {throw new PGPException("Message failed integrity check");}
    }
}

/**
 * Load a secret key ring collection from keyIn and find the private key corresponding to
 * keyID if it exists.
 *
 * @param keyIn input stream representing a key ring collection.
 * @param keyID keyID we want.
 * @param pass passphrase to decrypt secret key with.
 * @return
 * @throws IOException
 * @throws PGPException
 * @throws NoSuchProviderException
 */
public  static PGPPrivateKey findPrivateKey(InputStream keyIn, long keyID, char[] pass)
    throws IOException, PGPException, NoSuchProviderException
{
    //1/26/15 added Jca prefix to avoid eclipse warning, also used https://www.bouncycastle.org/docs/pgdocs1.5on/index.html
    PGPSecretKeyRingCollection pgpSec = new JcaPGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyIn));
    return findPrivateKey(pgpSec.getSecretKey(keyID), pass);

}

/**
 * Load a secret key and find the private key in it
 * @param pgpSecKey The secret key
 * @param pass passphrase to decrypt secret key with
 * @return
 * @throws PGPException
 */
public static PGPPrivateKey findPrivateKey(PGPSecretKey pgpSecKey, char[] pass)
    throws PGPException
{
    if (pgpSecKey == null) return null;

    PBESecretKeyDecryptor decryptor = new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(pass);
    return pgpSecKey.extractPrivateKey(decryptor);
}  

所有三个 Java 文件的完整代码都可以在文件共享网站by clicking on this link 上找到。

可以找到错误的完整堆栈跟踪by clicking on this link

作为参考,远程 Thunderbird 发送者加密的 GUI 说明总结在以下屏幕截图中:

我已经阅读了很多关于此的帖子和链接。尤其是this other SO posting looks similar,却是不同的。我的密钥使用 RSA RSA,但其他帖子没有。

编辑#1

根据@DavidHook 的建议,我已经阅读了SignedFileProcessor,并且我开始阅读更长的RFC 4880。但是,我需要学习实际的工作代码才能理解这一点。大多数通过 google 搜索找到此内容的人还需要工作代码来说明示例。

供参考,@DavidHook 推荐的SignedFileProcessor.verifyFile() 方法如下。 应如何自定义以解决上述代码中的问题?

private static void verifyFile(InputStream in, InputStream keyIn) throws Exception {
    in = PGPUtil.getDecoderStream(in);
    PGPObjectFactory pgpFact = new PGPObjectFactory(in);
    PGPCompressedData c1 = (PGPCompressedData)pgpFact.nextObject();
    pgpFact = new PGPObjectFactory(c1.getDataStream());
    PGPOnePassSignatureList p1 = (PGPOnePassSignatureList)pgpFact.nextObject();
    PGPOnePassSignature ops = p1.get(0);
    PGPLiteralData p2 = (PGPLiteralData)pgpFact.nextObject();
    InputStream dIn = p2.getInputStream();
    int ch;
    PGPPublicKeyRingCollection  pgpRing = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(keyIn));
    PGPPublicKey key = pgpRing.getPublicKey(ops.getKeyID());
    FileOutputStream out = new FileOutputStream(p2.getFileName());
    ops.initVerify(key, "BC");
    while ((ch = dIn.read()) >= 0){
        ops.update((byte)ch);
        out.write(ch);
    }
    out.close();
    PGPSignatureList p3 = (PGPSignatureList)pgpFact.nextObject();
    if (ops.verify(p3.get(0))){System.out.println("signature verified.");}
    else{System.out.println("signature verification failed.");}
}

编辑#2

@DavidHook 推荐的SignedFileProcessor.verifyFile() 方法与我上面代码中的PGPUtils.verifyFile() 方法几乎相同,只是PGPUtils.verifyFile() 复制了extractContentFile 并调用PGPOnePassSignature.init() 而不是PGPOnePassSignature.initVerify()。这可能是由于版本差异。此外,PGPUtils.verifyFile() 返回一个布尔值,而SignedFileProcessor.verifyFile() 为两个布尔值提供 SYSO,并在 SYSO 之后返回 void。

如果我正确解释 @JRichardSnape 的 cmets,这意味着最好在上游调用 verifyFile() 方法以使用发送者的公钥确认传入文件的签名,然后,如果文件上的签名被验证,使用另一种方法使用接收者的私钥解密文件。它是否正确?如果是这样,我该如何重构代码来实现这一点?

【问题讨论】:

  • 哇——这比我想象的要复杂得多!!!我可以暂时说一件事 - 这不是 gpg 端的问题,因为如果您使用 bouncycastle 生成的密钥发送邮件,您会得到相同的结果(我知道您很难做到这一点 cftinyurl.com/p55uxd8),但我可以对此进行测试,并可以使用 BC (Java) 生成的密钥发送消息,Enigmail 将解密该密钥,但此代码显示了同样的问题。问题似乎是代码找到了签名和密钥对,但随后 Inputstream 似乎已用完。调查仍在继续……
  • 这类问题在 bouncy-dev 邮件列表上可能会得到更好的回答,所以如果你在这里没有得到答案...
  • 当然——最后的代码可以正常工作——你需要了解keyIn在这种情况下是发件人的公钥(或者严格来说,我认为用于签署电子邮件的任何密钥的公钥)。这与您的其他代码中的keyIn 不同,后者是接收者的私有(秘密)密钥。但是请注意,根据我对答案的评论,您只会获得电子邮件的文本位 - 如果这是您的最终目标,您需要递归提取附件并解密......
  • @JRichardSnape 我创建了一个聊天室,如果您愿意,可以在其中讨论这个问题。如果您进入房间,我应该在线并且能够看到警报。这是链接:chat.stackoverflow.com/rooms/70813/someroom
  • @JRichardSnape 如果您想写一个答案,我很乐意将其标记为已接受并 +1。当您向我展示如何向解密代码发送未签名但加密的消息时,您解决了这个问题。您帮助我了解我收到错误的原因是消息除了被加密之外还被签名。我认为签署问题需要一个单独的问题。但目前,我正在探索 Outlook。我还将寻找您的 5 个其他帖子,这些帖子值得根据其独立、独立的优点 +1。

标签: java encryption bouncycastle public-key-encryption gnupg


【解决方案1】:

这只是意味着内容已经签名然后加密,提供的例程不知道如何处理它,但至少告诉你。 PGP协议呈现为一系列数据包,其中一些可以包装在其他数据包中(例如压缩数据也可以包装签名数据或简单的文字数据,这些也可以用来生成加密数据,实际内容总是出现在文字数据中)。

如果您查看 Bouncy Castle OpenPGP 示例包中 SignedFileProcessor 中的 verifyFile 方法,您将了解如何处理签名数据并获取包含实际内容的文字数据。

我还建议您查看 RFC 4880,以便您了解该协议的工作原理。该协议非常松散,GPG、BC 和那里的各种产品都反映了这一点——也就是说,松散确实意味着如果您尝试剪切和粘贴您的解决方案,您最终会遇到灾难。并不复杂,但这里也需要理解。

【讨论】:

  • 谢谢。我非常想了解。但我需要更多细节。你能显示代码来解决这个问题吗?我擅长分解工作代码示例。这种材料很复杂。你的段落看起来很深奥,没有工作代码来说明你的意思。
  • 我在 OP 末尾添加了 SignedFileProcessor.verifyFile() 的代码。
  • 这里的问题在于理解 PGP 规范(啊!)。我现在发送了一条经过编码但未签名的消息,并使用您的代码的(稍微修改过的)版本对其进行解码。但是,当消息被签名时,您想要获取的 PGP 对象流(使用 LiteralData 对象)有效地“嵌套”在 CompressedData 对象中。我们需要完全清楚您想要做什么 - 您是否已经从原始电子邮件中获取了电子邮件的附件部分,或者您想在电子邮件到达时完全解码?
  • 基本上-我认为您确实想保存加密的附件,然后将其解码为文件。这更适合您的decryptFile() 代码模型,我认为 可能更适合您的目的。我认为这个问题对于SO来说已经相当长了。也许我们应该在聊天中继续它?
【解决方案2】:

如果有人有兴趣了解如何使用 bouncy castle openPGP 库加密和解密 gpg 文件,请查看以下 java 代码:

以下是您需要的 4 种方法:

以下方法将从 .asc 文件中读取并导入您的密钥:

public static PGPSecretKey readSecretKeyFromCol(InputStream in, long keyId) throws IOException, PGPException {
in = PGPUtil.getDecoderStream(in);
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(in, new BcKeyFingerprintCalculator());

PGPSecretKey key = pgpSec.getSecretKey(keyId);

if (key == null) {
    throw new IllegalArgumentException("Can't find encryption key in key ring.");
}
return key;
}

以下方法将从 .asc 文件中读取并导入您的公钥:

@SuppressWarnings("rawtypes")
public static PGPPublicKey readPublicKeyFromCol(InputStream in) throws IOException, PGPException {
    in = PGPUtil.getDecoderStream(in);
    PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(in, new BcKeyFingerprintCalculator());
    PGPPublicKey key = null;
    Iterator rIt = pgpPub.getKeyRings();
    while (key == null && rIt.hasNext()) {
        PGPPublicKeyRing kRing = (PGPPublicKeyRing) rIt.next();
        Iterator kIt = kRing.getPublicKeys();
        while (key == null && kIt.hasNext()) {
            PGPPublicKey k = (PGPPublicKey) kIt.next();
            if (k.isEncryptionKey()) {
                key = k;
            }
        }
    }
    if (key == null) {
        throw new IllegalArgumentException("Can't find encryption key in key ring.");
    }
    return key;
}

以下2种解密和加密gpg文件的方法:

public void decryptFile(InputStream in, InputStream secKeyIn, InputStream pubKeyIn, char[] pass) throws IOException, PGPException, InvalidCipherTextException {
    Security.addProvider(new BouncyCastleProvider());

    PGPPublicKey pubKey = readPublicKeyFromCol(pubKeyIn);

    PGPSecretKey secKey = readSecretKeyFromCol(secKeyIn, pubKey.getKeyID());

    in = PGPUtil.getDecoderStream(in);

    JcaPGPObjectFactory pgpFact;


    PGPObjectFactory pgpF = new PGPObjectFactory(in, new BcKeyFingerprintCalculator());

    Object o = pgpF.nextObject();
    PGPEncryptedDataList encList;

    if (o instanceof PGPEncryptedDataList) {

        encList = (PGPEncryptedDataList) o;

    } else {

        encList = (PGPEncryptedDataList) pgpF.nextObject();

    }

    Iterator<PGPPublicKeyEncryptedData> itt = encList.getEncryptedDataObjects();
    PGPPrivateKey sKey = null;
    PGPPublicKeyEncryptedData encP = null;
    while (sKey == null && itt.hasNext()) {
        encP = itt.next();
        secKey = readSecretKeyFromCol(new FileInputStream("PrivateKey.asc"), encP.getKeyID());
        sKey = secKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(pass));
    }
    if (sKey == null) {
        throw new IllegalArgumentException("Secret key for message not found.");
    }

    InputStream clear = encP.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey));

    pgpFact = new JcaPGPObjectFactory(clear);

    PGPCompressedData c1 = (PGPCompressedData) pgpFact.nextObject();

    pgpFact = new JcaPGPObjectFactory(c1.getDataStream());

    PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject();
    ByteArrayOutputStream bOut = new ByteArrayOutputStream();

    InputStream inLd = ld.getDataStream();

    int ch;
    while ((ch = inLd.read()) >= 0) {
        bOut.write(ch);
    }

    //System.out.println(bOut.toString());

    bOut.writeTo(new FileOutputStream(ld.getFileName()));
    //return bOut;

}

public static void encryptFile(OutputStream out, String fileName, PGPPublicKey encKey) throws IOException, NoSuchProviderException, PGPException {
    Security.addProvider(new BouncyCastleProvider());

    ByteArrayOutputStream bOut = new ByteArrayOutputStream();

    PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);

    PGPUtil.writeFileToLiteralData(comData.open(bOut), PGPLiteralData.BINARY, new File(fileName));

    comData.close();

    PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.TRIPLE_DES).setSecureRandom(new SecureRandom()));

    cPk.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(encKey));

    byte[] bytes = bOut.toByteArray();

    OutputStream cOut = cPk.open(out, bytes.length);

    cOut.write(bytes);

    cOut.close();

    out.close();
}

现在这里是如何调用/运行上面的:

try {
         decryptFile(new FileInputStream("encryptedFile.gpg"), new FileInputStream("PrivateKey.asc"), new FileInputStream("PublicKey.asc"), "yourKeyPassword".toCharArray());

        PGPPublicKey pubKey = readPublicKeyFromCol(new FileInputStream("PublicKey.asc"));

        encryptFile(new FileOutputStream("encryptedFileOutput.gpg"), "fileToEncrypt.txt", pubKey);




    } catch (PGPException e) {
        fail("exception: " + e.getMessage(), e.getUnderlyingException());
    }

【讨论】:

  • 感谢您和 +1 抽出宝贵时间为这个老问题添加见解。
猜你喜欢
  • 1970-01-01
  • 2018-12-27
  • 2011-09-02
  • 1970-01-01
  • 1970-01-01
  • 2018-02-23
  • 2011-04-12
  • 2012-06-02
  • 1970-01-01
相关资源
最近更新 更多