Android Security: Keystore, Biometrics, R8 and OWASP Mobile

Android Development with Kotlin: 21 Real Projects · Production Android · 4 min read · Updated

Protect the token

  1. After login the server returns a token.
  2. A key in the Android Keystore encrypts it. The key itself never leaves secure storage.
  3. Only the encrypted bytes are saved.
  4. To use the token again, the user confirms with fingerprint or face.

Security is part of every app that handles accounts, money or personal data. The goal is simple: protect the user's data on the device and on the network, and assume that a determined person can inspect your APK. This lesson lists the practical steps.

Login, passwords and OTP

  • Validate on the device for good feedback (email shape, minimum length), and on the server for real. Never rely only on the app.
  • Never store a password. Store a short-lived token, and ask for the password again for sensitive actions.
  • Rate-limit and lock out repeated wrong attempts on the server.
  • OTP (one-time password) codes should expire quickly and be single-use. Let the user request a new one, but not endlessly.

Android Keystore and encrypted storage

The Android Keystore stores cryptographic keys in protected hardware where available, so your app can use a key but cannot read it out. Create an AES key in the Keystore, encrypt the token with it, and store the encrypted bytes. The older EncryptedSharedPreferences does this for you but is deprecated in recent versions of the security library, so check the current guidance; a DataStore plus Keystore encryption (for example through the Tink library) is the modern pattern.

fun createKey(alias: String): SecretKey {
    val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    generator.init(
        KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .build()
    )
    return generator.generateKey()
}

fun encrypt(key: SecretKey, plain: ByteArray): Pair<ByteArray, ByteArray> {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    return cipher.iv to cipher.doFinal(plain)      // store the iv with the data
}

Do not invent your own cryptography. Use well-known libraries and the algorithms above, and keep the key in the Keystore, never in code.

Biometric authentication

The BiometricPrompt API shows the system fingerprint or face dialog, so you never handle biometric data yourself. Use it to unlock the app or to approve an action, and ideally tie it to a Keystore key that needs authentication (setUserAuthenticationRequired(true)). Always offer a fallback such as device credentials.

val info = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Unlock")
    .setSubtitle("Confirm it is you")
    .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
    .build()

val prompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity),
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { unlock() }
    })
prompt.authenticate(info)

R8, obfuscation and tampering

R8 shrinks and renames code in release builds (isMinifyEnabled = true). That makes reverse engineering harder, but it is not security: assume attackers can read your logic. Root detection and integrity checks (Play Integrity API) can raise the effort for fraud-prone apps, but they can be bypassed, so enforce the important rules on the server.

OWASP Mobile ideas and best practice

  • Improper credential usage: no keys or passwords in the app.
  • Insecure communication: HTTPS only, validate certificates.
  • Insecure data storage: sensitive data encrypted, and not in logs, backups or screenshots (FLAG_SECURE on sensitive screens).
  • Insufficient input validation: validate everything, including deep link and intent data.
  • Insecure authentication: short-lived tokens, server-side checks.
  • Keep dependencies updated, request only the permissions you need, and mark components exported=false unless they must be public.

Your task

  1. Encrypt a token with a Keystore key and decrypt it again.
  2. Add a biometric unlock screen to one project with a device credential fallback.
  3. Turn on R8 for a release build and confirm the app still works.
  4. Review one project against the list above and write down three fixes.

How to check it worked

  • No secret appears in the source or logs.
  • Biometric login falls back correctly.
  • You can say why obfuscation is not real security.

Frequently asked questions

Is obfuscation enough to protect my app?

No. It only slows people down. Put real rules and secrets on the server.

Where do I keep an auth token?

Encrypted with a Keystore key, in DataStore or a similar store. Not in plain SharedPreferences, logs or code.

Do I handle fingerprint data myself?

No. BiometricPrompt shows the system dialog and only tells you success or failure.