【发布时间】: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.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."); 的行相对应
原始代码来自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