8

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.");
}
NoKey
  • 129
  • 11
  • 32
  • 1
    Upvote because you added a failing test. – Software Engineer Feb 11 '21 at 13:27
  • 1
    You tried to use a **PGP** RSA key pair for encryption/decryption in regular RSA algorithm, that won't work. Kindly see the examples that Bouncy Castle provides in their GitHub repository: https://github.com/bcgit/bc-java/tree/master/pg/src/main/java/org/bouncycastle/openpgp/examples. BTW: version 1.46 seems a lit bit outdated... – Michael Fehr Feb 11 '21 at 17:20
  • @MichaelFehr Ah, I did not notice I used an old version. I upgraded to V1.68. I cloned the project and checked usages from 'readPublicKey' in 'PGPExampleUtil'. That is for encrypting files only, like many other examples. I managed to read the public key, but now I am stuck with a `PGPPublicKey` object, which is not a `Key` object and thus can not be used on the `init` function on `Cipher`. – NoKey Feb 11 '21 at 19:40
  • I don't think your approach will work. Maybe a BC collection of examples offers an alternative: [java-crypto-tools-src.zip](https://www.bouncycastle.org/java-crypto-tools-src.zip). Here you can find PGP examples in chapter 13, especially the ex. java-crypto-tools-src\gen\src\main\java\chapter13\PGPEncryptionExample.java might be interesting for you, because it also operates with streams. The example uses `PGPPublicKey` and `PGPPrivateKey` instances that are generated. Alternatively, you can import your existing keys with the `readSecretKey` and `readPublicKey` functions you already use. – Topaco Feb 11 '21 at 20:59
  • Imo, the solution posted in the answer does exactly what you are asking for in your question. Please describe where you think the solution differs from your requirements. Btw, the 3rd argument is the key generation password. – Topaco Feb 13 '21 at 22:05
  • 1
    @Topaco yes you are right, I imported the wrong classes in the example in the answer. I will accept that answer when I can award the bounty – NoKey Feb 14 '21 at 10:50

1 Answers1

7

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.

dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
  • Maybe I am using the wrong imports (they are not shown in the example), but when I wrap things in ArmoredOutputStream, I see the BEGIN PGP MESSAGE. When I try to decode the message in https://8gwifi.org/pgpencdec.jsp, the message is corrupt. Any idea? – NoKey Feb 16 '21 at 12:59
  • And like you said, the example you gave is limited to only 117 bytes. When I use this wrapper over Bouncy Castle: https://github.com/justinludwig/jpgpj I don't have this limit and the message decoding won't fail. Do you know why this happens? – NoKey Feb 16 '21 at 13:15
  • As I said, but adding emphasis, this (mostly useless) format is INCOMPATIBLE WITH PGP AND PGP IMPLEMENTATIONS. Armoring it doesn't change that, it is still incompatible. How PGP encrypts messages is totally different from just running the data through RSA as you asked to; see RFC4880 for complete details. As I said, if you want compatibility with PGP you need code that does PGP format encryption and/or decryption, which can be done with BCPG, but not with a plain JCA Cipher (or CipherStream) for RSA. Which is why using a PGP-format key for this useless encryption is itself useless and silly. – dave_thompson_085 Feb 17 '21 at 03:20
  • **If what you actually want is PGP-format encryption or decryption in Java with BCPG**, see for example https://github.com/bcgit/bc-java/blob/master/pg/src/main/java/org/bouncycastle/openpgp/examples/KeyBasedLargeFileProcessor.java which encrypts to and decrypts from streams -- in that example they are file-based streams, but you can use any valid (type of) stream. – dave_thompson_085 Feb 17 '21 at 03:23
  • I already tried the examples for there, either the output is corrupt (can't decrypt) or the message isn't encrypted properly at all. That is what I am asking, a proper implementation of Bouncy Castle's encrypted OutputStream. – NoKey Feb 18 '21 at 09:27
  • I have used the same logic as the BouncyCastle examples (changing the factoring because I don't like theirs) and it works for me. If you post an MCVE that doesn't work, including key (which obviously should be a test key not a real one) and data (either armored or a form that preserves binary) I'll look at it, but that probably needs to be a new Q not an edit of this one. If you insist on implementing PGP yourself rather than using BouncyCastle, that will be thousands of lines of code, and would take me months to write, which is hugely more than I will do for free. – dave_thompson_085 Feb 19 '21 at 08:21
  • Thanks for your help. I want to use Bouncy Castle, I don't want to create my own. I made this repo: https://gitlab.com/benjamintestapp/pgpbc. Inside the test directory, there is a Working.java, in which I use JPGPJ (it's a wrapper around BC). That test passes. I don't want to use JPGPJ in my real project, I just want to use BC. There is another test in KeyBasedLargeFileProcessor.java (a modified file from the BC example dir) in which I use BC exclusively. That test fails, I added some comments in the code. I don't understand why that test doesn't pass and the wrapper 'just works'. – NoKey Feb 19 '21 at 12:12
  • @dave_thompson_085 referring to the linked GitHub example from Bouncy Castle, "PGP-format encryption or decryption in Java with BCPG" -- Does the `InputStream` retrieved on [line 147](https://github.com/bcgit/bc-java/blob/master/pg/src/main/java/org/bouncycastle/openpgp/examples/KeyBasedLargeFileProcessor.java#L147) read over top of the encrypted `InputStream` argument? Or have the decrypted contents already been flushed to memory? (_eg: underlying `byte[]`_) To put it another way, am I going to blow my memory if my encrypted `InputStream` is 45 GB? Or will it decrypt iteratively as read? – Dan Lugg Feb 08 '23 at 06:44
  • 1
    @DanLugg: it is certainly possible to decrypt PGP incrementally, and IME Bouncy is well coded i.e. does things 'right', but there are too many twisty little classes here for me to easily be certain of that specific point. Why not test -- it would only take a minute or so? – dave_thompson_085 Feb 08 '23 at 17:44
  • @dave_thompson_085 Am about to! ;-) Just figured I'd ask. I'll report back! – Dan Lugg Feb 09 '23 at 21:41
  • @dave_thompson_085 — I was able to write some wrapper providers that seamlessly allowed `InputStream` wrapping without a janky API :-) – Dan Lugg Jul 05 '23 at 06:03