Pages

Thursday, September 22, 2011

AES and Java: Part the Third

So, in review, I needed to be able to encrypt a string using AES.

  • I started with a simple test to see if I could get the original string back after encrypting it.
  • The next step was to actually try hooking up the Java Cryptography classes to do the heavy lifting.
  • In this installment I'll clean up my current solution and extend it just a bit to be more generally useful.
Between last time and this, I extracted a class AESEncryptor, and removed some of the duplication. The constructor initializes both the encryptor and decryptor that are then used by the encrypt and decrypt functions.

public class AESEncryptor {
private Cipher encryptor;
private Cipher decryptor;
public AESEncryptor() {
byte[] sessionKey = null;
final byte[] iv = new byte[]{0x7F, 0x6E, 0x5D, 0x4C, 0x3B, 0x2A, 0x19, 0x08,
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
try {
KeyGenerator kGen = KeyGenerator.getInstance("AES");
kGen.init(128);
sessionKey = kGen.generateKey().getEncoded();
encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptor.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sessionKey, "AES"), new IvParameterSpec(iv));
decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
decryptor.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sessionKey, "AES"), encryptor.getParameters());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
}
public String encrypt(String plainText) throws UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException {
// get bytes from string, encrypt, encode
byte[] utf8bytes = plainText.getBytes("utf-8");
byte[] ciphertext = encryptor.doFinal(utf8bytes);
return new BASE64Encoder().encode(ciphertext);
}
public String decrypt(String cipherText) throws IOException, IllegalBlockSizeException, BadPaddingException {
// decode, decrypt, use bytes to create string
byte[] encryptedBytes = new BASE64Decoder().decodeBuffer(cipherText);
byte[] plaintext = decryptor.doFinal(encryptedBytes);
return new String(plaintext);
}
}

And I expanded the test case to be a bit more readable. This also has the benefit of making more of the intermediate data visible if debugging is necessary.
public class AESEncryptorTest {
private final AESEncryptor aesExample = new AESEncryptor();
private static String SAMPLE_TEXT = "Original Plaintext";
@Test
public void decryptingCiphertextShouldReturnOriginalPlaintext() throws Exception {
String cipherText = aesExample.encrypt(SAMPLE_TEXT);
String plainText = aesExample.decrypt(cipherText);
assertEquals(SAMPLE_TEXT, plainText);
}
}


So, am I done or is there another test that comes to mind?
Well, you may have noticed we have a bit of a problem here. The encryption key and vector are embedded in the constructor. Since a new key is generated each time an AESEncryptor object is created, I can't actually encrypt anything that someone else can successfully decrypt. While this approach may result in securely encrypted data, it lacks a bit of usefulness.

What test can I write that will advance my solution. I think that if I can get the same ciphertext out of two different AESEncryptor objects I'll be making some progress. So here is my new test:
@Test
public void encryptingTheSameStringShouldGiveTheSameCiphertext() throws Exception {
String cipherText1 = aesExample.encrypt(SAMPLE_TEXT);
String cipherText2 = new AESEncryptor().encrypt(SAMPLE_TEXT);
assertEquals(cipherText1, cipherText2);
}
And when I run the test I get a failure (as expected):
org.junit.ComparisonFailure: Expected :QXaIz4zs1ATJBkIEceLKE+Ad9gR35TYNyHZrsDgi1eo= Actual :JgbV3qcM6rdN0aPFKqPK7VBN5YhxfU8exzyM4+S3ZBY=

