Android Keystore: Store Sensitive Data in Android

Today we will look into Android Keystore. We will look at what is an Android Keystore. Why should you use it or even consider using it? We will also see how it will help you in storing sensitive data inside the android system’s secure storage.

Introduction to the Android Keystore

The first thing you should understand about the Keystore is that it is secure, very secure. It is secure because it is backed by something known as a keychain. The term keychain is mostly used in desktop *inux-based OS.

But that’s not all, there is some other mechanism known as the Android Keystore provider which can be used if the developer doesn’t want to rely on a keychain-based approach. One thing you should keep in mind is that both are equally secure it’s just a matter of preference.

Now for the Keystore to be secure we must consider the two things.

  1. The key or confidential data must never enter the application’s processes.
  2. There might be dedicated hardware where all the manipulation of the secure data happens. But this is optional as legacy devices might not have this.

If you understood the gist of the above paragraphs. Then we are headed in the right direction. Let us jump into the code directly.

In this tutorial, I have skipped UI creation and similar basic things. We will only look into the actual logic of Android Keystore.

Step #1 : Create models to hold data for encryption and decryption

Please follow the steps exactly or you can even copy-paste the final source code after each section.

#1 First, we create a new file named Secret.kt and add the code below

sealed class Secret(
    private val alias: String,
    private val data: String,
    private val iv: String
)Code language: Kotlin (kotlin)

Let us look at what is being done in the class.

  • The alias variable will hold the name of the secret data we will be stored in the Keystore.
  • The variable named data will contain the actual data, either encrypted or decrypted.
  • The variable named iv is the piece of random information that is required for encryption and decryption.

You will get a better idea of each of them when you finally run the code yourself.

#2 We create additional classes to hold the encryption and decryption data respectively

In the same file add the following blocks of code to create the new classes based off of the sealed class.

data class EncryptionSecret(
    val alias: String,
    val data: String
)Code language: Kotlin (kotlin)

The class above will hold the raw data. In other words, it will hold the data for encryption.

data class DecryptionSecret(
    val alias: String,
    val data: String,
    val iv: String
)Code language: Kotlin (kotlin)

The class above will hold the encrypted data. In other words the data for decryption.

If you followed the steps I mentioned. Then you will end up with the final code that looks something like this:

// file name -> Secret.kt

sealed class Secret(
    private val alias: String,
    private val data: String,
    private val iv: String
)

sealed class Secret(
    private val alias: String,
    private val data: String,
    private val iv: String
)

data class DecryptionSecret(
    val alias: String,
    val data: String,
    val iv: String
)Code language: Kotlin (kotlin)

Step #2: Create a Keystore helper class

Now that we have completed the creation of the necessary classes to hold our data. The next thing that we should do is create a class which will contain all the logic.

You should create a file named KeystoreHelper.kt and copy the code below

object KeystoreHelper {

    private const val KEY_SIZE = 128
    private const val PROVIDER = "AndroidKeyStore"
    private const val TRANSFORMATION = "AES/GCM/NoPadding"

    private val NULL = null
    private const val EMPTY = ""
    private const val NEW_LINE = "\n"

    fun encrypt(encryptionSecret: EncryptionSecret): DecryptionSecret {
        val kg =
            KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, PROVIDER)

        val kps = KeyGenParameterSpec.Builder(
            encryptionSecret.alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        ).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).build()

        kg.init(kps)
        val sk = kg.generateKey()

        val c = Cipher.getInstance(TRANSFORMATION)
        c.init(Cipher.ENCRYPT_MODE, sk)

        val eb = c.doFinal(encryptionSecret.data.toByteArray())
        val ctB64 = Base64.encodeToString(eb, Base64.NO_PADDING)
        val ivB64 = Base64.encodeToString(c.iv, Base64.NO_PADDING)

        return DecryptionSecret(
            data = ctB64.replace(NEW_LINE, EMPTY),
            iv = ivB64.replace(NEW_LINE, EMPTY),
            alias = encryptionSecret.alias
        )
    }

    fun decrypt(decryptionSecret: DecryptionSecret): EncryptionSecret {

        val iv = Base64.decode(decryptionSecret.iv, Base64.NO_PADDING)
        val ct = Base64.decode(decryptionSecret.data, Base64.NO_PADDING)

        val ks = KeyStore.getInstance(PROVIDER)
        ks.load(NULL)

        val ske: KeyStore.SecretKeyEntry =
            ks.getEntry(decryptionSecret.alias, NULL) as KeyStore.SecretKeyEntry

        val c = Cipher.getInstance(TRANSFORMATION)
        val gps: AlgorithmParameterSpec = GCMParameterSpec(KEY_SIZE, iv)
        c.init(Cipher.DECRYPT_MODE, ske.secretKey, gps)

        val decryptedBytes = String(c.doFinal(ct))

        return EncryptionSecret(
            alias = decryptionSecret.alias,
            data = decryptedBytes
        )
    }

}Code language: Kotlin (kotlin)

