创建具有PGP Bouncy Castle依赖关系的CipherOutputStream



我想从另一个OutputStream创建一个OutputStream,其中新的OutputStream将自动加密我写入该OutputStream的内容。我想使用Bouncy Castle,因为我已经在使用其他功能的依赖关系了。

我在互联网上看到了关于如何使用Bouncy Castle加密数据的各种问题,但答案要么是加密给定的File(我不使用文件,我使用OutputStream),要么是有大量代码需要复制粘贴。我不敢相信它一定那么难。

这是我的设置:

  1. 我正在使用这个Bouncy Castle依赖项(V1.68)
  2. 我正在使用Java 8
  3. 我有一个由生成的公钥和私钥https://pgpkeygen.com/.算法为RSA,密钥大小为1024
  4. 我将公钥和私钥保存为计算机上的文件
  5. 我想确保下面的测试通过

我有一些代码被注释掉了,Cipher上的init函数(代码编译了,但测试失败了)。我不知道应该在init函数中放入什么作为第二个参数。读取功能来自:https://github.com/jordanbaucke/PGP-Sign-and-Encrypt/blob/472d8932df303d6861ec494a3e942ea268eaf25f/src/SignAndEncrypt.java#L272.只有testEncryptDecryptWithoutSigning是我写的。

代码:

@Test
void testEncryptDecryptWithoutSigning() throws Exception {
// The data will be written to this property
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Security.addProvider(new BouncyCastleProvider());
PGPSecretKey privateKey = readSecretKey(pathToFile("privatekey0"));
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
//cipher.init(Cipher.ENCRYPT_MODE, privateKey);
CipherOutputStream os = new CipherOutputStream(baos, cipher);
// I also need to use a PrintWriter
PrintWriter printWriter =
new PrintWriter(new BufferedWriter(new OutputStreamWriter(
os,
StandardCharsets.UTF_8.name())));
// This is an example of super secret data to write
String data = "Some very sensitive data";
printWriter.print(data);
printWriter.close();
// At this point, the data is 'inside' the byte array property
// Assert the text is encrypted
if (baos.toString(StandardCharsets.UTF_8.name()).equals(data)) {
throw new RuntimeException("baos not encrypted");
}
PGPSecretKey publicKey = readSecretKey(pathToFile("publickey0"));
//cipher.init(Cipher.DECRYPT_MODE, publicKey);
ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray());
ByteArrayOutputStream decrypted = new ByteArrayOutputStream();
// Decrypt the stream, but how?
if (!decrypted.toString(StandardCharsets.UTF_8.name()).equals(data)) {
throw new RuntimeException("Not successfully decrypted");
}
}
static PGPSecretKey readSecretKey(InputStream input) throws IOException, PGPException
{
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());
//
// we just loop through the collection till we find a key suitable for encryption, in the real
// world you would probably want to be a bit smarter about this.
//
Iterator keyRingIter = pgpSec.getKeyRings();
while (keyRingIter.hasNext())
{
PGPSecretKeyRing keyRing = (PGPSecretKeyRing)keyRingIter.next();
Iterator keyIter = keyRing.getSecretKeys();
while (keyIter.hasNext())
{
PGPSecretKey key = (PGPSecretKey)keyIter.next();
if (key.isSigningKey())
{
return key;
}
}
}
throw new IllegalArgumentException("Can't find signing key in key ring.");
}
static PGPSecretKey readSecretKey(String fileName) throws IOException, PGPException
{
InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));
PGPSecretKey secKey = readSecretKey(keyIn);
keyIn.close();
return secKey;
}
static PGPPublicKey readPublicKey(String fileName) throws IOException, PGPException
{
InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));
PGPPublicKey pubKey = readPublicKey(keyIn);
keyIn.close();
return pubKey;
}
/**
* A simple routine that opens a key ring file and loads the first available key
* suitable for encryption.
*
* @param input data stream containing the public key data
* @return the first public key found.
* @throws IOException
* @throws PGPException
*/
static PGPPublicKey readPublicKey(InputStream input) throws IOException, PGPException
{
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());
//
// we just loop through the collection till we find a key suitable for encryption, in the real
// world you would probably want to be a bit smarter about this.
//
Iterator keyRingIter = pgpPub.getKeyRings();
while (keyRingIter.hasNext())
{
PGPPublicKeyRing keyRing = (PGPPublicKeyRing)keyRingIter.next();
Iterator keyIter = keyRing.getPublicKeys();
while (keyIter.hasNext())
{
PGPPublicKey key = (PGPPublicKey)keyIter.next();
if (key.isEncryptionKey())
{
return key;
}
}
}
throw new IllegalArgumentException("Can't find encryption key in key ring.");
}

