The Android Keystore System stores cryptographic keys in secure hardware (Trusted Execution Environment or StrongBox), where they cannot be extracted even by root-level attacks. It's the foundation of secure storage, biometric authentication, and digital signing on Android.
Why Keystore?
Keys stored in Keystore are:
- Non-extractable: the private key bytes never leave the secure hardware
- Hardware-backed: operations (signing, decryption) happen inside TEE or StrongBox
- Access-controlled: can require user authentication, screen lock, biometrics
Generating a Key
fun generateKey(alias: String): SecretKey {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(false) // set true to require biometric/PIN
.setInvalidatedByBiometricEnrollment(true) // invalidate if biometrics change
.build()
)
return keyGenerator.generateKey()
}
Encrypting and Decrypting
class KeystoreEncryption(private val alias: String = "app_master_key") {
private val keystore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private fun getOrCreateKey(): SecretKey {
return keystore.getKey(alias, null) as? SecretKey ?: generateKey(alias)
}
fun encrypt(plaintext: ByteArray): EncryptedData {
val key = getOrCreateKey()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
val ciphertext = cipher.doFinal(plaintext)
return EncryptedData(ciphertext = ciphertext, iv = cipher.iv)
}
fun decrypt(encrypted: EncryptedData): ByteArray {
val key = getOrCreateKey()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, encrypted.iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
return cipher.doFinal(encrypted.ciphertext)
}
}
data class EncryptedData(
val ciphertext: ByteArray,
val iv: ByteArray // GCM IV; must be stored alongside ciphertext
)
Storing Encrypted Data
Never store raw keys in SharedPreferences or databases. Store the encrypted data + IV:
class SecurePreferences(context: Context, private val encryption: KeystoreEncryption) {
private val prefs = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
fun putString(key: String, value: String) {
val encrypted = encryption.encrypt(value.toByteArray(Charsets.UTF_8))
prefs.edit()
.putString("${key}_ct", Base64.encodeToString(encrypted.ciphertext, Base64.DEFAULT))
.putString("${key}_iv", Base64.encodeToString(encrypted.iv, Base64.DEFAULT))
.apply()
}
fun getString(key: String): String? {
val ciphertext = prefs.getString("${key}_ct", null)?.let { Base64.decode(it, Base64.DEFAULT) } ?: return null
val iv = prefs.getString("${key}_iv", null)?.let { Base64.decode(it, Base64.DEFAULT) } ?: return null
return encryption.decrypt(EncryptedData(ciphertext, iv)).toString(Charsets.UTF_8)
}
}
Practical: EncryptedSharedPreferences (Jetpack Security)
For most apps, use Jetpack Security — it handles all of the above automatically:
// implementation("androidx.security:security-crypto:1.1.0-alpha06")
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// Use like normal SharedPreferences
encryptedPrefs.edit().putString("auth_token", token).apply()
val token = encryptedPrefs.getString("auth_token", null)
Key Deletion and Rotation
fun deleteKey(alias: String) {
val keystore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
keystore.deleteEntry(alias)
// Any data encrypted with this key is now permanently unreadable
}
fun rotateKey(alias: String) {
// 1. Decrypt all data with old key
val oldData = decryptAllWithAlias(alias)
// 2. Delete old key
deleteKey(alias)
// 3. Generate new key with same alias
generateKey(alias)
// 4. Re-encrypt with new key
encryptAllWithAlias(alias, oldData)
}
Key Takeaways
| Concept | Rule |
|---|---|
| Key generation | Always use AndroidKeyStore provider — never BC (BouncyCastle) for app keys |
| GCM mode | Use AES/GCM — provides both encryption and authentication |
| IV storage | Store GCM IV alongside ciphertext; IV is not secret but must be unique per encryption |
| Key alias | Pick a stable alias; changing it means all existing data is unreadable |
| Jetpack Security | Use EncryptedSharedPreferences and EncryptedFile for simple cases |
| Key extraction | Hardware-backed keys can't be extracted even by root — verify with isHardwareBacked() |