If you look the code above closely you will see that we are passing the DecryptionSecret and EncryptionSecret for decryption and encryption respectively.

That’s all you have to do for the storing and retrieving the secret from the keystone.

Step #3: Using the Android Keystore to store and retrieve secret data

Now that we have finally completed all the heavy lifting. We created all the necessary classes that is required for us to encrypt and decrypt our secret data. Let us see how we can use it in our application.

#1 To encrypt the data

// prepare the data for encryption
val encryptionSecret = EncryptionSecret(alias = "my_secret_key", data = "ABC-1920")
// pass the data for encryption we will receive the encrypted data as output
// you may save it somewhere like backend or shared preferences
val encryptedData: DecryptionSecret = KeystoreHelper.encrypt(encryptionSecret)Code language: Kotlin (kotlin)

Let us understand what is happening in the code above.

  • First we create an encryption secret that has alias and data that we want to encrypt
  • Then we invoke the method KeystoreHelper.encrypt with the data that we created earlier
  • Store the information somewhere you should store all the three information

The data that might be obtained after encryption will look something like this:

alias = my_secret_key
data  = wyI5f6i5ZaJLsGQaLXeR7rpcE4Sjrj3f
iv    = gLjtA7en9Sm9gf1y

#2 To decrypt the data

Now we have to pass in the encrypted data above and invoked the decrypt method.

Let us prepare the DecryptionSecret by passing in the values above.

// prepare the data for decryption
val toDecrypt = DecryptionSecret(
    alias = "my_secret_key",
    data = "wyI5f6i5ZaJLsGQaLXeR7rpcE4Sjrj3f",
    iv = "gLjtA7en9Sm9gf1y"
)
// pass the above data to begin the process of decryption
val decryptedData = KeystoreHelper.decrypt(toDecrypt)

The variable named decryptedData will have the output of your actual unencrypted data. In this case it will give out information as:

alias = my_secret_key
data  = ABC-1920

So, in our case, the ABC-1920 is the secret key that we encrypted.

But wait, we can take it even a step further. Let us automate the saving and storing of encrypted data.

Step #4: Saving the encrypted output to shared preferences for automation

Yes, let us go one step further and save the encrypted data automatically using shared preferences. This will not only remove the burden of having to save everything manually.

It will not only make the whole process flawless. But also help you to focus on only invoking just two methods.

Create a file named KeystorePreference.kt and add the code below to that file.

object KeystorePreference {

    private const val SP_NAME = "__encrypted_data"

    private lateinit var application: Application

    private val sp: SharedPreferences by lazy {
        application.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
    }

    fun init(application: Application) {
        this.application = application
    }

    fun get(alias: String): String? {
        val json: String = sp.getString(alias, null) ?: return null

        // decrypt the data
        val obj = JSONObject(json)

        val decryptionSecret = DecryptionSecret(
            alias = obj.getString("alias"),
            iv = obj.getString("iv"),
            data = obj.getString("data"),
        )

        return KeystoreHelper.decrypt(decryptionSecret).data
    }

    @Throws
    fun save(key: String, value: String): DecryptionSecret {

        // check if the key is already present
        // we do not want to override it
        if (sp.contains(key)) throw IllegalAccessException("The key already exist.")

        val input = EncryptionSecret(
            alias = key,
            data = value
        )
        val encrypted = KeystoreHelper.encrypt(input)

        // save the encrypted data to shared preferences
        val json = JSONObject().apply {
            put("alias", encrypted.alias)
            put("data", encrypted.data)
            put("iv", encrypted.iv)
        }
        sp.edit().putString(key, json.toString()).apply()

        return encrypted
    }

    private fun get(encryptionSecret: EncryptionSecret): String {
        return KeystoreHelper.encrypt(encryptionSecret).data
    }

    fun remove(key: String) {
        sp.edit().remove(key).apply()
    }

}Code language: Kotlin (kotlin)

If you see the code above, you will realize that I have done nothing new. I just created a shared preference object and used it alongside Step #3.

Now the first thing you should do is invoke the KeystorePreference.init(application), where the application is the application instance. If you miss doing that then, your application will crash.

To save a secret data all you have to do is invoke the method like this:

KeystorePreference.save("api_key", "this is a really secret data")Code language: Kotlin (kotlin)

And you can retrieve it like this:

val data = KeystorePreference.get("api_key")Code language: Kotlin (kotlin)

This is all that you have to do everything else will be managed under the hood automatically. And of course, there is something you should remember.

You will not be able to decrypt the data if you uninstall the application and reinstall it again.

It is because the keys for encryption and decryption is managed by Android OS.

So, when you uninstall the application the keys will be deleted by the system.

This concludes our Android Keystore tutorial. Keep learning, keep coding.

Also Read:

Android Fingerprint Authentication Tutorial: How to Guide

Android Fragments vs Activities

Retrofit File Upload using FileProvider ( for Content URIs )

Parcelable in Android: How to transfer data in-between ?

Related Posts