作为初步的,该网站不会生成一个密钥对,而是生成三个。从历史上看,在PGP中,实际的加密密钥和密钥对以及PGP用户所称的密钥之间长期存在一些模糊性,因为给定用户(或实体或角色等)通常有一个"主"或"主"密钥以及一个或多个子密钥与该主密钥绑定。对于DSA+ElG密钥,在技术上有必要使用子密钥(而不是主密钥)进行加密;对于RSA来说,这样做被认为是一种良好的做法,因为单独管理(例如可能吊销)这些密钥通常会更好。有些人还认为,使用子密钥而不是主密钥对数据进行签名是一种很好的做法,并且只使用主密钥对密钥进行签名(PGP称之为certification-C),但有些人不这么认为。当PGP用户和文档谈论"密钥"时,他们通常指的是主密钥及其(所有)子密钥的组,他们说主密钥或子密钥(或加密子密钥或签名子密钥)指的是特定的实际密钥。

当您选择RSA时,该网站会生成一个使用SCEA(即所有目的)的主密钥(密钥对),以及两个子密钥(每个子密钥使用SEA),所有目的都对一个子密钥有效。这是荒谬的;如果主密钥支持签名和加密,大多数PGP程序将永远不会使用任何子密钥,即使它没有或您覆盖了它,子密钥之间也没有任何有意义的区别,也没有选择使用哪个的逻辑方法。

BouncyCastle通过更改术语加剧了这一点:大多数PGP程序使用key表示实际密钥或一组主密钥加上如上所述的子密钥,"public"one_answers"secret"密钥表示每个密钥组的一半,"keyring"指代所有您存储的密钥组,通常是在一个文件中,该文件可能适用于许多不同的人或实体。然而,Bouncy将带有子密钥(以公开或秘密形式)的主密钥组称为KeyRing,并将可能包含多个组的文件称为KeyRingCollection,这两个组都有公开和秘密变体。无论如何

你的第一个问题是你把它倒过来了在公钥密码学中,我们使用公钥加密(一半),并使用PGP(以及BCPG)称为机密私钥解密(一半)。此外,由于PGP中的私钥/密钥是密码加密的,因此要使用它,我们必须首先对其进行解密。(在JKS和PKCS12等"正常"JCA密钥库中也是如此,但在其他密钥库中不一定如此。)

您的第二个问题是类型尽管给定非对称算法的(特定)PGP密钥在语义上只是该算法的密钥,加上一些元数据(身份、偏好和信任/签名信息),但PGP密钥的BCPG中的Java对象(类)不在Java加密体系结构(JCA)中用于密钥的对象的类型层次结构中。简单地说,org.bouncycastle.openpgp.PGPPublicKey不是java.security.PublicKey的子类。因此,这些关键对象必须转换为与JCA兼容的对象才能与JCA一起使用。

通过这些更改和一些添加,以下代码可以工作(FSVO工作):

