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.