Securing API Keys and Sensitive Data in Android Apps: A Practical, Modern Guide
1. Introduction
Importance of Secure Key Management in Mobile Apps
Modern Android apps are no longer just code, they are gateways to maps, payments, analytics, authentication, storage, and more. Every one of these connections depends on API keys, tokens, and cryptographic material. When these secrets are exposed, attackers don’t just gain access, they gain control. Reverse-engineered APKs, intercepted traffic, and automated abuse can unravel user trust, drain finances, disrupt services, and even trigger policy violations.
And the threats are escalating:
Android malware surged by 40% in 2024/2025, new attack vectors are exploiting Android 14/15, and quantum computing is pushing us toward post-quantum cryptography readiness.
Get Numesh Dilusha’s stories in your inbox
Join Medium for free to get updates from this writer.
The message is clear, securing secrets in Android apps is no longer optional; it’s survival.
Common Use Cases for Keys
- API authentication: calling first- and third-party services
- Analytics and attribution: SDK initialization keys
- Payment flows: ephemeral tokens and session keys
- User authentication: OAuth access/refresh tokens, session cookies
- Local protection: keys for encrypting on-device data
Consequences of improper storage
- Quota and billing abuse
- Data leaks and compliance issues
- Service bans and degraded user trust
- Incident response overhead and costly key rotations
Throughout this guide, we’ll focus on concrete, broadly applicable techniques sourced from official guidance and established practice:
2. What counts as “sensitive” data
API Keys and Tokens
- maps, analytics, cloud APIs, A/B frameworks
Cryptographic Keys
- Symmetric keys (AES) for local encryption
- Asymmetric keys (EC/RSA) for signing or key exchange
- MAC keys for message integrity
User Authentication Data
- OAuth tokens
- Session and refresh tokens
3. Threats and Risks
Reverse Engineering APKs
APKs can be decompiled (e.g., JADX, apktool), exposing hardcoded strings and resources.
Man-in-the-Middle (MITM) Attacks
If sensitive data is transmitted insecurely, attackers on the same network can intercept it. weak TLS config or mis-pinning can expose traffic.
Memory Dumps and Runtime Attacks
Rooted devices can be used to inspect memory and extract runtime secrets.
Credential Stuffing
leaked tokens used across apps/services.
Policy Enforcement
app stores prohibit embedding sensitive secrets directly in client code.
4. Common Mistakes in Key Storage
❌ Hardcoding Secrets in Code
val apiKey = "AIzaSy..." // Bad Practice❌ Committing Secrets to Version Control
Sensitive files like secrets.properties, signing keystores or debug only tokens should never be checked into Git.
❌ Storing Secrets in Plain SharedPreferences
sharedPreferences.edit().putString("api_key", "your_api_key_here").apply()❌ Storing in Plain-Text Files or Assets
context.openFileOutput("keys.txt", Context.MODE_PRIVATE).use {
it.write("API_KEY=your_key_here".toByteArray())
}❌ Using weak crypto or incorrect modes
Avoid ECB or CBC without integrity. Prefer AES-GCM with a fresh IV per message.
❌ Logging secrets
Never log tokens or keys; scrub logs in production builds.
5. Best Practices for Secure key handling
A) Don’t ship long‑lived server secrets — use your backend
- Treat the client as untrusted. Move privileged calls to your backend when possible.
- Exchange client credentials for short‑lived, scoped tokens issued by your backend.
- Validate requests on the server; don’t rely solely on client‑side checks.
Augment backend validation with:
- Device/app integrity signals (e.g., Play Integrity API) to reduce automated abuse.
- Rate limiting and anomaly detection on the server.
- Fine‑grained scopes and short token lifetimes.
B) Use the Android Keystore for cryptographic keys
- The Android Keystore stores keys in hardware‑backed secure elements where available (TEE/StrongBox) and prevents key material export.
- Prefer hardware‑backed keys and StrongBox when supported.
- Consider user‑authentication‑bound keys for sensitive operations (e.g., require biometrics before decryption).
- Handle key invalidation gracefully (e.g., biometric enrollment or device lockscreen changes can invalidate keys).
Kotlin: Generating an AES‑GCM key (Keystore)
val keyGenParams = KeyGenParameterSpec.Builder(
"app_aes_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setKeySize(256)
// Optionally tie to user auth:
// setUserAuthenticationRequired(true)
// setUserAuthenticationParameters(120, KeyProperties.AUTH_BIOMETRIC_STRONG)
// Prefer StrongBox when available:
// setIsStrongBoxBacked(true)
}.build()val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
).apply {
init(keyGenParams)
}
keyGenerator.generateKey()C) Use modern, authenticated encryption
- Use AES‑GCM with a new random IV per encryption, and store IV alongside ciphertext.
- Never reuse IVs; avoid static or derived IVs.
Kotlin: AES‑GCM encrypt/decrypt helpers
fun encryptAesGcm(alias: String, plaintext: ByteArray): Pair<ByteArray, ByteArray> {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val key = ks.getKey(alias, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key) // random IV
val iv = cipher.iv
val ciphertext = cipher.doFinal(plaintext)
return iv to ciphertext
}fun decryptAesGcm(alias: String, iv: ByteArray, ciphertext: ByteArray): ByteArray {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val key = ks.getKey(alias, null) as SecretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
return cipher.doFinal(ciphertext)
}D) Use Jetpack Security for preferences and files
- EncryptedSharedPreferences and EncryptedFile simplify secure on‑device storage.
- Internally, these use strong primitives with keys protected by the Keystore.
Kotlin: EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)encryptedPrefs.edit().putString("token", token).apply()
E) Keep build‑time secrets out of source
- Store local secrets in a file excluded by .gitignore (e.g., secrets.properties) or use a secrets plugin.
- Inject values at build time; never commit them.
Gradle snippet
// Do not commit secrets.properties
val secretProps = Properties().apply {
val f = rootProject.file("secrets.properties")
if (f.exists()) load(FileInputStream(f))
}android {
defaultConfig {
val mapsKey = (secretProps["MAPS_API_KEY"] ?: "") as String
buildConfigField("String", "MAPS_API_KEY", "\"$mapsKey\"")
}
}F) Harden your network layer
- Always use HTTPS with TLS; rely on the platform trust store or use Network Security Config for pinning or CA restrictions.
- Rotate pins gracefully and monitor failures if you pin.
- Avoid accepting user‑added CAs in release builds; keep debug configuration separate.
- Disallow cleartext traffic unless you have an explicit local/dev use‑case.
G) Obfuscate, but don’t rely on it
- Enable R8/ProGuard to obfuscate and shrink code.
- Obfuscation raises the bar but doesn’t replace good key management.
Gradle
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}H) Reduce exposure surface
- Don’t export components unless necessary; use explicit intents.
- Prefer immutable PendingIntent where possible.
- Apply least‑privilege permissions and signature‑level protection for internal IPC.
- Remove secrets from logs; configure log stripping for release.
- Exclude secrets from backups (e.g., via fullBackupContent / data extraction policies).
I) CI/CD and distribution
- Inject secrets at build/deploy time from your CI’s secret vault (e.g., GitHub Actions secrets).
- Prefer OIDC + cloud secret managers (GCP Secret Manager, AWS Secrets Manager, Azure Key Vault) to avoid long‑lived CI tokens.
- Protect signing keys and use Google Play App Signing.
- Automate secret rotation; keep short TTLs.
GitHub Actions example
- name: Create secrets.properties
run: |
{
echo "MAPS_API_KEY=${{ secrets.MAPS_API_KEY }}"
echo "ANALYTICS_KEY=${{ secrets.ANALYTICS_KEY }}"
} > secrets.properties⚠ Obfuscation slows down attackers but is not a replacement for proper key management.
6. Runnable sample module: Keystore + EncryptedSharedPreferences + Network Security Config
What it shows
- A Keystore‑backed AES‑GCM key for local encryption (no key export).
- EncryptedSharedPreferences for storing a token.
- Network Security Config that:
- In debug: allows user‑added CAs (helps local TLS interception while testing).
- In release: disallows user‑added CAs and cleartext traffic.
How to run
- Create a new empty Android project (Kotlin, no Compose required), or clone a sandbox project.
- Drop the “secure‑sample” module folder into the project root.
- Replace settings.gradle.kts with the one below (or add include(“:secure-sample”)).
- Sync and Run “secure‑sample” on a device/emulator.
- Use the UI to save/load a token and to encrypt/decrypt a message.
Files are provided at the end of this article — copy them as‑is into your project.
Practical Example ~ Kotlin Key Manager
class SecretKeyManager(private val context: Context) {
...
fun encrypt(data: String): String {
val bytes = encryptCipher.doFinal(data.toByteArray(Charset.forName("UTF-8")))
return Base64.encodeToString(bytes, Base64.DEFAULT)
}
fun decrypt(data: String): String {
val bytes = Base64.decode(data, Base64.DEFAULT)
return String(decryptCipher.doFinal(bytes), Charset.forName("UTF-8"))
}
}7. Testing and operations
- Threat model regularly: what happens if a token leaks? Can it be abused at scale?
- Secret scanning: enable automated scanning (e.g., GitHub secret scanning, gitleaks, trufflehog).
- Code review: include security checks for PRs that touch auth, storage, or networking.
- Monitor and rotate: set alerting on unusual API usage; rotate keys on suspicious events.
- Incident response: have a playbook for key revocation and app update rollout.
8. Notes for the newest Android versions (Android 16+)
Platform security and privacy hardening evolves each release. Before updating your targetSdk to the newest version:
- Keystore/KeyMint and StrongBox differences
- Don’t assume a fixed attestation schema; detect StrongBox availability at runtime.
- Handle user‑auth‑bound key invalidation (e.g., biometric enrollment or lockscreen changes). Be ready to re‑key and re‑establish sessions.
- Networking defaults and pinning
- Re‑validate your certificate pinning strategy, trust anchors, and fallback behavior after OS upgrades.
- Behavior changes
- Review the latest “Features and behavior changes” documentation for any changes that affect storage, cryptography, biometrics, or networking.
- Test release builds on real devices and across OEMs.
Treat these as proactive verification steps when moving to the newest platform level. Always validate on‑device behavior with your production build config.
9. Tools & Libraries
10. CI/CD Considerations
Inject Secrets Securely
- GitHub Actions:
secrets.* - GitLab CI/CD: Protected variables
Protect .keystore Files
- Use secrets for passwords
- Leverage Google Play App Signing
11. Real-World Key Leaks
- 2018: 100,000+ GitHub repos with exposed secrets
- 2019: Ride-sharing app exposed Firebase and Google Maps keys
- Consequences: Massive bills, app bans, loss of trust
Quick checklist
- Do not ship long‑lived secrets in the APK.
- Use short‑lived tokens from your backend; verify integrity signals server‑side.
- Use Android Keystore (prefer hardware‑backed) and AES‑GCM.
- Use Jetpack Security for encrypted preferences/files.
- Keep secrets out of VCS; inject via CI/CD; rotate regularly.
- Enforce TLS, consider pinning carefully, separate debug vs release trust.
- Minimize exported components; use immutable PendingIntent.
- Strip logs; exclude secrets from backups; automate secret scanning.
12. Conclusion
Key Takeaways
- Never hardcode secrets in your app
- Use Android Keystore and Jetpack Security
- Shift sensitive logic to backend when possible
- Use CI/CD secrets management
- Obfuscate as a last line of defense
Proactive Measures
- Automated secret scanning
- Code reviews for security
- Key rotation policies
- API usage monitoring
Final Thoughts
Securing your API keys and sensitive data is not just a best practice, it’s a necessity in modern Android development. As your app grows and handles more critical data, attackers will increasingly target your weakest points. By adopting secure storage techniques and combining them with a solid CI/CD pipeline and regular audits, you can safeguard both your users and your backend services.
Security is not a one-time task. Keep learning, stay up-to-date with platform updates, and always be proactive.
👋 Follow for more Kotlin tips, Android insights, and real-world development stories.