static void SO66155608BCPGPRawStream (String[] args) throws Exception {
byte[] plain = "testdata".getBytes(StandardCharsets.UTF_8);

PGPPublicKey p1 = null;
FileInputStream is = new FileInputStream (args[0]);
Iterator<PGPPublicKeyRing> i1 = new JcaPGPPublicKeyRingCollection (PGPUtil.getDecoderStream(is)).getKeyRings();
for( Iterator<PGPPublicKey> j1 = i1.next().getPublicKeys(); j1.hasNext(); ){
PGPPublicKey t1 = j1.next();
if( t1.isEncryptionKey() ){ p1 = t1; break; }
}
is.close();
if( p1 == null ) throw new Exception ("no encryption key");
PublicKey k1 = new JcaPGPKeyConverter().getPublicKey(p1);

Cipher c1 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
c1.init(Cipher.ENCRYPT_MODE, k1);
ByteArrayOutputStream b1 = new ByteArrayOutputStream();
CipherOutputStream s1 = new CipherOutputStream(b1,c1);
s1.write(plain);
s1.close();
byte[] cipher = b1.toByteArray();
long id = p1.getKeyID();
System.out.println("keyid="+Long.toString(id,16)+" "+Arrays.toString(cipher));
if( Arrays.equals(cipher,plain) ) throw new Exception ("didn't encrypt!");

PGPSecretKey p2 = null;
is = new FileInputStream (args[1]); 
Iterator<PGPSecretKeyRing> i2 = new JcaPGPSecretKeyRingCollection (PGPUtil.getDecoderStream(is)).getKeyRings();
for( Iterator<PGPSecretKey> j2 = i2.next().getSecretKeys(); j2.hasNext(); ){
PGPSecretKey t2 = j2.next();
if( t2.getKeyID() == id ){ p2 = t2; break; }
}
is.close();
if( p2 == null ) throw new Exception ("no decryption key");
PGPPrivateKey p3 = p2.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().build(args[2].toCharArray()));
PrivateKey k2 = new JcaPGPKeyConverter().getPrivateKey(p3);

Cipher c2 = Cipher.getInstance("RSA/ECB/PKCS1Padding");
c2.init(Cipher.DECRYPT_MODE, k2);
ByteArrayInputStream b2 = new ByteArrayInputStream(cipher);
CipherInputStream s2 = new CipherInputStream(b2,c2);
byte[] back = new byte[cipher.length]; // definitely more than needed
int actual = s2.read(back);
s2.close();
System.out.println ("Result->" + new String(back,0,actual,StandardCharsets.UTF_8));
}

(我发现在执行序列中把代码放在一个地方更清楚,但你可以把它分解成碎片,就像你有它一样,没有实质性的改变。)

我保留了你的逻辑(来自Bouncy的例子),即从第一组中选择第一个具有加密功能的公钥,无论是主密钥还是子密钥,其中每个Bouncy都会错误地调用KeyRing;由于根据以上所述,您使用的网站提供了主密钥SCEA,因此这始终是主密钥。根据是否允许加密,不可能类似地选择密钥/私钥,而且在任何情况下都不能保证公钥文件总是按照相同的顺序,因此选择解密密钥的正确方法是从用于加密的密钥中匹配keyid

此外,现代加密算法(既有像RSA这样的不对称算法,也有像AES或"3DES"这样的对称算法)产生的数据是任意比特模式,尤其是大多数无效的UTF-8,因此将这些字节"解码"为UTF-8与明文进行比较通常会损坏您的数据;如果你想要这个(不必要的)检查,你应该比较字节数组,如我所示。

最后,如果您不知道,非对称算法通常不会用于加密大或可变大小的数据,而这正是您通常使用Java流的目的;维基百科的文章也对此进行了解释。这种方法直接使用RSA PKCS1-v1_5,使用1024位密钥,只能处理117字节的数据(根据具体情况,可能少于117个字符)。

如果你希望结果与任何真正的PGP实现兼容或可互操作,那肯定不是——这意味着从PGP密钥格式转换的努力是浪费的,因为你可以按照Oracle网站上的基本教程或Stack上的数百个例子,直接生成JCA表单密钥。如果你想与GPG或类似的东西进行互操作,你需要使用BCPG类进行PGP格式的加密和解密,它可以在字节流上分层,但与JCA的Cipher{Input,Output}Stream完全不同,也不兼容。

最新更新