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.
Quick Navigation
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.
- The key or confidential data must never enter the application’s processes.
- 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
anddata
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 )