Cryptography in Android is not about implementing algorithms — never do that. It is about using the platform's provided APIs correctly: the Android Keystore for key management, the Cipher class for encryption and decryption, and SecureRandom for randomness.
Getting any single piece wrong — wrong algorithm, wrong mode, wrong IV handling — can render encryption useless while appearing to work correctly.
Android Keystore
The Android Keystore is a hardware-backed secure storage system for cryptographic keys. Keys stored there cannot be extracted from the device — not even by root access in most cases. The key never leaves the Keystore; you bring data to the key, not the key to your data.
private val KEY_ALIAS = "my_secure_key"
fun generateKey() {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
)
keyGenerator.generateKey()
}
fun getKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey
}
Generate the key once. On subsequent app launches, retrieve it from the Keystore — it persists across restarts.
AES-GCM Encryption
AES-GCM (Galois/Counter Mode) is the recommended symmetric encryption algorithm. It provides both confidentiality (data is unreadable without the key) and authenticity (tampering is detected).
fun encrypt(plaintext: ByteArray): Pair<ByteArray, ByteArray> {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getKey())
val iv = cipher.iv // 12-byte IV generated by the cipher
val ciphertext = cipher.doFinal(plaintext)
return Pair(iv, ciphertext)
}
fun decrypt(iv: ByteArray, ciphertext: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
return cipher.doFinal(ciphertext)
}
The IV (Initialization Vector) must be unique for every encryption operation. Never reuse an IV with the same key — doing so breaks GCM's security guarantees. The cipher generates a random IV automatically in ENCRYPT_MODE. Store the IV alongside the ciphertext; it is not secret.
Storing Encrypted Data
The typical pattern is to encrypt a value and store the IV + ciphertext together, then decrypt on retrieval:
fun saveEncryptedToken(token: String) {
val (iv, ciphertext) = encrypt(token.toByteArray())
val combined = ByteArray(iv.size + ciphertext.size)
iv.copyInto(combined)
ciphertext.copyInto(combined, iv.size)
val encoded = Base64.encodeToString(combined, Base64.DEFAULT)
sharedPrefs.edit().putString("token", encoded).apply()
}
fun loadToken(): String? {
val encoded = sharedPrefs.getString("token", null) ?: return null
val combined = Base64.decode(encoded, Base64.DEFAULT)
val iv = combined.copyOfRange(0, 12)
val ciphertext = combined.copyOfRange(12, combined.size)
return decrypt(iv, ciphertext).toString(Charsets.UTF_8)
}
For most Android apps, EncryptedSharedPreferences does this automatically. Use the manual approach when you need fine-grained control — encrypting database fields, files, or values outside SharedPreferences.
Hashing and Password Storage
Never store passwords. Store a salted hash, and never use MD5 or SHA-1 for passwords — they are fast, which makes brute-force attacks cheap.
For password hashing, use bcrypt or Argon2 (via a library like jBCrypt or android-argon2). For general-purpose hashing (file integrity, checksums), SHA-256 is appropriate:
fun sha256(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
}
SecureRandom
Use SecureRandom for anything security-sensitive — session tokens, nonces, salts:
fun generateSessionToken(): String {
val bytes = ByteArray(32)
SecureRandom().nextBytes(bytes)
return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING)
}
Random and Math.random() are not cryptographically secure — their output can be predicted if an attacker observes enough samples.
Biometric-Bound Keys
For the highest security level, bind a Keystore key to biometric authentication. The key is only usable after a successful biometric check, enforced by the hardware:
fun generateBiometricKey() {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder("biometric_key", PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE_GCM)
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
)
keyGenerator.generateKey()
}
With setUserAuthenticationRequired(true), initializing the Cipher fails if the user has not authenticated within the current session. Pass the Cipher into BiometricPrompt.CryptoObject to tie authentication and encryption together.
Common Mistakes
| Mistake | Consequence |
|---|---|
| Reusing an IV with AES-GCM | Breaks authentication and leaks plaintext |
| Using ECB mode | Patterns in plaintext are visible in ciphertext |
| Hardcoding an encryption key in source code | Trivially extractable from the APK |
| Using MD5/SHA-1 for password hashing | Brute-force feasible |
Random instead of SecureRandom | Predictable tokens |
Storing keys in SharedPreferences | Readable on rooted devices |
Practice
Implement an EncryptedFileManager that: generates a Keystore-backed AES-256-GCM key on first use, encrypts the contents of a given string and writes the IV + ciphertext to a file, and decrypts and returns the plaintext on subsequent reads. Write a unit test with a fake Keystore to verify encrypt/decrypt round-trips correctly.
Summary
Use the Android Keystore to store keys — never derive or hardcode them. Use AES-256-GCM for encryption — it provides both confidentiality and authenticity. Always use a unique IV per encryption. Use SecureRandom for token generation. Never roll your own crypto; use platform and library APIs that have been audited.