Symmetric Encryption in Java

Encryption is one of the more obtuse things that we do as programmers, perhaps appropriately so. Regardless of the language, search results are guaranteed to provide a myriad of different strategies, each describing different methods, many of which are subtly flawed, subverting their security goals. As is Java’s nature, performing encryption requires more programmatic steps than higher level languages, increasing the difficulty for those using encryption in their application.

Security Context

First, let’s take a look at what we’re going to do here. This post covers symmetric encryption, wherein a single key is used for encryption and decryption. In such a system there is a single thing, the encryption key, that must be kept secret and shared between anyone wanting to encrypt or decrypt data. In our example, we are also using a message authentication code (MAC), which requires its own key that must be secret & shared. Pieces of information like these are appropriately called shared secrets.

The MAC ensures that the outputs of our encryption (namely the ciphertext and initialization vector) have not been altered. It is theoretically possible to alter the initialization vector (IV) and ciphertext in concert to change the message without knowing the secret key. Additionally, either of those pieces could be manipulated so as to cause a fault when they are fed into the decryption system, causing an unexpected exception in your decryption system.

Prerequisites

Generally, you should employ the highest strength encryption readily available. Java ships by default with a security policy that complies with United States cryptography export control regulations that limits the encryption algorithms & key lengths that can be used. To get all of the available cryptographic tools you must install the Java Cryptography Extension Unlimited Strength Jurisdiction Policy Files. Without these, you will get InvalidAlgorithmParameterException because Java can’t find the requested algorithm or InvalidKeyException if the given key length exceeds that which is permitted under the jurisdiction policy files. Look out for these misleading errors when deploying your code to new machines.

Check out our basic-java-encryption repository to follow along at home.

Getting Started

The javax.crypto package deals exclusively in bytes. If you have strings you want to encrypt, you’ll need to represent them as bytes. For arbitrary things like your encryption keys, the easiest way is to store them as Base64 encoded strings and read or write them to disk using something like Commons Codec’s Base64.

If you’re dealing with strings of unknown stripe, be sure to use explicit encodings—given how opaque encryption programming is, the last thing you want is to get hung up because what you thought was a UTF-8 string was actually interpreted as US-ASCII.

Encrypting

You need a few different objects to perform encryption: the actual Cipher, the initialization vector, and the key. It’s also a good idea to generate a message authentication code (MAC) of the ciphertext, to protect against tampering with the ciphertext or IV that could be used to manipulate the Cipher object upon decryption.

The encryption flow looks like this:

SecureRandom secureRandom = new SecureRandom();
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Mac mac = Mac.getInstance("HmacSHA512");

// Sizes appropriate for AES: 128 bit IV, 256 bit key
private static final int IV_SIZE = 16;
private static final int KEY_SIZE = 32;

byte[] iv = new byte[IV_SIZE];
byte[] aesKey = new byte[KEY_SIZE];
byte[] macKey = new byte[KEY_SIZE];
byte[] ciphertext;
byte[] macBytes;

// Generate the keys & iv
secureRandom.nextBytes(iv);
secureRandom.nextBytes(aesKey);
secureRandom.nextBytes(macKey);

try {
    SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
    ciphertext = cipher.doFinal(byteArrayToEncrypt);

    mac.init(macKey);
    mac.update(iv);
    macBytes = mac.doFinal(ciphertext);
} catch (IllegalBlockSizeException | BadPaddingException |
        InvalidAlgorithmParameterException | InvalidKeyException e) {
    logger.warn("Problem encrypting", e);
    // throw exception or other error handling
}

The ciphertext, IV, and MAC can be stored wherever you keep data—they can be serialized into a single object, for example—without any special considerations. The keys, however, are sensitive and should be stored securely.

Decrypting

Decryption is a similar flow, but in reverse. You start by checking that the stored MAC matches the ciphertext & IV that you want to decrypt, and if they do, then you decrypt.

Assuming the objects from above still exist:

byte[] plaintext = new byte[0];

try {
    mac.init(hmacKey);
    mac.update(iv);
    // macBytes from above
    if (MessageDigest.isEqual(macBytes, mac.doFinal(ciphertext))) {
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        plaintext = cipher.doFinal(ciphertext);
    } else {
        logger.warn("MAC mismatch");
        // do something
    }
} catch (IllegalBlockSizeException | BadPaddingException |
        InvalidAlgorithmParameterException | InvalidKeyException e) {
    logger.warn("Problem encrypting", e);
    // throw exception or other error handling
}

Demo Code

Play around with a working example by cloning our basic-java-encryption repository.

Bits & Baubles

If encrypting multiple things, you must call init() and doFinal() for each plaintext with a new initialization vector each time.

Having explained this whole thing, the real answer to encryption is to abstract away as much as possible. If possible, use libraries like keyczar or Jasypt that will aid you in performing high-level cryptographic functions, while taking care of many of the minutiae for you.

Posted by Drew Stephens @dinomite ·

Drew’s expertise lies in high-traffic web services, big data, and building hyper-efficient software teams. At Clearspring, Drew created and supported APIs that handled more than 6 million daily requests, monitored detailed metrics from processing clusters with hundreds of nodes, and delivered thousands of events per second to users in real-time. Drew is skilled in systems administration and takes automation seriously, applying the Unix adage "if you do it twice, you're doing it wrong" ruthlessly. As a certified Scrum Master, Drew worked alongside Ryan to build a kaizen-based development organization at Genius.com capable of delivering high-quality products as-promised and on-time. Drew spends his time away from computers lifting heavy things and racing cars that are well past their prime.

About Palomino Labs

Palomino Labs unlocks the potential of software to change people and industries. Our team of experienced software developers, designers, and product strategists can help turn any idea into reality.

See the Palomino Labs website for more information, or send us an email and let's start talking about how we can work together.