androidengineers.Book a session

Security & System Design

Cryptography in Android

article45 minHard

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

MistakeConsequence
Reusing an IV with AES-GCMBreaks authentication and leaks plaintext
Using ECB modePatterns in plaintext are visible in ciphertext
Hardcoding an encryption key in source codeTrivially extractable from the APK
Using MD5/SHA-1 for password hashingBrute-force feasible
Random instead of SecureRandomPredictable tokens
Storing keys in SharedPreferencesReadable 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.

YOUR LEARNING JOURNEY

0 of 17 available lessons completed

Progress saved in this browser. No account needed.
Cryptography in Android | Senior Android Developer | Android Engineers