Symmetric Encryption using Android KeyStore
In this post I will show you an example of symmetric encryption in Android using the Android KeyStore.

In this post I will show you a simple class created to utilize symmetric encryption in Android using the KeyStore.

The Android Keystore is built into the Android system to hold cryptographic keys in a secure way. It is used to access existing keys or create new ones.

Android KeyStore Encryption

Encryption Types In A Nutshell

  1. Symmetric encryption is where the same key is used to encrypt and decrypt data
  2. Asymmetric encryption is where two keys are generated, public and private key sets. The public key is handed to clients and the private key is located on the host.
    • The client uses the public key to encrypt data that is sent to the host.
    • The host then decrypts data with the private key.

This example uses symmetric encryption in the Android system.

I did not create a UI, the work in done and viewed in the logcat. It would be easy enough for anyone to create the UI. I just wanted to show the implementation of Android Keystore and how to call the functions.

This project was derived from the project and explanation in the link below. There is more detailed information there so I would advice to go and read for complete understanding of the implementation.

https://proandroiddev.com/shedding-light-on-android-encryption-android-crypto-api-part-3-android-keystore-0054fb386a98

Code

KeystoreUtils

I created a new file name KeystoreUtils. This holds all the necessary functions / methods to implement Keystore encryption.

  • Retrieves existing key, if one is not found
    • Generates new key with defined parameters.
  • Method to encrypt data.
  • Method to decrypt data.
class KeystoreUtils {

    private val ALIAS : String = "my_secret"

    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
        load(null)
    }

    private val encryptCipher = Cipher.getInstance(TRANSFORMATION).apply {
        init(KeyProperties.PURPOSE_ENCRYPT, getKey())
    }

    private fun decryptCipherForIV() : Cipher {
        return Cipher.getInstance(TRANSFORMATION)
    }


    private fun createKey() : SecretKey {
        Log.d("Testing Keystore", "Creating key...")
        return KeyGenerator.getInstance(ALGORITHM).apply {
            init(
                KeyGenParameterSpec.Builder(
                    ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                )
                    .setBlockModes(BLOCK_MODE)
                    .setEncryptionPaddings(PADDING)
                    .setUserAuthenticationRequired(false)
                    .setRandomizedEncryptionRequired(true)
                    .build()
            )
        }.generateKey()
    }

    private fun getKey() : SecretKey {
        val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
        return existingKey?.secretKey ?: createKey()
    }

    fun encrypt(byteArray: ByteArray) : String {
        val encryptedBytes = encryptCipher.doFinal(byteArray)
        val iv = encryptCipher.iv

        val encryptedBytesWithIV = ByteArray(iv.size + encryptedBytes.size)
        System.arraycopy(iv, 0, encryptedBytesWithIV, 0, iv.size)
        System.arraycopy(encryptedBytes, 0, encryptedBytesWithIV, iv.size, encryptedBytes.size)
        return Base64.encodeToString(encryptedBytesWithIV, Base64.DEFAULT)
    }

    fun decryptWithIV(data: String) : String {
        val encryptedBytesWithIV = Base64.decode(data, Base64.DEFAULT)
        val cipher = decryptCipherForIV()
        val iv = encryptedBytesWithIV.copyOfRange(0, cipher.blockSize)
        cipher.apply {
            init(KeyProperties.PURPOSE_DECRYPT, getKey(), IvParameterSpec(iv))
        }
        val encryptedBytes = encryptedBytesWithIV.copyOfRange(cipher.blockSize, encryptedBytesWithIV.size)
        val decryptedBytes = cipher.doFinal(encryptedBytes)
        return String(decryptedBytes, Charsets.UTF_8)
    }

    companion object {
        private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
        private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
        private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
        private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
    }
}

MainActivity

Then to use the class, I just called it from a LaunchEffect from the MainActivity. Not best practice but just to show proof of concept.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SampleAndroidKeystoreTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

                    LaunchedEffect(Unit) {
                        val message = "{\"message\":\"This dog is amazing!\",\"id\":25,\"data\":\"Watch spot run!\"}"
                        val keystoreUtils = KeystoreUtils()
                        val encryptedMsg = keystoreUtils.encrypt(message.encodeToByteArray())
                        Log.d("Testing Keystore", " Message : $message")
                        Log.d("Testing Keystore", " Message : $encryptedMsg")

                        val decryptedMsg = keystoreUtils.decryptWithIV(encryptedMsg)
                        Log.d("Testing Keystore", " Message : $decryptedMsg")
                    }
                }
            }
        }
    }
}

Hope this help.