创建具有PGP Bouncy Castle依赖项的CipherOutputStream [英] Create CipherOutputStream with PGP Bouncy Castle dependency

查看:81
本文介绍了创建具有PGP Bouncy Castle依赖项的CipherOutputStream的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

这是我的设置:

  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.

代码:

 <代码> @Testvoid testEncryptDecryptWithoutSigning()引发异常{//数据将被写入该属性ByteArrayOutputStream baos = new ByteArrayOutputStream();Security.addProvider(new BouncyCastleProvider());PGPSecretKey privateKey = readSecretKey(pathToFile("privatekey0&")));密码cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");//cipher.init(Cipher.ENCRYPT_MODE,privateKey);CipherOutputStream os =新的CipherOutputStream(baos,cipher);//我还需要使用PrintWriterPrintWriter printWriter =新的PrintWriter(新的BufferedWriter(新的OutputStreamWriter(os,StandardCharsets.UTF_8.name())));//这是要写入的超级机密数据的示例字符串数据=一些非常敏感的数据";printWriter.print(data);printWriter.close();//此时,数据位于字节数组属性的内部"//声明文本已加密如果(baos.toString(StandardCharsets.UTF_8.name()).equals(data)){抛出新的RuntimeException(未加密的包");}PGPSecretKey publicKey = readSecretKey(pathToFile("publickey0&"));//cipher.init(Cipher.DECRYPT_MODE,publicKey);ByteArrayInputStream inputStream =新的ByteArrayInputStream(baos.toByteArray());解密的ByteArrayOutputStream =新的ByteArrayOutputStream();//解密流,但是如何?如果(!decrypted.toString(StandardCharsets.UTF_8.name()).equals(data)){抛出新的RuntimeException(未成功解密");}}静态PGPSecretKey readSecretKey(InputStream输入)引发IOException,PGPException{PGPSecretKeyRingCollection pgpSec =新的PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(input),new JcaKeyFingerprintCalculator());////我们只遍历整个集合,直到找到真正适合加密的密钥//您可能想对此变得更聪明的世界.//迭代器keyRingIter = pgpSec.getKeyRings();而(keyRingIter.hasNext()){PGPSecretKeyRing keyRing =(PGPSecretKeyRing)keyRingIter.next();迭代器keyIter = keyRing.getSecretKeys();而(keyIter.hasNext()){PGPSecretKey key =(PGPSecretKey)keyIter.next();如果(key.isSigningKey()){返回键;}}}抛出新的IllegalArgumentException(在钥匙圈中找不到签名密钥.");}静态PGPSecretKey readSecretKey(String fileName)引发IOException,PGPException{InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));PGPSecretKey secKey = readSecretKey(keyIn);keyIn.close();返回secKey;}静态PGPPublicKey readPublicKey(String fileName)引发IOException,PGPException{InputStream keyIn = new BufferedInputStream(new FileInputStream(fileName));PGPPublicKey pubKey = readPublicKey(keyIn);keyIn.close();返回pubKey;}/***一个简单的例程,打开一个密钥集文件并加载第一个可用密钥*适用于加密.** @param输入数据流,包含公钥数据* @返回找到的第一个公钥.* @抛出IOException* @抛出PGPException*/静态PGPPublicKey readPublicKey(InputStream输入)引发IOException,PGPException{PGPPublicKeyRingCollection pgpPub =新的PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(input),new JcaKeyFingerprintCalculator());////我们只遍历整个集合,直到找到真正适合加密的密钥//您可能想对此变得更聪明的世界.//迭代器keyRingIter = pgpPub.getKeyRings();而(keyRingIter.hasNext()){PGPPublicKeyRing keyRing =(PGPPublicKeyRing)keyRingIter.next();迭代器keyIter = keyRing.getPublicKeys();而(keyIter.hasNext()){PGPPublicKey key =(PGPPublicKey)keyIter.next();如果(key.isEncryptionKey()){返回键;}}}抛出新的IllegalArgumentException(在密钥环中找不到加密密钥.");} 

解决方案

最初,该网站不会生成 a 密钥对,而是三个.从历史上看,PGP一直以来在实际的加密密钥和密钥对以及PGP用户称为密钥之间一直存在一些歧义,因为给定用户(或实体或角色等)通常拥有一个主"或主"密钥,并且与该主密钥绑定的一个或多个子密钥.对于DSA + ElG密钥,在技术上必须使用子密钥(而不是主密钥)进行加密.对于RSA,这样做是一种很好的做法,因为通常最好分别管理(例如,可能撤消)这些密钥.有人还认为,使用子密钥而不是万能密钥对数据进行签名是一种好习惯,而仅将万能密钥用于对密钥进行签名(PGP将其称为certification-C),但有些人则不这样做.当PGP用户和文档谈论密钥"时,它们通常是指一个主密钥及其(所有)子密钥的组,并且他们说主密钥或子密钥(或加密子密钥或签名子密钥)是指特定的实际密钥.

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

BouncyCastle通过更改术语来加剧这一点:大多数PGP程序将密钥用于实际密钥或一组上述主密钥和子密钥,而'public'和'secret'密钥指的是每个密钥的一半组,以及密钥"来指代 all 您存储的密钥组,通常存储在文件中,该文件可能适用于许多不同的人或实体.然而,Bouncy将主键组及其子键(以公共或秘密形式)称为KeyRing,并将可能包含多个组的文件称为KeyRingCollection,这两个组均具有Public和Secret变体.无论如何...

您的第一个问题是您遇到了麻烦.公钥密码学中我们使用 public 密钥(一半)进行加密,并使用 private 密钥(一半)进行解密,PGP(因此也称为BCPG)将其称为秘密.此外,由于PGP中的私钥/秘钥是用密码加密的,因此要使用它,我们必须先对其进行解密.(在普通" JCA密钥库(如JKS和PKCS12)中也是如此,但在其他情况下则不一定.)

您的第二个问题是类型.尽管给定非对称算法的(特定)PGP密钥在语义上只是该算法的密钥以及一些元数据(身份,首选项和信任/签名信息),PGPG的BCPG中的Java对象(类)不在Java加密体系结构(JCA)中用于密钥的对象的类型层次结构中.用简单的话来说, org.bouncycastle.openpgp.PGPPublicKey 不是 java.security.PublicKey 的子类.因此,这些关键对象" + 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密钥格式进行转换的工作,因为您可能只是生成了JCA形式的密钥首先,直接按照Oracle网站上的基础教程或此处Stack上的数百个示例进行操作.如果要与GPG或类似产品进行互操作,则需要使用BCPG类进行PGP格式的加密和解密,该类可以在 plain 字节流上分层,但与JCA的密码{输入,输出}流.

I want to create a OutputStream from another OutputStream in which the new OutputStream will automatically encrypt the content I write to that OutputStream. I want to use Bouncy Castle since I am already using that dependency for other functionality.

I see various questions over the internet how to encrypt data with Bouncy Castle, but the answers either encrypt a given File (I don't use files, I use OutputStreams) or have a huge amount of code I need to copy paste. I can not believe it must be that difficult.

This is my setup:

  1. I am using this Bouncy Castle dependency (V1.68)
  2. I am using Java 8
  3. I have a public and private key generated by https://pgpkeygen.com/. The algorithm is RSA and the keysize is 1024.
  4. I saved the public key and private key as a file on my machine
  5. I want to make sure the test below passes

I have some code commented out, the init function on Cipher (the code compiles, but the test fails). I don't know what I should put in as second argument in the init function. The read functions are from: https://github.com/jordanbaucke/PGP-Sign-and-Encrypt/blob/472d8932df303d6861ec494a3e942ea268eaf25f/src/SignAndEncrypt.java#L272. Only the testEncryptDecryptWithoutSigning is writting by me.

Code:

@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.");
}

解决方案

As a preliminary, that website doesn't generate a keypair, but three. Historically in PGP there has long been some ambiguity between actual cryptographic keys and keypairs, and what PGP users call keys, because it is common for a given user (or entity or role etc) to have one 'master' or 'primary' key and one or more subkey(s) tied to that masterkey. For DSA+ElG keys it was technically necessary to use a subkey (and not the masterkey) for encryption; for RSA it is considered good practice to do so because it is often better to manage (e.g. potentially revoke) these keys separately. Some people also consider it good practice to use a subkey rather than the masterkey for signing data, and use the masterkey only for signing keys (which PGP calls certifying - C), but some don't. When PGP users and documents talk about a 'key' they often mean the group of a masterkey and (all) its subkey(s), and they say masterkey or subkey (or encryption subkey or signing subkey) to mean a specific actual key.

When you choose RSA that website generates a masterkey (keypair) with usage SCEA -- i.e. all purposes -- AND TWO subkeys each with usage SEA -- all purposes valid for a subkey. This is nonsensical; if the masterkey supports Signing and Encryption most PGP programs will never use any subkey(s), and even if it didn't or you override it, there is no meaningful distinction between the subkeys and no logical way to choose which to use.

And BouncyCastle exacerbates this by changing the terminology: most PGP programs use key for either an actual key or a group of masterkey plus subkeys as above, and 'public' and 'secret' key to refer to the halves of each key or group, and 'keyring' to refer to all the key group(s) you have stored, typically in a file, which might be for many different people or entities. Bouncy however calls the group of a masterkey with its subkeys (in either public or secret form) a KeyRing, and the file containing possibly multiple groups a KeyRingCollection, both of them in Public and Secret variants. Anyway ...

Your first problem is you have it backwards. In public key cryptography we encrypt with the public key (half) and decrypt with the private key (half) which PGP (and thus BCPG) calls secret. Further, because private/secret keys in PGP are password-encrypted, to use it we must first decrypt it. (The same is true in 'normal' JCA keystores like JKS and PKCS12, but not necessarily in others.)

Your second problem is the types. Although a (specific) PGP key for a given asymmetric algorithm is semantically just a key for that algorithm, plus some metadata (identity, preference, and trust/signature information), the Java objects (classes) in BCPG for PGP keys are not in the type hierarchy of the objects used for keys in Java Crypto Architecture (JCA). In simpler words, org.bouncycastle.openpgp.PGPPublicKey is not a subclass of java.security.PublicKey. So these key objects must be converted to JCA-compatible objects to be used with JCA.

With those changes and some additions, the following code works (FSVO work):

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));
}

