Recently I had to build a little website with a feature to allow clients to upload files. That’s no problem with Apache Tomcat and Apache’s Commons File Upload. The requirement that made this interesting was to encrypt files on receipt so that if the webserver machine gets compromised, the files are difficult to read.
The Commons FileUpload API allows catching the inbound upload stream from the remote client, so it’s possible to do some encryption without ever storing the file in the clear on the local system. However, I didn’t want to use a symmetric key like a simple password (for example, no password-protected zip archive). A symmetric key means the password have to be in a config file on the web server, or I would have to type the password in at each reboot. Even tho the machine is on a UPS and unlikely to restart on its own, a manual intervention at each restart didn’t look attractive. So I looked for a solution using asymmetric aka public-key cryptography. The idea is to encrypt the inbound stream using a public key on the server, move the file to a secure place, then decrypt the file using a private key on the secure machine. Because the private key is never on the webserver, the webserver cannot decrypt the files it creates, and neither can an attacker (at least not easily). That was the theory anyhow!
Well I’m not a crypto guy, and didn’t want to make the noob mistake of inventing something even less secure than cleartext. I searched for examples and stumbled on Java’s javax.crypto.CipherOutputStream. But that seems designed only for symmetric keys. After that I found many, many references to the Bouncy Castle stuff (http://bouncycastle.org/). I was a little worried about using code from people who would choose that sort of name, and their Javadoc didn’t give me any confidence either. But they provide great examples and a nice implementation of Open PGP (RFC 4880). I extended one of their file-encryption classes to a stream-encryption class, added in a helper method to generate the public and private keys, and had a working solution.
My solution relies heavily on the BouncyCastle provider and openPGP libraries. Below is the result, just a single class, for encrypting and decrypting data read from a jave InputStream and written to a java OutputStream. To use it, you have to choose a user identity (identifies the public key) and a pass phrase (protects the private key). The class also compresses the input during encryption, and decompresses again on decryption, because I was required to accept files up to many tens of megabytes. Nearly half of the class is code to allow invocation from the command line for testing; the core code is actually not that large. I know, accepting a password on the command line isn’t secure on a multi-user system. Making this class work on your system’s JRE may require fussing with policy files that control what key size can be used by java. I have never installed any special policies and whatever reason I didn’t hit this issue.
package io.github.chrisinmtown.crypto;
import java.io.BufferedInputStream;
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.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Date;
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.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
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;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPUtil;
/**
* Supports encrypting and decrypting streams, including compression. Provides a
* static method that generates a PGP key pair and writes the public/private
* keys to separate files, which are read by the constructor.
*
* Requires the BouncyCastle OpenPGP package, available from
* http://www.bouncycastle.org/latest_releases.html. Developed using release
* bcpg-jdk16-145.
*
* Built by combining and extending two examples from BouncyCastle, package
* org.bouncycastle.openpgp.examples, classes KeyBasedLargeFileProcessor and
* RSAKeyPairGenerator. The (rather sparse) Javadoc for the BouncyCastle OpenPGP
* API is online at http://www.bouncycastle.org/docs/pgdocs1.6/index.html
*
* @author Copyright June 2010 by ChrisInMtown
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
public class PGPKeyCipherStream {
/**
* Supplied to many different methods.
*/
public static final String bouncyCastleProviderName = "BC";
/**
* Bit size used to generate an RSA public/private key pair.
*/
public static final int asymmetricKeySize = 1024;
/**
* The collection of public keys.
*/
private PGPPublicKeyRingCollection pgpPublicKeyRingCollection;
/**
* The collection of secret keys. A "secret key" is the PGP name for an
* encrypted private key.
*/
private PGPSecretKeyRingCollection pgpSecretKeyRingCollection;
/**
* Creates an instance with PGP key ring collection(s) read from the
* specified input stream(s). At least one stream (i.e., one key) must be
* provided; the other can be null. Must supply a public key to encrypt
* streams; must supply a secret key to decrypt streams.
*
* @param publicKeyStream
* may be null
* @param secretKeyStream
* may be null
* @throws IllegalArgumentException
* If both arguments are null.
* @throws IOException
* @throws PGPException
*/
public PGPKeyCipherStream(InputStream publicKeyStream,
InputStream secretKeyStream) throws IOException, PGPException {
if (publicKeyStream == null && secretKeyStream == null)
throw new IllegalArgumentException("No key stream(s) provided");
if (Security.getProvider(bouncyCastleProviderName) == null)
Security.addProvider(new BouncyCastleProvider());
if (publicKeyStream != null) {
InputStream decoderStream = PGPUtil
.getDecoderStream(publicKeyStream);
pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(
decoderStream);
}
if (secretKeyStream != null) {
InputStream decoderStream = PGPUtil
.getDecoderStream(secretKeyStream);
pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(
decoderStream);
}
}
/**
* Gets the first public key with the specified identity in its userIds
* property.
*
* @return A public key for the specified identity; null if not found.
* @throws IllegalArgumentException
* if the key is not suitable for encryption.
*/
@SuppressWarnings("unchecked")
private PGPPublicKey getPublicKey(String identity) {
PGPPublicKey publicKey = null;
// iterate through the key rings.
// Not yet upgraded to Java 1.5 generics, apparently.
Iterator /* PGPPublicKeyRing */rIt = pgpPublicKeyRingCollection
.getKeyRings();
while (publicKey == null && rIt.hasNext()) {
PGPPublicKeyRing kRing = (PGPPublicKeyRing) rIt.next();
Iterator /* PGPPublicKey */kIt = kRing.getPublicKeys();
while (publicKey == null && kIt.hasNext()) {
PGPPublicKey k = (PGPPublicKey) kIt.next();
if (!k.isEncryptionKey())
continue;
Iterator /* String */idIter = k.getUserIDs();
while (idIter.hasNext()) {
Object id = idIter.next();
if (identity.equals(id))
publicKey = k;
} // for all IDs
} // for all keys in ring
} // for all rings in collection
return publicKey;
}
/**
* Gets the private key with the specified key ID, as decrypted by the
* specified pass phrase.
*
* @return A private key for the specified key ID; null if not found.
*/
private PGPPrivateKey getPrivateKey(long keyID, String pass)
throws PGPException, NoSuchProviderException {
PGPSecretKey pgpSecKey = pgpSecretKeyRingCollection.getSecretKey(keyID);
if (pgpSecKey == null)
return null;
else
return pgpSecKey.extractPrivateKey(pass.toCharArray(),
bouncyCastleProviderName);
}
/**
* Encrypts the contents of the specified input stream and writes the
* ciphertext to the specified output stream, using the specified identity
* to find a suitable public key.
*
* @param identity
* User identity associated with public key
* @param withIntegrityCheck
* If true, turns on integrity check in encrypted data
* @param in
* Input stream that supplies cleartext
* @param out
* Output stream that receives ciphertext
* @throws IOException
* @throws NullPointerException
* If no public key ring is available
* @throws InvalidKeyException
* if no public key is found for the specified identity for the
* specified identity
* @throws PGPException
* @throws NoSuchProviderException
*/
public void encrypt(String identity, boolean withIntegrityCheck,
InputStream in, OutputStream out) throws IOException,
InvalidKeyException, NoSuchProviderException, PGPException {
if (pgpPublicKeyRingCollection == null)
throw new NullPointerException(
"No public key ring is available for encryption");
PGPPublicKey publicKey = getPublicKey(identity);
if (publicKey == null)
throw new InvalidKeyException("No public key found for identity "
+ identity);
PGPEncryptedDataGenerator encrDataGen = new PGPEncryptedDataGenerator(
PGPEncryptedData.CAST5, withIntegrityCheck, new SecureRandom(),
bouncyCastleProviderName);
encrDataGen.addMethod(publicKey);
OutputStream encryptedOutputStream = encrDataGen.open(out,
new byte[1 << 16]);
PGPCompressedDataGenerator comprDataGen = new PGPCompressedDataGenerator(
PGPCompressedData.ZIP);
OutputStream compressedEncryptedOutputStream = comprDataGen
.open(encryptedOutputStream);
PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
OutputStream pgpOutputStream = lData.open(
compressedEncryptedOutputStream, PGPLiteralData.BINARY,
"stream", new Date(), new byte[1 << 16]);
int len;
byte[] buf = new byte[1 << 16];
while ((len = in.read(buf)) > 0)
pgpOutputStream.write(buf, 0, len);
pgpOutputStream.close();
compressedEncryptedOutputStream.close();
encryptedOutputStream.close();
}
/**
* Decrypts the contents of the specified input stream and writes the clear
* text to the specified output stream. Searches the secret key store to
* find a suitable private key based on the key ID found in the input
* stream.
*
* @param pass
* Pass phrase used to decrypt the private key
* @param in
* Input stream that supplies ciphertext
* @param out
* Output stream that receives cleartext
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchProviderException
* @throws NullPointerException
* If no private key ring was loaded
* @throws PGPException
*/
@SuppressWarnings("unchecked")
public void decrypt(String pass, InputStream in, OutputStream out)
throws IOException, InvalidKeyException, NoSuchProviderException,
PGPException {
if (pgpSecretKeyRingCollection == null)
throw new NullPointerException(
"No secret key ring is available for decryption");
InputStream decodedInputStream = PGPUtil.getDecoderStream(in);
PGPObjectFactory pgpF = new PGPObjectFactory(decodedInputStream);
Object o = pgpF.nextObject();
// the first object might be a PGP marker packet.
PGPEncryptedDataList enc;
if (o instanceof PGPEncryptedDataList)
enc = (PGPEncryptedDataList) o;
else
enc = (PGPEncryptedDataList) pgpF.nextObject();
// This loop looks like it is ready for multiple encrypted
// objects, but really only one is expected.
Iterator it = enc.getEncryptedDataObjects();
PGPPublicKeyEncryptedData pbe = null;
PGPPrivateKey privateKey = null;
while (privateKey == null && it.hasNext()) {
pbe = (PGPPublicKeyEncryptedData) it.next();
privateKey = getPrivateKey(pbe.getKeyID(), pass);
if (privateKey == null)
throw new IllegalArgumentException(
"Failed to find private key with ID " + pbe.getKeyID());
}
InputStream clearStream = pbe.getDataStream(privateKey,
bouncyCastleProviderName);
PGPObjectFactory plainFact = new PGPObjectFactory(clearStream);
PGPCompressedData cData = (PGPCompressedData) plainFact.nextObject();
InputStream compressedStream = new BufferedInputStream(cData
.getDataStream());
PGPObjectFactory pgpFact = new PGPObjectFactory(compressedStream);
Object streamData = pgpFact.nextObject();
if (streamData instanceof PGPLiteralData) {
PGPLiteralData ld = (PGPLiteralData) streamData;
InputStream uncStream = ld.getInputStream();
int len;
byte[] buf = new byte[1 << 16];
while ((len = uncStream.read(buf)) > 0)
out.write(buf, 0, len);
uncStream.close();
compressedStream.close();
clearStream.close();
} else {
throw new PGPException(
"input is not PGPLiteralData - type unknown.");
}
// TODO Printing to stderr is bad form; maybe throw an exception??
if (pbe.isIntegrityProtected() && !pbe.verify())
System.err.println("message failed integrity check");
}
/**
* Generates keys using the specified identity and pass-phrase strings, and
* writes the keys to the specified files. Primarily for bootstrapping the
* use of this class.
*
* @param identity
* Associated with the public key.
* @param passPhrase
* Guards the secret key.
* @param asAscii
* If true, writes the keys as plain text; i.e., uses PGP's
* "ASCII armor" feature.
* @param publicKeyTarget
* File where the public key will be stored.
* @param secretKeyTarget
* File where the private key will be stored.
* @throws IllegalArgumentException
* If either file exists.
* @throws NoSuchProviderException
* @throws NoSuchAlgorithmException
* @throws PGPException
*/
public static void generateKeys(String identity, String passPhrase,
boolean asAscii, File publicKeyTarget, File secretKeyTarget)
throws IllegalArgumentException, IOException,
NoSuchAlgorithmException, NoSuchProviderException, PGPException {
if (publicKeyTarget.exists())
throw new IllegalArgumentException("File exists: "
+ publicKeyTarget.getPath());
if (secretKeyTarget.exists())
throw new IllegalArgumentException("File exists: "
+ secretKeyTarget.getPath());
if (Security.getProvider(bouncyCastleProviderName) == null)
Security.addProvider(new BouncyCastleProvider());
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA",
bouncyCastleProviderName);
kpg.initialize(asymmetricKeySize);
KeyPair kp = kpg.generateKeyPair();
// TODO Verify that all these parameters are reasonable.
// The constructor's javadoc has zero words of description.
PGPSecretKey pgpSecretKey = new PGPSecretKey(
PGPSignature.DEFAULT_CERTIFICATION, PGPPublicKey.RSA_GENERAL,
kp.getPublic(), kp.getPrivate(), new Date(), identity,
PGPEncryptedData.CAST5, passPhrase.toCharArray(), null, null,
new SecureRandom(), bouncyCastleProviderName);
PGPPublicKey pgpPublicKey = pgpSecretKey.getPublicKey();
FileOutputStream pubFos = new FileOutputStream(publicKeyTarget);
FileOutputStream secFos = new FileOutputStream(secretKeyTarget);
OutputStream outputStream;
outputStream = null;
if (asAscii)
outputStream = new ArmoredOutputStream(pubFos);
else
outputStream = pubFos;
pgpPublicKey.encode(outputStream);
outputStream.close();
outputStream = null;
if (asAscii)
outputStream = new ArmoredOutputStream(secFos);
else
outputStream = secFos;
pgpSecretKey.encode(outputStream);
outputStream.close();
pubFos.close();
secFos.close();
}
/**
* Test method that generates keys, writes as text, reads back.
*
* @param pub
* @param secret
* @throws Exception
*/
private static void genAndTest(String identity, String pass, File pub,
File secret) throws Exception {
System.out.println("Generating key pair, may take a few seconds..");
PGPKeyCipherStream.generateKeys(identity, pass, true, pub, secret);
System.out.println("Wrote public key to file " + pub.getPath());
System.out.println("Wrote secret key to file " + secret.getPath());
System.out.println("Reading generated keys back from files");
FileInputStream pubFis = new FileInputStream(pub);
FileInputStream secFis = new FileInputStream(secret);
PGPKeyCipherStream cs = new PGPKeyCipherStream(pubFis, secFis);
PGPPublicKey pubKey = cs.getPublicKey(identity);
if (pubKey == null)
System.err.println("Failed to find pub key by user ID " + identity);
else
System.out.println("Found pub key using user ID " + identity);
long keyId = pubKey.getKeyID();
PGPPrivateKey prvKey = cs.getPrivateKey(keyId, pass);
if (prvKey == null)
System.err.println("Failed to find prv key by key ID " + keyId);
else
System.out.println("Found prv key using key ID " + keyId);
}
/**
* Test method that encrypts a file.
*
* @param pub
* @param in
* @param out
* @throws Exception
*/
private static void encryptFile(File pub, String id, File in, File out)
throws Exception {
System.out.println("Loading public key from file " + pub.getPath());
FileInputStream pubInStrm = new FileInputStream(pub);
PGPKeyCipherStream cs = new PGPKeyCipherStream(pubInStrm, null);
pubInStrm.close();
System.out.println("Reading cleartext from " + in.getPath());
FileInputStream fileInStrm = new FileInputStream(in);
FileOutputStream fileOutStrm = new FileOutputStream(out);
cs.encrypt(id, false, fileInStrm, fileOutStrm);
fileInStrm.close();
fileOutStrm.close();
System.out.println("Wrote ciphertext to " + out.getPath());
}
/**
* Test method that decrypts a file.
*
* @param sec
* @param in
* @param out
* @throws Exception
*/
private static void decryptFile(File sec, String pass, File in, File out)
throws Exception {
System.out.println("Loading secret key from file " + sec.getPath());
FileInputStream secInStrm = new FileInputStream(sec);
PGPKeyCipherStream cs = new PGPKeyCipherStream(null, secInStrm);
System.out.println("Reading ciphertext from " + in.getPath());
FileInputStream fileInStrm = new FileInputStream(in);
FileOutputStream fileOutStrm = new FileOutputStream(out);
cs.decrypt(pass, fileInStrm, fileOutStrm);
System.out.println("Wrote cleartext to " + out.getPath());
}
private static String generateAction = "-gen";
private static String encryptAction = "-enc";
private static String decryptAction = "-dec";
private static void usage(String msg) {
if (msg != null)
System.err.println(msg);
System.err.println("Usage for PGPKeyCipherStream:");
System.err.println(" Generate: " + generateAction
+ " -id user-id-string -pass pass-phrase"
+ " -pub public-key-file -prv private-key-file");
System.err.println(" Encrypt: " + encryptAction
+ " -id user-id-string" + " -pub public-key-file"
+ " -in in-file -out out-file");
System.err.println(" Decrypt: " + decryptAction
+ " -pass pass-phrase" + " -prv private-key-file"
+ " -in in-file -out out-file");
return;
}
/**
* Parses command-line arguments and invokes methods appropriately. This is
* for testing the class. Accepting the pass phrase on the command line is a
* risk on a multi-user system, I know.
*
* I didn't use the Commons CLI library because this method is primarily for
* testing, not for users.
*
* @param args
*/
public static void main(String[] args) {
if (args.length < 2 || args[0].charAt(0) != '-') {
usage("Unexpected arguments");
return;
}
String action = null;
String userId = null;
String passPhrase = null;
String publicKeyFileName = null;
String privateKeyFileName = null;
String inputFileName = null;
String outputFileName = null;
for (int i = 0; i < args.length; ++i) {
if (generateAction.equals(args[i]) || encryptAction.equals(args[i])
|| decryptAction.equals(args[i])) {
if (action != null) {
usage("Unexpected repeated argument: " + args[i]);
return;
} else {
action = args[i];
}
} else if ("-id".equals(args[i])) {
if (userId != null || i + 1 == args.length) {
usage("Unexpected use of -id option");
} else {
++i;
userId = args[i];
}
} else if ("-pass".equals(args[i])) {
if (passPhrase != null || i + 1 == args.length) {
usage("Unexpected use of -pass option");
} else {
++i;
passPhrase = args[i];
}
} else if ("-pub".equals(args[i])) {
if (publicKeyFileName != null || i + 1 == args.length) {
usage("Unexpected use of -pub option");
} else {
++i;
publicKeyFileName = args[i];
}
} else if ("-prv".equals(args[i])) {
if (privateKeyFileName != null || i + 1 == args.length) {
usage("Unexpected use of -prv option");
} else {
++i;
privateKeyFileName = args[i];
}
} else if ("-in".equals(args[i])) {
if (inputFileName != null || i + 1 == args.length) {
usage("Unexpected use of -in option");
} else {
++i;
inputFileName = args[i];
}
} else if ("-out".equals(args[i])) {
if (outputFileName != null || i + 1 == args.length) {
usage("Unexpected use of -out option");
} else {
++i;
outputFileName = args[i];
}
} else {
usage("Unexpected argument: " + args[i]);
return;
}
}
if (action == null) {
usage("No action specified");
return;
}
try {
if (generateAction.equals(action)) {
if (userId == null || passPhrase == null) {
usage("Missing user ID string or pass phrase");
return;
}
if (publicKeyFileName == null || privateKeyFileName == null) {
usage("Missing public or private key file name");
return;
}
File pub = new File(publicKeyFileName);
File sec = new File(privateKeyFileName);
// Stop if either file *exists* -- never overwrite here.
if (pub.exists())
System.err
.println("Stopping because public key file exists: "
+ pub.getPath());
else if (sec.exists())
System.err
.println("Stopping because secret key file exists: "
+ sec.getPath());
else
genAndTest(userId, passPhrase, pub, sec);
} else if (encryptAction.equals(action)) {
if (userId == null || publicKeyFileName == null) {
usage("Missing user ID string or public key file name");
return;
}
if (inputFileName == null || outputFileName == null) {
usage("Missing input or output file name");
return;
}
File pub = new File(publicKeyFileName);
File in = new File(inputFileName);
File out = new File(outputFileName);
if (!pub.exists() || !pub.isFile())
System.err.println("Not found or not a file: "
+ pub.getPath());
else if (!in.exists() || !in.isFile())
System.err.println("Not found or not a file: "
+ in.getPath());
else if (out.exists())
System.err.println("Stopping because out file exists: "
+ out.getPath());
else
encryptFile(pub, userId, in, out);
} else if (decryptAction.equals(action)) {
if (passPhrase == null || privateKeyFileName == null) {
usage("Missing pass phrase or secret key file name");
return;
}
if (inputFileName == null || outputFileName == null) {
usage("Missing input or output file name");
return;
}
File sec = new File(privateKeyFileName);
File in = new File(inputFileName);
File out = new File(outputFileName);
if (!sec.exists() || !sec.isFile())
System.err.println("Not found or not a file: "
+ sec.getPath());
else if (!in.exists() || !in.isFile())
System.err.println("Not found or not a file: "
+ in.getPath());
else if (out.exists())
System.err.println("Stopping because out file exists: "
+ out.getPath());
else
decryptFile(sec, passPhrase, in, out);
} else {
System.err.println("Programmer error, unknown action: "
+ action);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Blog index / feedback to christopher döt lott át gmail dðt com