M5: Insufficient Cryptography
Let’s assume that an application collects some Personal Identifiable Information (PII) which should be stored locally. Due to data relevance, it is encrypted. Now let’s think about an adversary “physically attaining” the mobile device where such data is stored. The adversary will have access to all third-party application directories; therefore, they’ll also have access to the stored data. In this scenario, whenever the adversary is able to return the encrypted data to its original unencrypted form, your cryptography was insufficient.
There are two fundamental mistakes in the development process leading to Insufficient Cryptography: either the encryption/decryption process relies on a flawless underlying process/library or the application may implement or leverage a weak encryption algorithm.
Keep in mind that encryption depends on secrets (keys) and even the best encryption algorithm will be useless if your application fails to keep its secrets by making the keys available to the attacker.
In the movie below, you’ll see how Goatlin cryptography fails by enabling the adversary to get the unencrypted version of stored data.
To address Insufficient Cryptography, we will replace the encryption algorithm with the AES - Advanced Encrypt Standard (Rijndael). As many other symmetric ciphers, AES can be implemented in different modes. In this case, we will use the GCM (Galoi Counter Mode). GCM is preferable to most popular CBC/ECB modes because the former is an authenticated cipher mode; meaning that after the encryption stage, an authentication tag is added to the ciphertext, which will then be validated prior to message decryption and ensuring the message has not been tampered with.
All major changes were done in the CryptoHelper class which was given two new methods: createUserKey()
and getUserKey()
. encrypt()
and decrypt()
methods were also changed to receive a username
argument:
package com.cx.goatlin.helpers
// ...
class CryptoHelper {
companion object {
fun createUserKey(username: String) { /* ... */ }
private fun getUserKey(username: String): SecretKey? { /* ... */ }
fun encrypt(original: String, username: String): String { /* ... */ }
fun decrypt(message: String, username: String): String { /* ... */ }
}
}
As previously stated, encryption depends on secrets (keys), which should be handled carefully. In this case, on successful signup, a random key is created and persisted in Android Keystore:
package com.cx.goatlin.helpers
// ...
class CryptoHelper {
companion object {
fun createUserKey(username: String) {
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
username,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(parameterSpec)
keyGenerator.generateKey()
}
}
}
Every time encryption/decryption is required, the username
should be provided to the appropriate CryptoHelper
method, since it is used as an alias to locate the user’s key in Android Keystore (see CryptoHelper.getUserKey()):
package com.cx.goatlin.helpers
// ...
class CryptoHelper {
companion object {
private fun getUserKey(username: String): SecretKey? {
val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
val entry = ks.getEntry(username, null) as? KeyStore.SecretKeyEntry
return entry?.secretKey
}
}
}
Finally, this key is used to encrypt/decrypt the user’s notes:
/**
* Encrypts given `message` using user's (`username`) specific encryption key.
* Returning value includes the IV as prefix.
*/
fun encrypt(original: String, username: String): String {
val entry = getUserKey(username)
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, entry)
val iv:ByteArray = cipher.iv
val message: ByteArray = cipher.doFinal(original.toByteArray(Charsets.UTF_8))
val final: ByteArray = iv+message
return Base64.encodeToString(final, Base64.DEFAULT)
}
As you can see above, AES GCM encryption requires an Initialization Vector (IV). By default, this is a random value. The value used during encryption should then be used on the corresponding decryption operation. Although randomness can be disabled (see setRandomizedEncryptionRequired()
), replacing random IV by a constant value will reduce encryption security.
In our implementation we kept IV random, prepending it to the encrypted message. Then while decrypting, the first 12 bytes correspond to the IV and the rest corresponds to the message. Note that IV is not secret.