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
| Feature | What happens |
|---|---|
| Shrinking | Removes unused classes, methods, fields |
| Optimization | Inlines methods, simplifies code |
| Obfuscation | Renames classes/methods to a, b, c |
| Resource shrinking | Removes 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:
- Network: HTTPS + certificate pinning blocks interception
- Storage: EncryptedSharedPreferences protects data at rest
- Code: R8 obfuscation makes reverse engineering harder
- Secrets: Server-side proxy keeps API keys off the device
- 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.