如何让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()
方法,使用发送方的公钥确认传入文件的签名,然后,如果文件的签名得到验证,则使用另一种方法使用接收方的私钥来解密文件。这正确吗?如果是,我该如何重构代码以实现这一点?
keyIn
是发送者的公钥(严格来说,应该是用来签署电子邮件的公钥)。 这与你其他代码中的keyIn
不同,后者是接收者的私有(秘密)密钥。 但是请注意,正如我在回答中评论的那样,你只会得到电子邮件的文本部分 - 如果这是您的最终目的,您需要递归提取附件并加密解密... - J Richard Snape