PGP Cryptography With The Legion of the Bouncy Castle – Part 3

Posted on Updated on

Continuing my 5 part series on using the Bouncy Castle API to create your own Java library to manage Private / Public key pairs. Part 2 went over digitally signing and verifying messages using the generated key pair from Part 1. Also, just a note, the signature generation from Part 2 are not detached signatures, they’re embedded within the information. I’ll talk about why I prefer to do this when we get to Part 4.

Encryption is done using the recipients public key and does not require us to enter a pass phrase. When the recipient receives the encrypted message they will need to enter their pass phrase in order to decrypt the message. This is mainly so because their public key is protected by password based symmetric encryption inside the key ring.

Below we add the encrypt and decrypt methods to our PGPCryptoTools source file.

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.util.Iterator;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
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.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
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.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
import org.bouncycastle.util.io.Streams;

/**
 *
 * Copyright George El-Haddad
 * <b>Time stamp:</b> Dec 10, 2012 - 09:30:12 AM

 * @author George El-Haddad
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
public final class PGPCryptoTools {

	static {
		Security.addProvider(new BouncyCastleProvider());
	}

	private PGPCryptoTools() {

	}

	/**
	 *
	 * @param outputFileName - the file name to use for the encrypted output
	 * @param inputFileName - the name of the file to encrypt
	 * @param pgpKeyRingFile - the PGP public key file
	 * @param asciiArmor - true to use ASCII armor
	 * @param integrityCheck - true to use integrity checks
	 * @throws IOException
	 * @throws PGPException
	 */
	public static final void encryptFile(File outputFileName, File inputFileName, File pgpKeyRingFile, boolean asciiArmor, boolean integrityCheck) throws IOException, PGPException {
		FileInputStream keyInputStream = new FileInputStream(pgpKeyRingFile);
		PGPPublicKey publicKey = readPublicKey(keyInputStream);

		OutputStream out = null;
		if (asciiArmor) {
			out = new ArmoredOutputStream(new FileOutputStream(outputFileName));
		}
		else {
			out = new BufferedOutputStream(new FileOutputStream(outputFileName));
		}

		JcePGPDataEncryptorBuilder PgpDataEncryptorBuilder = new JcePGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
		.setWithIntegrityPacket(integrityCheck)
		.setSecureRandom(new SecureRandom())
		.setProvider("BC");

		PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(PgpDataEncryptorBuilder);
		encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider("BC"));
		OutputStream dataGeneratorOut = encryptedDataGenerator.open(out, new byte[1 << 16]);
		PGPCompressedDataGenerator  compressDataGeneratorOut = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
		PGPUtil.writeFileToLiteralData(compressDataGeneratorOut.open(dataGeneratorOut), PGPLiteralData.BINARY, inputFileName, new byte[1 << 16]);

		compressDataGeneratorOut.close();
		dataGeneratorOut.close();
		out.close();
	}

	/**
	 *
	 * @param inputFileName - the file to decrypt
	 * @param keyFileName - the PGP key ring file
	 * @param passwd - the pass phrase protecting the PGP key ring file
	 * @param outputFileName - the file to output the decrypted data
	 * @throws IOException on I/O errors
	 * @throws PGPException on decryption errors
	 */
	public static final void decryptFile(File inputFileName, File keyFileName, char[] passwd, File outputFileName) throws IOException, PGPException {
		InputStream keyIn = new BufferedInputStream(new FileInputStream(keyFileName));
		InputStream in = PGPUtil.getDecoderStream(new BufferedInputStream(new FileInputStream(inputFileName)));

		PGPObjectFactory pgpObjFactory = new PGPObjectFactory(in);
		PGPEncryptedDataList pgpEncryptedDataList = null;

		Object o = pgpObjFactory.nextObject();
		if (o instanceof PGPEncryptedDataList) {
			pgpEncryptedDataList = (PGPEncryptedDataList)o;
		}
		else {
			pgpEncryptedDataList = (PGPEncryptedDataList)pgpObjFactory.nextObject();
		}

		PGPPrivateKey secretKey = null;
		PGPPublicKeyEncryptedData publicKeyEncryptedData = null;
		PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyIn));

		@SuppressWarnings("unchecked")
		Iterator it = pgpEncryptedDataList.getEncryptedDataObjects();

		while(it.hasNext() && secretKey == null) {
			publicKeyEncryptedData = it.next();
			PGPSecretKey pgpSecKey = pgpSecretKeyRingCollection.getSecretKey(publicKeyEncryptedData.getKeyID());

			if (pgpSecKey != null) {
				Provider provider = Security.getProvider("BC");
				secretKey = pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build()).setProvider(provider).build(passwd));
			}
		}

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

		if(publicKeyEncryptedData == null) {
			throw new NullPointerException("cannot continue with null public key encryption data.");
		}

		InputStream clear = publicKeyEncryptedData.getDataStream(new JcePublicKeyDataDecryptorFactoryBuilder().setProvider("BC").build(secretKey));
		PGPObjectFactory plainFact = new PGPObjectFactory(clear);
		PGPCompressedData compressedData = (PGPCompressedData)plainFact.nextObject();
		InputStream compressedStream = new BufferedInputStream(compressedData.getDataStream());
		PGPObjectFactory pgpFact = new PGPObjectFactory(compressedStream);
		Object message = pgpFact.nextObject();

		if (message instanceof PGPLiteralData) {
			PGPLiteralData literalData = (PGPLiteralData)message;
			InputStream literalDataInputStream = literalData.getInputStream();
			OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFileName));
			Streams.pipeAll(literalDataInputStream, out);
			out.close();
		}
		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 (publicKeyEncryptedData.isIntegrityProtected()) {
			if (!publicKeyEncryptedData.verify()) {
				throw new PGPException("message failed integrity check");
			}
		}

		keyIn.close();
		in.close();
	}

	/**
	 * <p>Return the first suitable key for encryption in the key ring
	 * collection. For this case we only expect there to be one key available
	 * for encryption.</p>
	 * 
	 * @param input - the input stream of the PGP key ring
	 * @return the first suitable PGP public key found for encryption
	 * @throws IOException on I/O related errors
	 * @throws PGPException on signing errors
	 */
	private static final PGPPublicKey readPublicKey(InputStream input) throws IOException, PGPException
	{
		PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(input));
		PGPPublicKey pubKey = null;

		@SuppressWarnings("unchecked")
		Iterator<PGPPublicKeyRing> keyRingIter = pgpPub.getKeyRings();
		while (keyRingIter.hasNext() && pubKey == null) {
			PGPPublicKeyRing keyRing = keyRingIter.next();

			@SuppressWarnings("unchecked")
			Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
			while (keyIter.hasNext()) {
				PGPPublicKey key = keyIter.next();

				if (key.isEncryptionKey()) {
					pubKey = key;
					break;
				}
			}
		}

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

