PCSalt
YouTube GitHub
Back to Security
Security · 4 min read

Securing Android Apps — Certificate Pinning, ProGuard & Network Security Config

Harden your Android app — network security configuration, certificate pinning, ProGuard/R8 obfuscation, encrypted storage, and preventing reverse engineering.


Your Android app communicates with your server over HTTPS. That’s a start, but it’s not enough. An attacker on the same Wi-Fi can intercept traffic with a proxy. Your APK can be decompiled to read hardcoded secrets. Unencrypted SharedPreferences can be read on a rooted device.

This post covers the practical steps to harden your Android app.

Network Security Configuration

Android’s Network Security Configuration lets you customize how your app handles network connections — which certificates to trust, whether to allow cleartext traffic, and certificate pinning.

Block cleartext (HTTP) traffic

res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
</network-security-config>

Reference it in AndroidManifest.xml:

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

This blocks all HTTP traffic — only HTTPS is allowed. If any library or WebView tries to load an HTTP URL, it fails.

Allow cleartext for development only

<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

Allows HTTP only for local development (emulator’s host IP and localhost). Production traffic stays HTTPS-only.

Certificate pinning

HTTPS verifies the server’s certificate against the system’s trusted CA store. But if an attacker installs a rogue CA on the device (corporate proxy, malware), they can intercept HTTPS traffic.

Certificate pinning says: “Only trust THIS specific certificate or public key for my server.” Even if a rogue CA is installed, the pin check fails and the connection is rejected.

Pin with Network Security Configuration

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2026-01-01">
            <pin digest="SHA-256">base64encodedSHA256HashOfPublicKey=</pin>
            <pin digest="SHA-256">base64encodedBackupPin=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

Always include a backup pin — if your primary certificate is renewed or revoked, the backup prevents bricking your app.

Get the pin hash

# From a live server
openssl s_client -connect api.example.com:443 | \
  openssl x509 -pubkey -noout | \
  openssl pkey -pubin -outform DER | \
  openssl dgst -sha256 -binary | \
  base64

Pin with OkHttp (more control)

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

val certificatePinner = CertificatePinner.Builder()
    .add(
        "api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // primary
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="  // backup
    )
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

When to use certificate pinning

  • Banking and financial apps
  • Apps handling sensitive personal data
  • Apps communicating with your own server (you control the certificate)

When to skip it

  • Apps that connect to many different servers
  • Apps using third-party CDNs (certificate changes are unpredictable)
  • If you can’t commit to updating the app before the certificate expires

ProGuard / R8 — Code obfuscation

R8 (the default since AGP 3.4) shrinks, optimizes, and obfuscates your code. Without it, anyone can decompile your APK and read your source code.

Enable in build.gradle.kts

android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

What R8 does

FeatureWhat happens
ShrinkingRemoves unused classes, methods, fields
OptimizationInlines methods, simplifies code
ObfuscationRenames classes/methods to a, b, c
Resource shrinkingRemoves unused resources (images, layouts)

Keep rules

R8 might remove or rename things that are accessed via reflection (serialization, Retrofit interfaces). Add keep rules:

# Keep Retrofit interfaces
-keep interface com.example.api.** { *; }

# Keep serialization models
-keep class com.example.models.** { *; }

# Keep data classes used with Gson
-keepclassmembers class com.example.models.** {
    <fields>;
}

# Keep enum values
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

Mapping file

R8 produces a mapping.txt file that maps obfuscated names back to original names. Keep this file — you need it to deobfuscate crash reports.

Upload it to Firebase Crashlytics, Play Console, or your crash reporting tool.

Encrypted storage

EncryptedSharedPreferences

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val securePrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

// Use like regular SharedPreferences
securePrefs.edit().putString("auth_token", token).apply()
val savedToken = securePrefs.getString("auth_token", null)

Both keys and values are encrypted at rest. Even on a rooted device, the data is unreadable without the master key (stored in Android Keystore).

What to store securely

  • Authentication tokens
  • Refresh tokens
  • API keys (if they must be on-device)
  • Encryption keys
  • Sensitive user data

What doesn’t need encryption

  • User preferences (theme, language)
  • Non-sensitive app state
  • Cached public data

Hardcoded secrets

Never hardcode API keys or secrets in your code:

// NEVER do this
const val API_KEY = "sk-123456789abcdef"

Anyone can decompile the APK and find this string.

Alternatives

1. Build config fields (slightly better)

// build.gradle.kts
android {
    defaultConfig {
        buildConfigField("String", "API_KEY", "\"${System.getenv("API_KEY")}\"")
    }
}

// Usage
val key = BuildConfig.API_KEY

Still in the APK, but loaded from environment variables at build time. Not in source control.

2. Server-side proxy (best)

Don’t put the API key in the app at all. Have your server proxy the request:

App → Your Server (authenticated) → Third-party API (with API key)

The API key lives on your server, never on the device.

3. Android Keystore for runtime secrets

Store secrets in the hardware-backed Keystore:

val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)

// Generate a key
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
    KeyGenSpec.Builder("my_key_alias")
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build()
)
keyGenerator.generateKey()

Keys in the Keystore can’t be extracted — they can only be used for crypto operations within the Keystore.

Root detection

On rooted devices, the security sandbox is weakened. Detect rooting and warn the user or restrict functionality:

fun isDeviceRooted(): Boolean {
    val paths = listOf(
        "/system/app/Superuser.apk",
        "/system/xbin/su",
        "/system/bin/su",
        "/sbin/su",
        "/data/local/xbin/su",
        "/data/local/bin/su"
    )
    return paths.any { File(it).exists() }
}

Root detection is a hint, not a guarantee — determined attackers can bypass it. But it raises the bar.

For high-security apps (banking), consider libraries like SafetyNet/Play Integrity API for device attestation.

Checklist

Network

  • HTTPS only (cleartext blocked)
  • Certificate pinning for your API
  • Backup pins configured
  • Network security config in manifest

Code protection

  • R8/ProGuard enabled for release builds
  • Resource shrinking enabled
  • Keep rules for serialization/reflection
  • Mapping file uploaded to crash reporter

Storage

  • Tokens stored in EncryptedSharedPreferences
  • No hardcoded secrets in source code
  • API keys proxied through your server
  • Sensitive data cleared on logout

Runtime

  • Root detection (for sensitive apps)
  • Debug detection (prevent debugging release builds)
  • Screenshot prevention for sensitive screens
  • No sensitive data in logs

Summary

Android security is defense in depth:

  1. Network: HTTPS + certificate pinning blocks interception
  2. Storage: EncryptedSharedPreferences protects data at rest
  3. Code: R8 obfuscation makes reverse engineering harder
  4. Secrets: Server-side proxy keeps API keys off the device
  5. Runtime: Root detection and Play Integrity raise the bar

No single measure is bulletproof. But layered together, they make your app significantly harder to attack.