Kotlin SCP: Module 5, 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 by 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 usernane 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. This key is user specific (see SignupActivity) and it is used to encrypt/decrypt a user’s notes only.

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
            // @todo handle null entry
            return entry?.secretKey
        }
    }
}

There is another implementation detail worth mentioning, since it may prove challenging. 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.

Resources

Readings