Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create CipherOutputStream with PGP Bouncy Castle dependency

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.");
}
like image 785
NoKey Avatar asked Feb 11 '21 13:02

NoKey


1 Answers

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.

like image 176
dave_thompson_085 Avatar answered Oct 04 '22 01:10

dave_thompson_085