AES encryption with Java
The following code is for encrypting short messages (it acts on byte arrays and not streams so the whole message needs to fit in memory). It creates a random Initialisation Vector (IV) for each message which is prepended to the start of the encrypted stream.
It is safe to leave your IV in the open so long as your key is kept secure (think salt on a password hash). This means that the same key can be used throughout without the same data being encrypted in the same way twice. This does add 16 bytes to the length of each message so if your throughput is high volume but small messages, that could be a factor. This implementation also uses GCM for message authentication though so that is an additional 16 bytes again. This means your encrypted message will be 32 bytes (or 2x 128 bits) longer than the input. The string used below in the test is 152 bytes originally but 184 after GCM and the IV payload.
Disclaimer: I am not a cryptography guru nor do I mean to imply that the following code is suitable for use without more than a passing understanding of the concepts therein.
Firstly the test. This test takes a string and encrypts it twice with the same key (the class AesUniqueIv will generate a key if none is provided which is made available by the getKey() method). It should be fairly obvious but the assertions are that the same message can be encrypted twice and decrypted twice and that the two encrypted messages are not the same. It also asserts that the length of the encrypted message is indeed 32 bytes longer as stated earlier.
package uk.tommyt.kafka.krypto; | |
import org.junit.Test; | |
import javax.crypto.BadPaddingException; | |
import javax.crypto.IllegalBlockSizeException; | |
import javax.crypto.NoSuchPaddingException; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.NoSuchProviderException; | |
import static org.hamcrest.CoreMatchers.equalTo; | |
import static org.hamcrest.CoreMatchers.not; | |
import static org.junit.Assert.assertThat; | |
public class AesUniqueIvTest { | |
@Test | |
public void canEncryptAndDecrypt() throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchProviderException { | |
AesUniqueIv in = new AesUniqueIv(); | |
AesUniqueIv out = new AesUniqueIv(in.getKey()); | |
byte[] message = "Hello from the dark side, I must've killed all the jedi, can't say that I'm sorry for blowing up Alderan, We could've ruled the galaxy as father and son".getBytes(); | |
byte[] encrypted1 = in.encrypt(message); | |
byte[] encrypted2 = in.encrypt(message); | |
byte[] decrypted1 = out.decrypt(encrypted1); | |
byte[] decrypted2 = out.decrypt(encrypted2); | |
assertThat(message, equalTo(decrypted1)); | |
assertThat(message, equalTo(decrypted2)); | |
assertThat(message, not(equalTo(encrypted1))); | |
assertThat(encrypted1, not(equalTo(encrypted2))); | |
assertThat(encrypted1.length, equalTo(message.length + 32)); | |
} | |
} |
Now the actual code, firstly the keysize and cipher used require either the use of OpenJDK or the Java Cryptography Extensions installed and BouncyCastle library available. If you don’t have OpenJDK (or the JCE available) then you are limited to 128 bit encryption. BouncyCastle provides the GCM authentication. If the constructor is called with no arguments, then it uses the built in key generator to create a key of the correct length. Otherwise it expects a byte array of the encoded key. On each call to encrypt, a the random IV is generated and copied to the start of the array. Equivilently, on decrypt, the IV is stripped from the start of the array. Evrything else should be fairly self explanitory.
package uk.tommyt.kafka.krypto; | |
import org.bouncycastle.jce.provider.BouncyCastleProvider; | |
import javax.crypto.BadPaddingException; | |
import javax.crypto.Cipher; | |
import javax.crypto.IllegalBlockSizeException; | |
import javax.crypto.KeyGenerator; | |
import javax.crypto.NoSuchPaddingException; | |
import javax.crypto.SecretKey; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.SecureRandom; | |
import java.security.Security; | |
public class AesUniqueIv { | |
private static final String CIPHER = "AES/GCM/NoPadding"; | |
private static final String KEY_TYPE = "AES"; | |
private static final int AES_BITS = 256; | |
private static final int IV_BYTES = 16; | |
private final SecureRandom secureRandom = new SecureRandom(); | |
private SecretKey secretKey; | |
private Cipher cipher; | |
static { | |
Security.addProvider(new BouncyCastleProvider()); | |
} | |
/** | |
* Encryptor with random key | |
*/ | |
public AesUniqueIv() throws NoSuchAlgorithmException, NoSuchPaddingException { | |
this(newKey()); | |
} | |
/** | |
* Encryptor with provided key | |
*/ | |
public AesUniqueIv(byte[] key) throws NoSuchAlgorithmException, NoSuchPaddingException { | |
this(new SecretKeySpec(key, KEY_TYPE)); | |
} | |
private AesUniqueIv(SecretKey key) throws NoSuchPaddingException, NoSuchAlgorithmException { | |
this.secretKey = key; | |
cipher = Cipher.getInstance(CIPHER); | |
} | |
public byte[] getKey() { | |
return secretKey.getEncoded(); | |
} | |
public byte[] encrypt(byte[] message) throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { | |
IvParameterSpec iv = newIvParameterSpec(); | |
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); | |
byte[] encrypted = cipher.doFinal(message); | |
byte[] encryptedWithIv = new byte[IV_BYTES + encrypted.length]; | |
System.arraycopy(iv.getIV(), 0, encryptedWithIv, 0, iv.getIV().length); | |
System.arraycopy(encrypted, 0, encryptedWithIv, IV_BYTES, encrypted.length); | |
return encryptedWithIv; | |
} | |
public byte[] decrypt(byte[] message) throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { | |
byte[] iv = new byte[IV_BYTES]; | |
byte[] encrypted = new byte[message.length - IV_BYTES]; | |
System.arraycopy(message, 0, iv, 0, IV_BYTES); | |
System.arraycopy(message, IV_BYTES, encrypted, 0, message.length - IV_BYTES); | |
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); | |
return cipher.doFinal(encrypted); | |
} | |
private static SecretKey newKey() throws NoSuchAlgorithmException { | |
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_TYPE); | |
keyGenerator.init(AES_BITS); | |
return keyGenerator.generateKey(); | |
} | |
private IvParameterSpec newIvParameterSpec() { | |
byte[] iv = new byte[IV_BYTES]; | |
secureRandom.nextBytes(iv); | |
return new IvParameterSpec(iv); | |
} | |
} |
Disclaimer once again: I am not a cryptography guru nor do I mean to imply that the following code is suitable for use without more than a passing understanding of the concepts therein. This code is public domain.