What happens when our site security breaks down and despite our best efforts our servers get compromised. Our database gets exposed, our password files get exposed, etc. Hopefully, at the very least most of us would take the precaution to encrypt our passwords before storing them. That should be the bare minimum.
Stored content
But there are many more things to protect. If an attacker gains control of our storage devices, he can carry out brute force attacks on the data stored on them. Choosing a strong cipher suite which complies with modern encryption standards is essential to protect our content because in the brute forcing attacks, the attacker has any amount time and tries to guess our secret key.
Host to host communication
Another situation where content encryption is useful is when communicate between hosts on the same computer system where we control both the sending and the receiving side. For example we may have two servers which talk to each other over the open internet and we manage both of them. It would be reasonable to secure the communication channel between them with symmetric encryption, just like with stored content, because both sides can use the same key. For example using AES encrypted communication channels with a strong secret key would make it very hard to guess the encrypted messages.
Cookies and other session tokens
Yet another example is when we send data to another system that we expect to receive back and verify that we indeed sent it. Think session cookies. Both the writer and reader of the information is the same just like in the previous case. After it goes through open networks, the cookies come back to us and we have to read them. While the information stored in cookies is actually sent by another party, we are the only one who should be able to have access to it. This is yet another case for symmetric encryption.
This is why we must consider encrypting our content. Social security numbers, drivers license numbers, or pictures of sensitive documents should be stored or transferred encrypted. The Java ecosystem offers a simple API for content encryption, Java Cryptographic Extension (JCE).
Encryption with AES in ECB Mode
It offers various methods for symmetric encryption, when both encryption and decryption keys are the same. We use symmetric algorithms for content encryption because the same party is doing both encryption and decryption, unlike in public key encryption where the two parties might not even know each other. Note, that using asymmetric encryption such as RSA public key encryption in these cases would not be efficient, because it would add more complexity to the implementation, where this is not necessary, since we don’t have a key exchange problem.
Using JCE from Scala
We can use the Java API directly from Scala. We don’t need a class because we don’t need to store any state. That is why we are using an object. This is what the code would look like.
import java.security.SecureRandom
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.{IvParameterSpec, SecretKeySpec}
import org.apache.commons.codec.binary.Hex
import scala.util.{Failure, Try}
case class CipherFailedException(msg: String, cause: Throwable)
extends Exception(msg, cause)
object SymmetricCipher {
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
private def key16(s: String): String =
"%16s".format(s)
def encrypt(key: Array[Byte], input: Array[Byte]): Try[Array[Byte]] =
Try{
val secretBytes = new SecretKeySpec(key, "AES")
cipher.init(Cipher.ENCRYPT_MODE, secretBytes, new SecureRandom())
cipher.getIV ++ cipher.doFinal(input)
} recoverWith {
case e: Throwable =>
e.printStackTrace()
Failure(new CipherFailedException("Encryption failed", e))
}
def encrypt(key: String, input: Array[Byte]) : Try[Array[Byte]]=
encrypt(key.getBytes, input)
def encryptTextToBase64WithKey16(key: String, text: String) : Try[String]=
encrypt(key16(key), text.getBytes).map(Base64.getEncoder.encodeToString)
def decrypt(key: Array[Byte], input: Array[Byte]): Try[Array[Byte]] =
Try{
val secretBytes = new SecretKeySpec(key, "AES")
cipher.init(Cipher.DECRYPT_MODE, secretBytes, new IvParameterSpec(input.slice(0, 16)))
cipher.doFinal(input.slice(16, input.length))
} recoverWith {
case e: Throwable =>
Failure(new CipherFailedException("Decryption failed", e))
}
def decrypt(key: String, input: Array[Byte]) : Try[Array[Byte]]=
decrypt(key.getBytes, input)
def decryptFromBase64ToTextWithKey16(key: String, encryptedBase64String: String) : Try[String]=
decrypt(key16(key), Base64.getDecoder.decode(encryptedBase64String)).map(_.map(_.toChar).mkString)
def encryptHex(key: String, s: String) : Try[String] =
encrypt(key16(key), s.getBytes("UTF-8")).map(bytes => new String(Hex.encodeHex(bytes, false)))
def decryptHex(key: String, s: String) : Try[String] =
decrypt(key16(key), Hex.decodeHex(s)).map(new String(_, "UTF-8"))
}
Creating a Cipher
The first thing we need to do is creating a cipher. This object will be shared both by encryption and decryption, so we make it a data member and we must initialize it with a transformation string that has three parts: the algorithm, the mode, and the padding.
- Algorithm: We essentially have a choice between three different sets of algorithms. AES, DES, and RSA. Of these, two are symmetric ciphers: AES (2001) was introduced as an improvement to DES (1977), which had a problem with small key sizes which made it relatively less secure and with computation speed. AES is the one to use! It is both safer and faster. As for RSA, we use symmetric ciphers for content encryption because exchanging the keys is not a problem and the same key can be used to encrypt and decrypt. RSA is asymmetric.
- Mode: Both AES and DES are block ciphers, and so they can be used with two modes ECB and CBC, corresponding to Electronic Codebook and Cipher Block Chaining. Cipher block chaining adds more security, because it processes blocks in a way that each step depends on the blocks processed up to that point. This makes it harder to crack the code it produces. CBC is safer!
- Padding: Many messages end in predictable ways (e. g. letters start and end with the same words), so initially random text was added when padding the messages, to create a higher level of randomness. This made it harder to feed the algo manipulated text where changes in the produced output mapped to changes in the input could be used to guess how the algorithm worked. Now days the various padding algorithms are more complex than this, but their goal remains the same, to pad the messages in a way that makes it harder to guess how the hashing function works. PKCS5Padding is a popular padding method.
Adding these points together, we created a cipher with a combination of what we consider to be the safest selections, and created the cipher as follows: Cipher.getInstance(“AES/CBC/PKCS5Padding“).
Initialization Vector (IV)
The initialization vector needs to be different every time we encrypt and it must be 16 characters long. It adds another layer of security that makes it harder to crack the cipher. Without it multiple encryptions with the same exact key would allow an attacker to guess the key. While it adds to security by making it harder to guess the key, the IV does not need to be kept secret.
We can initialize it to SecureRandom to make sure it is different every time, and since we don’t have to keep it secret, we prepend it to the encrypted data. This will keep it handy when we need to decrypt: All we have to do is extract the first 16 bytes from the encrypted data and we can use that as the IV to decrypt.
Unlimited Strength: Key size matters!
Note, that the above call will only work out of the box with a recent JDK. It worked for me on JDK 12. Otherwise, please manually download and install the unlimited strength JCE extension from https://www.oracle.com/java/technologies/javase-jce8-downloads.html
Convert Java Exceptions into Functional Style
We wrapped the call in a Try object, because in functional programming we are not supposed to throw exceptions and JCE throws exceptions when something goes wrong. This code will return a Failure[CipherFailedException] in those cases, while retaining the pure functional semantics of Scala. Otherwise it will return a Success[Array[Byte]].
Tests
The following will run our code with the correct password and also an incorrect password, using ScalaTest:
import org.scalatest.{FlatSpec, Matchers}
import scala.util.Success
class SymmetricCipherSpec extends FlatSpec with Matchers{
"ContentEncryptor with AES" should "perform symmetric encryption" in {
val mySecret = "1234567890123456"
val testByteArray = (0 until 256).map(_.toByte).toArray
SymmetricCipher
.encrypt(mySecret, testByteArray)
.flatMap(SymmetricCipher.decrypt(mySecret, _))
.map(_.mkString) shouldEqual Success(testByteArray.mkString)
}
it should "fail when wrong key is supplied" in {
val mySecret = "1234567890123456"
val testByteArray = (0 until 256).map(_.toByte).toArray
SymmetricCipher
.encrypt(mySecret, testByteArray)
.flatMap(SymmetricCipher.decrypt("wrong key", _))
.isFailure shouldEqual true
}
it should "perform symmetric encryption with all strings" in {
val mySecret = "1234567890123456"
val testStringToEncrypt = "12345678901234561234567890123456999999"
val decrypted = for{
encryptedInBase64 <- SymmetricCipher.encryptTextToBase64WithKey16(mySecret, testStringToEncrypt)
decryptedAlsoInBase64 <- SymmetricCipher.decryptFromBase64ToTextWithKey16(mySecret, encryptedInBase64)
} yield decryptedAlsoInBase64
decrypted shouldEqual Success(testStringToEncrypt)
}
}
Conclusion
It’s fairly easy to add hard to break content encryption to your project using the JCE APIs, that are readily available in all JDKs. Although, we must use a recent version of the JDK, if we want to add high strength security such as AES-256, or otherwise we must download the extension JCE for stronger keys.