(I find it clearer to have the code in one place in execution sequence, but you can break it out into pieces as you had it with no substantive change.)

I kept your logic (from Bouncy examples) of choosing the first encryption-capable public key either master or sub from the first group having one which per above Bouncy miscalls a KeyRing; since per above the website you used gives the masterkey SCEA this is always the masterkey. It isn't possible to similarly select a secret/private key depending on whether it allows encryption, and in any case there is no guarantee that the public key file will always be in the same order, so the correct way to choose the decryption key is to match the keyid from the key that was used for encryption.

Also, modern encryption algorithms (both asymmetric like RSA and symmetric like AES or '3DES') produce data that is arbitrary bit patterns, and in particular mostly NOT valid UTF-8, so 'decoding' those bytes as UTF-8 to compare to the plaintext is generally going to corrupt your data; if you want this (unnecessary) check you should instead compare the byte arrays as I show.

Finally, in case you don't know, asymmetric algorithms are not normally used to encrypt data of large or variable size, which is what you would normally use Java streams for; this is also explained in the wikipedia article. This approach, using RSA PKCS1-v1_5 directly, with a 1024-bit key as you have, can only handle 117 bytes of data (which may be fewer than 117 characters, depending).

And if you expect the result to be compatible or interoperable with any real PGP implementation, it definitely isn't -- which means the effort of converting from PGP key format is wasted, because you could have simply generated JCA-form keys directly in the first place, following the basic tutorials on the Oracle website or hundreds of examples here on Stack. If you want to interoperate with GPG or similar, you need to use the BCPG classes for PGP-format encryption and decryption, which can layer on plain byte streams, but are completely different from and incompatible with JCA's Cipher{Input,Output}Stream.

这篇关于创建具有PGP Bouncy Castle依赖项的CipherOutputStream的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