androidengineers.Book a session

Security Architecture

Android Keystore & Key Management

article25 minHard

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

ConceptRule
Key generationAlways use AndroidKeyStore provider — never BC (BouncyCastle) for app keys
GCM modeUse AES/GCM — provides both encryption and authentication
IV storageStore GCM IV alongside ciphertext; IV is not secret but must be unique per encryption
Key aliasPick a stable alias; changing it means all existing data is unreadable
Jetpack SecurityUse EncryptedSharedPreferences and EncryptedFile for simple cases
Key extractionHardware-backed keys can't be extracted even by root — verify with isHardwareBacked()

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Android Keystore & Key Management | Android System Design | Android Engineers