As it turns out - it’s hard. Cryptography is hard to get right, and the thing is: you probably won’t know if you got it wrong.
In this blog post I want to show a way to encrypt something in a secure way on the JVM, the language of my choice is Kotlin.
AES has multiple modes: ECB, CBC, GCM and more.
The easy way: GCM
I’ll start with the simplest way to encrypt something with AES, using the GCM mode. The GCM mode is great as it also offers authentication. Authentication is important as it thwarts attacks on the cipher. You should always use authentication!
To encrypt something with AES GCM, we need a key and a nonce. The key can be 128 bit, 192 bit or 256 bit (see AES key sizes). The nonce (number used only once) must, as the name implies, only used once! The best way to ensure that is to generate it randomly using a cryptographically secure random number generator. The nonce is usually 96 bits long.
val secureRandom = SecureRandom()
val nonce = ByteArray(96 / 8) // 96 bits
secureRandom.nextBytes(nonce)
// now nonce contains 96 bit of random data
Next we have to generate a key, I chose a 256 bit key. We also use our random number generator for that:
val key = ByteArray(256 / 8) // 256 bits
secureRandom.nextBytes(key)
// now key contains 256 bit of random data
We use the Java Crypto API for the heavy lifting:
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val keySpec = SecretKeySpec(key, "AES")
// 128 = length in bits of the authentication tag (can have any size in bytes ranging from 64 bits to 128 bits with 8 bit increments)
val gcmSpec = GCMParameterSpec(128, nonce)
// Put the cipher in encrypt mode
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec)
plaintext = "This is a AES GCM example".toByteArray()
val ciphertext = cipher.doFinal(plaintext)
Now ciphertext
contains the AES encrypted data. We can now transmit the nonce and the ciphertext over an unsecure wire, they both contain
no sensitive data.
To decrypt the data, do everything in reverse:
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val keySpec = SecretKeySpec(key, "AES")
// We have to use the same nonce we used when encrypting the data
val gcmSpec = GCMParameterSpec(128, nonce)
// Put the cipher in decrypt mode
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec)
val plaintext = String(cipher.doFinal(ciphertext), Charsets.UTF_8)
And that’s it. If someone tampers with the ciphertext, a AEADBadTagException
is thrown.
If you want to use a password instead of random generated key, use a key derivation function to transform the password into a key. Use for example PBKDF#2.
Remember: DON’T REUSE THE NONCE! Generate a fresh one every time you encrypt something.
The hard way: CBC with HMAC
If you can’t use an authenticated encryption scheme like GCM, you have to do it the hard way: using CBC and a HMAC.
If you use only CBC, your encryption is vulnerable to a padding oracle attack. Therefore you have to use a HMAC to ensure the integrity of the message. The flow is the following:
- Generate a random IV
- AES-CBC the plaintext, yielding ciphertext C
- Use HMAC-SHA256 over IV + C, yielding HMAC H
- Send H, IV and C to recipient
- Recipient generates HMAC over IV + C, yielding HMAC H2
- Compare H and H2, if they do not match: abort!
- Decrypt C, yieling the plaintext
There are some things that can go wrong there. First, the HMAC function needs a secret key to work, otherwise the attacker could alter the ciphertext C and regenerate the HMAC. Where to get this key? You could reuse your AES key for that, but that isn’t such a great idea. There are two common solutions:
- Create a new key soley for the HMAC
- Use SHA512 on the AES key, use one 256 bit half for AES, the other half for HMAC
In this post I will go for solution 1 and generate a new key for HMAC.
In step 6 both HMACs are compared. You have to take care that the comparison isn’t vulnerable to a timing attack,
so don’t use Arrays.equals
!
The CBC mode uses an IV instead of a nonce. The size of the IV is the size of a AES block. It’s always 128 bit. The easiest way to get an IV is just to choose a value at random.
Let’s code:
// Generate two keys: one for AES, the other for HMAC
val aesKey = ByteArray(256 / 8) // 256 bits
secureRandom.nextBytes(aesKey)
val hmacKey = ByteArray(256 / 8) // 256 bits
secureRandom.nextBytes(hmacKey)
val iv = ByteArray(128 / 8) // 128 bits
secureRandom.nextBytes(iv)
Now we have two random keys and a random IV. In the next step, we AES encrypt our plaintext. We use PKCS5 padding. CBC is a block cipher (CBC stands for Cipher Block Chaining), the padding is used to pad the last block of the plaintext to 128 bit.
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val aesKeySpec = SecretKeySpec(aesKey, "AES")
val ivSpec = IvParameterSpec(iv)
// Put the cipher in encrypt mode
cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, ivSpec)
val ciphertext = cipher.doFinal(plaintext)
Now ciphertext
contains the AES data. We have to create a HMAC over the concatenation of the IV and the ciphertext:
// Use the hmacKey for the HMAC
val hmacKeySpec = SecretKeySpec(hmacKey, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(hmacKeySpec)
// Include the IV and the ciphertext in the HMAC
val hmac = mac.doFinal(iv + ciphertext)
Now hmac
contains the HMAC for the IV and the ciphertext. That scheme is called encrypt-then-mac.
We can now transmit the IV, the ciphertext and the HMAC over an unsecure wire, they all contain no sensitive data.
To decrypt the data, the receiver also creates the HMAC over the IV and the ciphertext:
val hmacKeySpec = SecretKeySpec(hmacKey, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(hmacKeySpec)
val hmac2 = mac.doFinal(iv + ciphertext)
Now we have to check if hmac
and hmac2
are equal, but beware of timing attacks!
fun checkHmac(hmac: ByteArray, expectedHmac: ByteArray): Boolean {
// Check for equality in a timing attack proof way
if (hmac.size != expectedHmac.size) return false
var result = 0
for (i in 0 until hmac.size) {
result = result.or(hmac[i].toInt().xor(expectedHmac[i].toInt()))
}
return result == 0
}
val hmacMatches = checkHmac(hmac, hmac2)
The variable hmacMatches
is true if both HMAC are equal. If hmacMatches
is false, abort immediately, someone has tampered the data!
The receiver can then decrypt the data:
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val aesKeySpec = SecretKeySpec(aesKey, "AES")
// We have to use the same IV we used when encrypting the data
val ivSpec = IvParameterSpec(iv)
// Put the cipher in decrypt mode
cipher.init(Cipher.DECRYPT_MODE, aesKeySpec, ivSpec)
val plaintext = String(cipher.doFinal(ciphertext), Charsets.UTF_8)
Phew. That’s it. That’s how you get a secure AES encryption when you can’t use AES GCM.
If you are interested in working code, check my GitHub repository.