Encrypt a file

public void encryptFile() {
	String keysDir = System.getProperty("user.dir")+File.separator+"myKeys";
	String filesDir = System.getProperty("user.dir")+File.separator+"myFiles";

	File textFile = new File(filesDir+File.separator+"TheFile.txt");
	File outputFile = new File(filesDir+File.separator+"TheFile.pgp");
	File publicKeyFile = new File(keysDir+File.separator+"public.asc");

	try {
		PGPCryptoTools.encryptFile(outputFile, textFile, publicKeyFile, true, true);
		System.out.println("File: "+textFile.getAbsolutePath());
		System.out.println("Encrypted to: "+outputFile.getAbsolutePath());
	}
	catch(Exception ex) {
		ex.printStackTrace();
	}
}

Decrypt a file

public void decryptFile() {
	String keysDir = System.getProperty("user.dir")+File.separator+"myKeys";
	String filesDir = System.getProperty("user.dir")+File.separator+"myFiles";

	File textFile = new File(filesDir+File.separator+"TheFile.txt");
	File inputFile = new File(filesDir+File.separator+"TheFile.pgp");
	File secretKeyFile = new File(keysDir+File.separator+"secret.asc");

	try {
		PGPCryptoTools.decryptFile(inputFile, secretKeyFile, "TestPass12345!".toCharArray(), textFile);
		System.out.println("File: "+inputFile.getAbsolutePath());
		System.out.println("Decrypted to: "+textFile.getAbsolutePath());
	}
	catch(Exception ex) {
		ex.printStackTrace();
	}
}

Part 4 coming soon showing how we Encrypt+Sign and Decrypt+Verify!

4 thoughts on “PGP Cryptography With The Legion of the Bouncy Castle – Part 3

    Juan Carlos Alvarez said:
    March 22, 2013 at 10:12 pm

    this was a very interesting post, i`m wating for the part 4. Thanks.

      geodma responded:
      March 25, 2013 at 12:33 am

      Thank you very much Juan, I am glad you found my article useful for your needs. I haven’t had a lot of time to dedicate to part 4 (which is a big one) and part 5 was actually using bouncy castle on Android.

      I’ll try to see if I can finish the part 4 and 5 some time soon 🙂

    Someone said:
    June 14, 2013 at 11:29 am

    The method “encryptFile” calls the function “readPublicKey(keyInputStream);”, but this function is not part of the souce shown in this article. In your other article one finds the function “readSecretKey”. Of course one could create an according function for the public key based on this, but including the function “readPublicKey” in the provided source, would make things a lot easier.

      geodma responded:
      June 14, 2013 at 11:48 am

      Thanks again for spotting this error too. It seems I forgot to copy/paste it form my java files. I have just added the missing method.

Leave a reply to Juan Carlos Alvarez Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.