PGP Encryption/Decryption in Java

A new version of the code (and a new post) has been posted at :
http://sloanseaman.com/wordpress/2012/05/13/revisited-pgp-encryptiondecryption-in-java/
Please note that if you are using Bouncy Castles libraries < 1.47 you should use the code in this post

I have a requirement where I need to decrypt a file that is PGP encrypted for a project I am working on. At the same time I need a unit test for my code so I need to encrypt a file as well.

I started digging around and found that there is really no documentation I could find that covered the whole process. Some postings only covered encryption, some decryption, but none covered both as well as how to make the public/private keys.

So, here you go 🙂

The Downloads/Installs

Before you even start coding we need to discuss Java Security. The JRE by default ships with security for cryptography called Java Cryptography Extension (JCE). The JCE will support PGP but because import/export of cryptography can be sketchy, it only supports weak keys by default (think keys that wouldn’t be too hard to crack). PGP won’t work under this environment so you need to first address this.

JCE Update

Credit where due: This part is taken mainly from this article on jce policy files

Go to the Java download page and go all the way to the bottom and download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. The number at the end (currently 6 or 7) is just the JRE version.

Extract the files and copy the two jars to your JRE’s /lib/security directory. This will override two existing jars and will allow you to use strong keys in security.

PGP Tools

Next you need to create you public and private key files. Since Symantec bought PGP in 2010 most of the freeware stuff has disappeared and most links lead to Symantec’s website. I found an old freeware version of PGP (Windows only) at http://www.pgpi.org/products/pgp/versions/freeware/win32/6.5.8/. Please note that newer version don’t work on Windows 7 or are owned by Symantac.

Install the program and then launch it by running PGPKeys.exe. To use it:

  1. Select Keys -> New Key
  2. Click Next
  3. Enter your information
  4. Click Next
  5. Use the Diffie-Hellman/DSS key Pair Type
  6. Click Next
  7. Select your Key Pair Size (I just used 1024 since I’m not that worried about it for now)
  8. Click Next
  9. Leave your Key Expiration as never expires
  10. Click Next
  11. Enter the passphrase (password) you want to use
  12. Click Next
  13. If you entered less than 8 characters you will be warned. Click Next
  14. Key will be generated
  15. Click Next
  16. Click Finish

The files that were generated and that you will need to use in your code can be found by going to Edit -> Options -> Files.
Find these files and copy them to someplace where your code can reach them.

Side note: If you just need a basic public/private key you can generate ones at https://www.igolder.com/pgp/generate-key/

BouncyCastle’s PGP Libraries

I’m using the PGP library from Bouncy Castle. Honestly it’s poorly documented and rather complex but it’s the best and most common one that I could find.

Either download the latest from http://www.bouncycastle.org/latest_releases.html or use maven like so:

    <dependency>
	<groupId>org.bouncycastle</groupId>
	<artifactId>bcpg-jdk16</artifactId>
        <version>1.45</version>
    </dependency>

You are now (finally) ready to actually code something!

The Code

First lets write a general util that will handle any InputStreams (shouldn’t we all be using Readers by now?) and then lets write one that wraps the first one and will handle files for us.

Here is the first util which will handle any type of InputStream:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Iterator;

import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;

/**
 * Taken from org.bouncycastle.openpgp.examples
 *
 * @author seamans
 *
 */
public class PGPUtil {

	@SuppressWarnings("unchecked")
	public static PGPPublicKey readPublicKey(InputStream in) throws IOException, PGPException {
        in = org.bouncycastle.openpgp.PGPUtil.getDecoderStream(in);

        PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(in);

        //
        // 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.
        //
        PGPPublicKey key = null;

        //
        // iterate through the key rings.
        //
        Iterator<PGPPublicKeyRing> rIt = pgpPub.getKeyRings();

        while (key == null && rIt.hasNext()) {
            PGPPublicKeyRing kRing = rIt.next();
            Iterator<PGPPublicKey> kIt = kRing.getPublicKeys();
            while (key == null && kIt.hasNext()) {
                PGPPublicKey k = kIt.next();

                if (k.isEncryptionKey()) {
                    key = k;
                }
            }
        }

        if (key == null) {
            throw new IllegalArgumentException("Can't find encryption key in key ring.");
        }

        return key;
    }

    /**
     * Load a secret key ring collection from keyIn and find the secret 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
     */
    private static PGPPrivateKey findSecretKey(InputStream keyIn, long keyID, char[] pass)
    	throws IOException, PGPException, NoSuchProviderException
    {
        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
        	org.bouncycastle.openpgp.PGPUtil.getDecoderStream(keyIn));

        PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);

        if (pgpSecKey == null) {
            return null;
        }

        return pgpSecKey.extractPrivateKey(pass, "BC");
    }

    /**
     * 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);

        PGPObjectFactory pgpF = new PGPObjectFactory(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 = findSecretKey(keyIn, pbe.getKeyID(), passwd);
        }

        if (sKey == null) {
            throw new IllegalArgumentException("Secret key for message not found.");
        }

        InputStream clear = pbe.getDataStream(sKey, "BC");

        PGPObjectFactory plainFact = new PGPObjectFactory(clear);

        Object message = plainFact.nextObject();

        if (message instanceof  PGPCompressedData) {
            PGPCompressedData cData = (PGPCompressedData) message;
            PGPObjectFactory pgpFact = new PGPObjectFactory(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");
            }
        }
    }

    public static void encryptFile(OutputStream out, String fileName,
        PGPPublicKey encKey, boolean armor, boolean withIntegrityCheck)
        throws IOException, NoSuchProviderException, PGPException
    {
    	Security.addProvider(new BouncyCastleProvider());

        if (armor) {
            out = new ArmoredOutputStream(out);
        }

        ByteArrayOutputStream bOut = new ByteArrayOutputStream();

        PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(
            PGPCompressedData.ZIP);

        org.bouncycastle.openpgp.PGPUtil.writeFileToLiteralData(comData.open(bOut),
            PGPLiteralData.BINARY, new File(fileName));

        comData.close();

        PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(
            PGPEncryptedData.CAST5, withIntegrityCheck,
            new SecureRandom(), "BC");

        cPk.addMethod(encKey);

        byte[] bytes = bOut.toByteArray();

        OutputStream cOut = cPk.open(out, bytes.length);

        cOut.write(bytes);

        cOut.close();

        out.close();
    }

}

I’m not going to go over it line-by-line because, honestly, I haven’t looked at it that hard so I’m not sure what it’s all doing 😉 Sorry.

Next let’s write a util that allows us to easily work with files (and Spring, if you are into that sort of thing :))

import java.io.FileInputStream;
import java.io.FileOutputStream;

public class PGPFileProcessor {

	private String passphrase;

	private String keyFile;

	private String inputFile;

	private String outputFile;

	private boolean asciiArmored = false;

	private boolean integrityCheck = true;

	public boolean encrypt() throws Exception {
		FileInputStream keyIn = new FileInputStream(keyFile);
        FileOutputStream out = new FileOutputStream(outputFile);
        PGPUtil.encryptFile(out, inputFile, PGPUtil.readPublicKey(keyIn),
        	asciiArmored, integrityCheck);
        out.close();
        keyIn.close();
        return true;
	}

	public boolean decrypt() throws Exception {
		 FileInputStream in = new FileInputStream(inputFile);
         FileInputStream keyIn = new FileInputStream(keyFile);
         FileOutputStream out = new FileOutputStream(outputFile);
         PGPUtil.decryptFile(in, out, keyIn, passphrase.toCharArray());
         in.close();
         out.close();
         keyIn.close();
         return true;
	}

	public boolean isAsciiArmored() {
		return asciiArmored;
	}

	public void setAsciiArmored(boolean asciiArmored) {
		this.asciiArmored = asciiArmored;
	}

	public boolean isIntegrityCheck() {
		return integrityCheck;
	}

	public void setIntegrityCheck(boolean integrityCheck) {
		this.integrityCheck = integrityCheck;
	}

	public String getPassphrase() {
		return passphrase;
	}

	public void setPassphrase(String passphrase) {
		this.passphrase = passphrase;
	}

	public String getKeyFile() {
		return keyFile;
	}

	public void setKeyFile(String keyFile) {
		this.keyFile = keyFile;
	}

	public String getInputFile() {
		return inputFile;
	}

	public void setInputFile(String inputFile) {
		this.inputFile = inputFile;
	}

	public String getOutputFile() {
		return outputFile;
	}

	public void setOutputFile(String outputFile) {
		this.outputFile = outputFile;
	}

}

Remember that if you use the PGPFileProcessor in Spring and intend on changing things programmatically via the get/sets you need to make the scope of the bean “prototype” otherwise you will have issues in a multi-threaded environment.

That should do it! You should be able to encrypt and decrypt files (streams really) using PGP!

Support for signed files

If you wish to support signed files, Mateusz Klos posted an excellent comment below that modifies the above code to support signed files.

I didn’t incorporate it into my code because I think he should get full credit for the modifications 🙂

Common Errors

Illegal key size or default parameters

It’s yelling at you because the default JRE security can’t handle PGP.
You didn’t follow the instructions (above) on updating the JCE jars in your JRE’s lib/security directory.

Secret key for message not found

Regenerate you pubring and secring using the PGP tool. Odds are something is wrong with them

The Comments

(Note: Added 4/2/2010)
There had been a lot of great activity in the comments as well as some very kind readers who have posted code snippets to do things that my initial library did not do. I highly suggest you dig through the comments if what you are looking for is not in my original post.

About sseaman

Connect with me on Google+
This entry was posted in Java, Programming and tagged , , , . Bookmark the permalink.

62 Responses to PGP Encryption/Decryption in Java

Leave a Reply to avinash Cancel reply

Your email address will not be published. Required fields are marked *