- 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void encryptingTheSameStringShouldGiveTheSameCiphertext() throws Exception { | |
String cipherText1 = aesExample.encrypt(SAMPLE_TEXT); | |
String cipherText2 = new AESEncryptor().encrypt(SAMPLE_TEXT); | |
assertEquals(cipherText1, cipherText2); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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