让BouncyCastle解密GPG加密的消息

5

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

我在CentOS 7命令行上使用“gpg --gen-key”创建了GPG密钥对。我选择了RSA RSA作为加密类型,并使用“gpg --export-secret-key -a“用户名” > /home/username/username_private.key”和“gpg --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.");

这个原始代码来自此链接的博客文章,虽然我进行了微小的更改以使其在使用Java 7的Eclipse Luna中编译。 链接博客的用户报告了同样的错误,博客作者回复说它不适用于GPG。 那么我该如何修复它以使其与GPG配合工作?

当GPG编码文件和GPG密钥传入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文件的完整代码可以通过单击此链接在文件共享网站上找到
错误的完整堆栈跟踪可以通过单击此链接找到
有关远程Thunderbird发送者加密的GUI说明摘要如下所示: 我已经阅读了许多帖子和链接。特别是,这个其他SO帖子看起来相似,但不同。我的Keys使用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()会对两个布尔值进行系统输出,然后返回void。

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


哇 - 这比我想象的要复杂得多!!! 我可以暂时说一件事 - 这不是 gpg 端的问题,因为如果您使用 bouncycastle 生成的密钥发送邮件,则会获得相同的结果(我知道您在这方面有困难,请参见 http://tinyurl.com/p55uxd8),但是我可以测试并发送一个带有 BC(Java)生成的密钥的消息,Enigmail 将解密该消息,但此代码显示相同的问题。 问题似乎是代码找到了签名和密钥对,但然后 Inputstream 似乎已经用尽。 调查仍在继续... - J Richard Snape
2
这类问题最好在bouncy-dev邮件列表中得到回答,所以如果你在这里没有得到答案... - Maarten Bodewes
当然 - 最后的代码会很好用 - 你需要明白,在这种情况下,keyIn发送者的公钥(严格来说,应该是用来签署电子邮件的公钥)。 这与你其他代码中的keyIn不同,后者是接收者的私有(秘密)密钥。 但是请注意,正如我在回答中评论的那样,你只会得到电子邮件的文本部分 - 如果这是您的最终目的,您需要递归提取附件并加密解密... - J Richard Snape
@JRichardSnape,我创建了一个聊天室,并且可以在里面讨论这个问题,如果您愿意的话。我应该在线并能够看到您进入房间的提醒。这是链接:http://chat.stackoverflow.com/rooms/70813/someroom - CodeMed
@JRichardSnape 如果您想撰写答案,我很乐意将其标记为已接受且+1。当您向我展示如何向解密代码发送未签名但已加密的消息时,您解决了这个问题。您帮助我看到错误的原因是消息除了被加密外还被签名了。我认为签名问题值得单独提出一个问题。但现在,我正在探索Outlook。我还会寻找5篇您自己独立、分开的帖子,并对它们进行+1评价。 - CodeMed
2个回答

6

如果有人想知道如何使用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;
}

以下是解密和加密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。 - CodeMed

2
这意味着内容已经被签名并加密,提供的例程不知道如何处理它,但至少告诉您这一点。PGP协议以一系列数据包的形式呈现,其中一些数据包可以套用其他数据包(例如压缩数据也可以套用签名数据或简单文字数据,这些可以用于生成加密数据,实际内容始终显示在文字数据中)。
如果您查看Bouncy Castle OpenPGP示例包中SignedFileProcessor中的verifyFile方法,您将了解如何处理签名数据并获取包含实际内容的文字数据。
我还建议查看RFC 4880,以便了解协议的工作方式。该协议非常松散,GPG、BC和各种产品也反映了这一点,但是这种宽松性意味着如果您试图复制和粘贴解决方案,您将会失败。它并不复杂,但是这里需要理解。

谢谢。我非常想理解。但我需要更多具体信息。你能否展示修复此问题的代码?我擅长分解可工作的代码示例。这个材料很复杂。如果没有工作代码来说明你的意思,你的段落似乎有些深奥。 - CodeMed
我在我的帖子末尾的编辑中添加了 SignedFileProcessor.verifyFile() 的代码。 - CodeMed
这里的问题在于理解PGP规范(呃!)。我现在已经发送了一条编码但未签名的消息,并使用了(稍作修改的)您的代码版本进行解码。然而,当消息被签名时,您想要获取的PGP对象流(带有LiteralData对象)实际上是“嵌套”在CompressedData对象中的。我们需要完全清楚您想要做什么-您是否已经从原始电子邮件中取出了电子邮件的附件部分,还是您想要完全解码电子邮件以确保其与原始邮件完全相同? - J Richard Snape
基本上 - 我认为你真的想要保存加密附件,然后将其解码为文件。这更适合您的decryptFile()代码模型,而且我认为可能更适合您的目的。我认为这个问题对于SO来说变得相当长了。也许我们应该在聊天中继续讨论? - J Richard Snape

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接