AES Encryption with Java
Java provides an extremely powerful encryption library built in that is useful for encrypting and decrypting data in applications. Unfortunately, like many things related to cryptography, the documentation is often confusing and full of terse references to concepts like block cipher modes and initialization vectors. Searching online will lead to a number of code samples that demonstrate how to successfully utilize the Java encryption libraries, but many of them do not demonstrate how to properly utilize these features.
Advanced Encryption Standard (AES) is an industry standard algorithm commonly used to encrypt data. The standard Java cryptographic libraries include support for AES. Although the cryptographic library attempts to enforce good defaults, it is up to the programmer to implement an AES solution properly, and there are a few pitfalls to doing so.
One commonly misunderstood aspect of Java encryption is the initialization vector (IV). One extremely important property of an encryption system is that it produces output that appears to be random. The system should choose plain text, readable by a human or a program, and produce cipher text (encrypted text) that shouldn't bear any resemblance to the input. Any pattern in the output can yield clues as to the plain text of a message that could allow an adversary to defeat the encryption.
One challenge with systems that encrypt data is the need to obscure the size of the input. The output of a plain text shouldn't produce a cipher text whose length gives clues as to the size of the input. One way to deal with this challenge is to use a block encryption algorithm that takes chunks of input, and encrypts each chunk one at a time. Of course, you may have input that is smaller than a given chunk size, or the last chunk of input might not be sufficient to fill an entire block, so you can use padding to fill out the input size to match the right chunk length.An initialization vector is a random seed that is used to defeat attacks on a cryptographic system.
There are two chunking methods that can be used with AES encryption, code book encryption (CBE) and cipher block chaining (CBC). There are trade offs for using each one, and choosing the right mode is extremely important. CBE is extremely fast. It breaks input into chunks and encrypts each chunk using an algorithm and a key. Unfortunately this mode has the distinct advantage that an attacker could reorder the chunks, or inject previously recorded chunks, and the tampering wouldn't be detected (the altered cipher text would still decrypt). Additionally CBE produces predictable patterns. The same chunk of plaintext encrypted twice produces the same cipher text. This opens cipher text up to a number of attacks, including frequency analysis and brute force dictionary attacks.
To defeat the weaknesses of CBE, CBC uses an additional input to its encryption function that allows for increased distinction and randomness of cipher text. This input is an initialization vector. It is designed to be public, since it must be used to both encrypt and decrypt data, but it allows cipher text encrypted with distinct initialization vectors to produce distinct cipher text. In many ways an initialization vector is like a salt for a hash.
To demonstrate these principles consider the following Java code sample that uses a static initialization vector to encrypt and decrypt a secret message. Imagine this code was used to send a secret message to an invading army. The message is sent over an open communications channel that can be observed by defenders but the invaders with to hide the contents of the message (a canonical use for encryption).
Do not use the code below, it suffers from a vulnerability explained later in this article!
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class IncorrectImplementation { static String secret_message = "Attack at dawn!"; static String encrypt_key = "26-ByteSharedKey"; static byte key_to_bytes[] = encrypt_key.getBytes(); static String iv_string = "16byte static iv"; // Don't do this, it is BAD! static byte iv[] = iv_string.getBytes(); static String encryption_mode = "AES/CBC/PKCS5Padding"; //Use AES with CBC and padding public static void main(String [] args) { try { byte[] cipher = encrypt(secret_message, key_to_bytes); System.out.print("cipher text:\t\t"); for (int i=0; i<cipher.length; i++) System.out.print(new Integer(cipher[i])+" "); String decrypted = decrypt(cipher, key_to_bytes); System.out.println("\ndecrypted plain text:\t" + decrypted); } catch (Exception e) { e.printStackTrace(); } } public static byte[] encrypt(String plainText, byte[] enc_key) throws Exception { Cipher cipher = Cipher.getInstance(encryption_mode); SecretKeySpec key = new SecretKeySpec(enc_key, "AES"); cipher.init(Cipher.ENCRYPT_MODE, key,new IvParameterSpec(iv)); return cipher.doFinal(plainText.getBytes()); } public static String decrypt(byte[] cipherText, byte[] enk_key) throws Exception{ Cipher cipher = Cipher.getInstance(encryption_mode); SecretKeySpec key = new SecretKeySpec(enk_key, "AES"); cipher.init(Cipher.DECRYPT_MODE, key,new IvParameterSpec(iv)); return new String(cipher.doFinal(cipherText)); } }
Running the program produces the following output:
cipher text: 23 106 -101 97 -80 -88 -125 -31 86 56 -117 -98 -72 -126 100 -3 decrypted plain text: Attack at dawn!
Imagine the invaders send this message one night. The defenders observe the cipher:
23 106 -101 97 -80 -88 -125 -31 86 56 -117 -98 -72 -126 100 -3
and record it. The next morning the invaders attack, but luckily are repulsed. The following night the defenders observe the same encrypted cipher:
23 106 -101 97 -80 -88 -125 -31 86 56 -117 -98 -72 -126 100 -3
and the following morning the invaders attack. Now the defenders might not know the exact plain text, but they certainly see a correlation between the cipher and the invader behavior. The cryptosystem has failed subtly but significantly.
Consider a much more practical scenario where you are storing passwords for third party services used by your system. You need to recover the passwords to make use of them, so you can't use hashing so you use AES encryption. If the passwords to two of the systems are the same, it will show in your password storage because the two encrypted values will be exactly the same.
In order to defeat this weakness of repetitive encryption of the same plain text consider a subtle change to the code listing above that uses a random, distinct, initialization vector:
Properly Implemented Randome Initialization Vector
import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class IVDemo { static String secret_message = "Attack at dawn!"; static String encrypt_key = "26-ByteSharedKey"; static byte key_to_bytes[] = encrypt_key.getBytes(); static byte iv[] = new byte[16]; public static void main(String [] args) { SecureRandom my_rand = new SecureRandom(); // Use the Java secure PRNG my_rand.nextBytes(iv); // Generate a new IV every time! try { byte[] cipher = encrypt(secret_message, key_to_bytes); System.out.print("IV:\t\t\t"); for (int i=0; i<iv.length; i++) System.out.print(new Integer(iv[i])+" "); System.out.print("\ncipher text:\t\t"); for (int i=0; i<cipher.length; i++) System.out.print(new Integer(cipher[i])+" "); String decrypted = decrypt(cipher, key_to_bytes); System.out.println("\ndecrypted plain text:\t" + decrypted); } catch (Exception e) { e.printStackTrace(); } } public static byte[] encrypt(String plainText, byte[] enc_key) throws Exception { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec key = new SecretKeySpec(enc_key, "AES"); cipher.init(Cipher.ENCRYPT_MODE, key,new IvParameterSpec(iv)); return cipher.doFinal(plainText.getBytes()); } public static String decrypt(byte[] cipherText, byte[] enk_key) throws Exception{ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec key = new SecretKeySpec(enk_key, "AES"); cipher.init(Cipher.DECRYPT_MODE, key,new IvParameterSpec(iv)); return new String(cipher.doFinal(cipherText)); } }
Now multiple runs of the program produce different cipher text, but decrypt to the same plaintext. For example:
IV: -43 -7 89 23 -86 123 85 -116 15 -80 47 116 19 -52 41 -108 cipher text: -14 -53 -118 102 -53 92 -48 30 52 124 127 -66 -6 -97 -77 22 decrypted plain text: Attack at dawn! IV: -7 15 69 -62 21 60 17 103 64 39 28 -5 -113 94 9 104 cipher text: 109 80 -8 -26 -115 79 124 119 -29 -12 54 94 112 49 73 48 decrypted plain text: Attack at dawn!
Now the cipher text gives no hints as the the plaintext, ensuring a strong cryptographic implementation that will resist attacks on the observed cipher text.
Note that when using this strategy to do things like store AES encrypted data you'll need to store the initialization vector along with the data so that you can decrypt the data later. Using a strong, unique IV for each data stored will increase the overall strength of the solution dramatically.