Looking at the way I initialize the Cipher instances, it looks like I need to pass in the byte array for the key and the byte array for the vector. Since I want to use the same key value across different tests, I'll move the key creation (and the population of the vector) to the test class. Furthermore, I want every test on that class to use the same values, so I'll initialize these arrays in an @BeforeClass method.
public class AESEncryptorTest {
private static String SAMPLE_TEXT = "Original Plaintext";
private static KeyGenerator KEY_GENERATOR = null;
private static byte[] SESSION_KEY = null;
private static byte[] VECTOR;
private AESEncryptor aesExample;
@BeforeClass
public static void initializeSharedState() throws NoSuchAlgorithmException {
KEY_GENERATOR = KeyGenerator.getInstance("AES");
KEY_GENERATOR.init(128);
SESSION_KEY = KEY_GENERATOR.generateKey().getEncoded();
VECTOR = new byte[]{0x7F, 0x6E, 0x5D, 0x4C, 0x3B, 0x2A, 0x19, 0x08,
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
}
@Before
public void setUp() {
aesExample = new AESEncryptor(SESSION_KEY, VECTOR);
}
@Test
public void decryptingCiphertextShouldReturnOriginalPlaintext() throws Exception {
String cipherText = aesExample.encrypt(SAMPLE_TEXT);
String plainText = aesExample.decrypt(cipherText);
assertEquals(SAMPLE_TEXT, plainText);
}
@Test
public void encryptingTheSameStringShouldGiveTheSameCiphertext() throws Exception {
String cipherText1 = aesExample.encrypt(SAMPLE_TEXT);
String cipherText2 = new AESEncryptor(SESSION_KEY, VECTOR).encrypt(SAMPLE_TEXT);
assertEquals(cipherText1, cipherText2);
}
}

And moving those initializations to the test removes some of the code from the AESEncryptor constructor:
public AESEncryptor(byte[] sessionKey, byte[] iv) {
try {
encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptor.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sessionKey, "AES"), new IvParameterSpec(iv));
decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
decryptor.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sessionKey, "AES"), encryptor.getParameters());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
}


Am I done yet?
I don't think so. Passing byte arrays around can be problematic. It would probably be better to take in a string for the initialization values. And given what we learned in the course of researching the previous step we should probably pass in a base64 encoded string.
package com.rhjensen.encryption;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class AESEncryptor {
private Cipher encryptor;
private Cipher decryptor;
public AESEncryptor(String sessionKey, String iv) {
byte[] keyBytes;
byte[] vectorBytes;
try {
keyBytes = new BASE64Decoder().decodeBuffer(sessionKey);
vectorBytes = new BASE64Decoder().decodeBuffer(iv);
encryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
encryptor.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(keyBytes, "AES"),
new IvParameterSpec(vectorBytes));
decryptor = Cipher.getInstance("AES/CBC/PKCS5Padding");
decryptor.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(keyBytes, "AES"),
encryptor.getParameters());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public String encrypt(String plainText) throws UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException {
// get bytes from string, encrypt, encode
byte[] utf8bytes = plainText.getBytes("utf-8");
byte[] ciphertext = encryptor.doFinal(utf8bytes);
return new BASE64Encoder().encode(ciphertext);
}
public String decrypt(String cipherText) throws IOException, IllegalBlockSizeException, BadPaddingException {
// decode, decrypt, use bytes to create string
byte[] encryptedBytes = new BASE64Decoder().decodeBuffer(cipherText);
byte[] plaintext = decryptor.doFinal(encryptedBytes);
return new String(plaintext);
}
}

And in light of that change here is the final test class:
package com.rhjensen.encryption;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import sun.misc.BASE64Encoder;
import javax.crypto.KeyGenerator;
import java.security.NoSuchAlgorithmException;
import static org.junit.Assert.assertEquals;
/**
* User: Richard H. Jensen
* Date: 9/21/11
* Time: 6:08 PM
*/
public class AESEncryptorTest {
private static String SAMPLE_TEXT = "Original Plaintext";
private static String SESSION_KEY = null;
private static String VECTOR;
private AESEncryptor aesExample;
@BeforeClass
public static void initializeSharedState() throws NoSuchAlgorithmException {
KeyGenerator KEY_GENERATOR = KeyGenerator.getInstance("AES");
KEY_GENERATOR.init(128);
byte[] keyBytes = KEY_GENERATOR.generateKey().getEncoded();
byte[] vectorBytes = new byte[]{0x7F, 0x6E, 0x5D, 0x4C, 0x3B, 0x2A, 0x19, 0x08,
0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00};
SESSION_KEY = new BASE64Encoder().encode(keyBytes);
VECTOR = new BASE64Encoder().encode(vectorBytes);
}
@Before
public void setUp() {
aesExample = new AESEncryptor(SESSION_KEY, VECTOR);
}
@Test
public void decryptingCiphertextShouldReturnOriginalPlaintext() throws Exception {
String cipherText = aesExample.encrypt(SAMPLE_TEXT);
String plainText = aesExample.decrypt(cipherText);
assertEquals(SAMPLE_TEXT, plainText);
}
@Test
public void encryptingTheSameStringShouldGiveTheSameCiphertext() throws Exception {
String cipherText1 = aesExample.encrypt(SAMPLE_TEXT);
String cipherText2 = new AESEncryptor(SESSION_KEY, VECTOR).encrypt(SAMPLE_TEXT);
assertEquals(cipherText1, cipherText2);
}
}

I think that is sufficient for this round of the problem.

No comments:

Post a Comment