Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 054 / 100

Android app security: a practical Android & Kotlin guide

Protect users and data.

Expona Admin6 min read

Why Android app security matters

Every Android app handles user data—credentials, payment details, personal information, location history. A single security oversight can expose thousands of users and destroy your app's reputation overnight. Android app security: a practical Android & Kotlin guide isn't about academic theory; it's about the concrete decisions we make in production to keep our users safe.

The Android ecosystem is mature and well-stocked with security tools. The challenge isn't availability; it's knowing which ones to use and when, then integrating them correctly into your build pipeline and runtime behavior. Over the next few sections, we'll walk through the most impactful security practices we've found in real-world production environments.

Secure storage: protecting sensitive data at rest

The biggest mistake teams make is storing secrets in plain text. SharedPreferences feels convenient, but any data you write there lives unencrypted on the device. An attacker with physical access, or an exploit that gains file-system access, can extract passwords, tokens, and API keys in seconds.

Android provides two practical options for secure storage. The first is EncryptedSharedPreferences, which encrypts all key-value pairs transparently. It's backed by the Android Keystore system and requires minimal code changes.

kotlin
// Old way: dangerous
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("api_token", token).apply()

// New way: encrypted
val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "secret_prefs",
    MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit().putString("api_token", token).apply()

The second option is the Android Keystore directly, which gives you finer control over key lifecycle, access restrictions, and expiration. Use this when you need per-key encryption, rotation policies, or biometric-gated access.

kotlin
// Example: storing a token with Keystore
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)

// Generate a key (if it doesn't exist)
if (!keyStore.containsAlias("my_secret_key")) {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore"
    )
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            "my_secret_key",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .build()
    )
    keyGenerator.generateKey()
}

// Encrypt and store your token
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val key = keyStore.getKey("my_secret_key", null)
cipher.init(Cipher.ENCRYPT_MODE, key)
val encryptedToken = cipher.doFinal(token.toByteArray())
// Persist encryptedToken to disk safely

For most apps, EncryptedSharedPreferences is the right default. It handles key management automatically and works across API levels 21 and up. Move to raw Keystore only if you need specific constraints like biometric auth or time-based expiration.

Network security: preventing man-in-the-middle attacks

HTTPS is non-negotiable. Every API call should go over TLS 1.2 or higher. But HTTPS alone isn't enough—certificate pinning adds a second layer of defense by locking your app to a specific certificate or public key, preventing attackers from using their own certificates even if they compromise a CA.

Start with a network security configuration file. Create res/xml/network_security_config.xml:

kotlin
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2026-01-01">
            <pin digest="SHA-256">base64-encoded-public-key-hash-1</pin>
            <pin digest="SHA-256">base64-encoded-public-key-hash-2</pin>
        </pin-set>
    </domain-config>
    <!-- Allow cleartext only for debug builds if needed -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

Then reference it in AndroidManifest.xml:

kotlin
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>
</application>

To get your certificate's public key hash, run:

kotlin
openssl s_client -connect api.example.com:443 < /dev/null \
  | openssl x509 -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl enc -base64

Always include two pins: one for your current certificate and one for a backup (e.g., a future renewal). If you pin only one and it expires unexpectedly, your app breaks for all users. Set an expiration date on the pin-set and rotate it proactively.

Configure your HTTP client (OkHttp, Retrofit) to enforce no cleartext traffic in production:

kotlin
val client = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .build()

// Verify your requests go through TLS
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com")
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Do not ship secrets in your codebase

API keys, OAuth tokens, signing certificates, and database credentials should never be committed to git or embedded in your APK. An attacker can decompile your APK in seconds and extract any hardcoded string.

We've seen teams burn days revoking leaked keys and rotating credentials because someone checked an AWS key into version control. Automate detection with pre-commit hooks:

kotlin
#!/bin/bash
# .git/hooks/pre-commit
if git diff --cached | grep -E '(AKIA|sk_live_|api_key|password)'; then
    echo "ERROR: Secrets detected in staged changes."
    exit 1
fi

For API keys and tokens you do need at runtime:

  • Fetch from a secure backend: Have your app authenticate to your server, then receive keys server-side. Keys never touch the APK.
  • Use Cloud-based config: Firebase Remote Config or similar services let you distribute keys without rebuilding.
  • Environment-specific builds: Different build variants (debug, staging, production) can have different (non-secret) configuration.

For credentials your app must store (like OAuth refresh tokens), use EncryptedSharedPreferences or Keystore as covered above. Never put them in BuildConfig constants or string resources.

Obfuscation: making reverse engineering harder

R8 is Android's built-in code shrinking and obfuscation tool. It removes unused code, renames classes and methods to meaningless single letters, and inlines methods. This makes your APK smaller and much harder to reverse-engineer.

Enable R8 in build.gradle (it's the default for release builds since AGP 3.4.0):

kotlin
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Create or update proguard-rules.pro to exclude classes your app or libraries need at runtime via reflection:

kotlin
# Keep classes used by name (e.g., Gson deserialization)
-keep class com.example.app.models.** { *; }

# Keep class and method names referenced in string literals
-keepclasseswithmembers class com.example.app.** {
    *** *(...);
}

# Example: keep Firebase and other third-party libs as they specify
-keeppackagenames com.google.firebase
-keep class com.google.firebase.** { *; }

Test your release builds thoroughly. Obfuscation can break runtime reflection and serialization if your rules aren't correct. Run the app on a real device before shipping.

Obfuscation is not encryption. A determined attacker can still decompile your code. But it raises the bar significantly and protects against casual reverse-engineering and API scraping.

Common mistakes

  • Storing tokens in unencrypted SharedPreferences: We still see this in production code. Use EncryptedSharedPreferences or Keystore, period.
  • Disabling certificate pinning for "debugging": Code that checks for BuildConfig.DEBUG and skips pinning in debug builds can end up in release APKs. Use separate certificate configs instead.
  • Committing secrets then rotating them later: git history is permanent. Any secret ever pushed should be considered compromised. Revoke immediately.
  • Over-obfuscating third-party SDK classes: If a library expects its public API to exist, obfuscating it breaks functionality. Trust the library's proguard rules; extend yours for your own code.
  • Assuming network security config alone protects you: A malicious app on the same device can still spy on your traffic via proxy. Always validate server-side actions and never trust client-side logic alone.

Key takeaways

  • Use EncryptedSharedPreferences for most sensitive data; graduate to Keystore for fine-grained control or biometric gates.
  • Enforce HTTPS with network security configuration and certificate pinning; rotate pins proactively.
  • Never hardcode secrets. Fetch them from a backend or use cloud-based config. Treat any secret in git history as compromised.
  • Enable R8 minification and shrinking in release builds to reduce APK size and deter reverse-engineering.
  • Test release builds end-to-end before shipping. Obfuscation and proguard rules can break reflection and serialization if misconfigured.
  • Security is layered. No single tool (encryption, pinning, obfuscation) is a silver bullet. Combine them, monitor for breaches, and rotate credentials regularly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles