Skip to main content
Start your own AI-powered blog — freeGet started →

Best Practices for Android App Security in 2026

Podcast episode2 voices
3:43
Best Practices for Android App Security in 2026
Photo by Denny Müller on unsplash

Release signing, R8, network security config, certificate pinning, and secure storage — the hardening checklist I run on every Android build before it ships.

I once reviewed an Android app for a client that had its entire API key, a database password, and a payment gateway secret sitting in a plaintext constants.kt file inside the APK. Anybody could have decompiled the app in under a minute and walked off with production credentials. The app was a year old and had never shipped an update that fixed it, because nobody had ever told the team that an APK is not a secret container — it is a public ZIP file that anyone can open.

Android security in 2026 is mostly boring, and that is exactly the point. The attacks that actually happen are the cheap ones: decompile the app, read the secrets, flip a flag, steal a token. The best practices in this article exist to make those cheap attacks expensive. I have spent years hardening production apps, and this is the checklist I now run on every build — release signing and Play App Signing, R8 minification, the network security config, certificate pinning, secure storage, and the tamper checks that are actually worth the effort.

1. Release Signing and the Keystore — the Line Between Yours and Someone Else's

Your app's signature is its identity. The two mistakes that matter, in order:

Mistake one: using the same keystore for debug and release. Google Play rejects apps signed with a debug key, and mixing keys causes a nightmare of "app already exists" failures. Your release keystore is generated once, kept out of version control, and protected by a strong password.

Mistake two: losing the keystore. If you lose the upload key, you can recover via Play App Signing by re-registering your upload key. But if you lose the app signing key in Play App Signing, the app is dead — you cannot update it, and users keep the last version forever. Store the keystore in at least two places: a password manager and a physical drive, never in the repo.

In 2026, the setup I recommend is Play App Signing with a split key model: Google holds the app signing key, you hold an upload key used only to upload to Play. That way even if an attacker gets your upload key, they cannot re-sign the app under your identity.

groovy
// build.gradle.kts (app module)
android {
    signingConfigs {
        create("release") {
            storeFile = file(System.getenv("RELEASE_STORE_FILE"))
            storePassword = System.getenv("RELEASE_STORE_PASSWORD")
            keyAlias = System.getenv("RELEASE_KEY_ALIAS")
            keyPassword = System.getenv("RELEASE_KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}

Note the environment variables: credentials come from CI or a local .env, never from the build file itself. I have seen signing passwords committed to build.gradle more times than I can count, and each one was a credential dump waiting for a leak.

2. R8 Minification and Obfuscation — Make the Cheap Attack Expensive

R8 (the successor to ProGuard, enabled by isMinifyEnabled = true in release) does four things: removes unused code, shrinks resources, renames classes and methods to meaningless names, and can be told to strip logging. It is free defense-in-depth: a decompiled, obfuscated APK is dramatically more painful to reverse than the clean constants.kt layout I found in that client's app.

The three rules that keep R8 from breaking your app:

  1. Keep rules for reflection and serialization. Any class loaded by reflection or Gson needs a keep rule. The crash usually shows up in release-only QA, so test the release build, not just debug.
  2. Strip logging in release. Add -assumenosideeffects rules for android.util.Log or use a wrapper so debug logs physically disappear from the release APK — logs leak URLs, tokens, and data.
  3. Keep a mapping file. R8 produces mapping.txt; upload it to Play Console or keep it in CI so your crash reports map back to readable names. A release build without its mapping file is an app you cannot debug.
proguard
# Keep models used by Gson (never rely on reflection-safety by accident)
-keep class com.yourapp.data.models.** { *; }
-keep class com.google.gson.reflect.TypeToken { *; }

# Strip logging in release (after verifying nothing depends on it)
-assumenosideeffects class android.util.Log {
    public static int d(...);
    public static int v(...);
    public static int i(...);
}

The honest limit: R8 obfuscation is a speed bump, not a wall. A determined reverse engineer will still get through it. Its real value is removing the embarrassing default — the app that leaks its secrets in plain text on the first decompile.

3. Network Security Config — HTTPS Everywhere, Enforced

Android has supported HTTPS-only enforcement since Android 9 (API 28), and cleartext traffic is the kind of thing you fix in one file. The default network security config denies cleartext — which means if your app currently talks to http://your-api.com, it will break, and breaking it is correct, because that traffic is readable by anyone on the network.

xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <!-- Enforce HTTPS everywhere; no cleartext at all -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>
    <!-- If you MUST allow a specific dev endpoint, scope it tightly -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">10.0.2.2</domain> <!-- emulator loopback only -->
    </domain-config>
</network-security-config>

Reference it from the manifest:

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

The rule of thumb: cleartextTrafficPermitted="false" globally, and if you genuinely need a plain-HTTP dev endpoint, allow it only for a loopback address, never a wildcard domain. The number of "secure" apps I have seen with a wildcard cleartext domain for "just staging" is higher than I would like to admit.

4. Certificate Pinning — With a Working Rotation Plan

Pinning means your app verifies not just that the connection is TLS, but that the server presents the exact certificate or public key you expect. It defeats man-in-the-middle attacks that work by installing a rogue CA on the device. In 2026, the practical approach is public key pinning — pin to a key hash rather than a certificate — so certificate renewals do not break your app.

kotlin
val certificatePinner = CertificatePinner.Builder()
    .add("your-api.com",
         "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("your-api.com",
         "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup key
    .build()

The second hash is the rotation strategy: when your primary key rotates, you ship an update that adds the new key while the old one still works. Without a backup pin, a certificate renewal bricks every active install of your app, and that is how otherwise-sensible teams talk themselves out of pinning entirely.

The caveat that keeps pinning honest: never pin your CDN or third-party analytics domains — those rotate certificates and IPs constantly, and you will turn a minor infra change into a full app release. Pin only the API endpoints you fully control.

5. Secure Storage — Keystore, Not SharedPreferences

SharedPreferences is plaintext on disk. Any app on the device with the right permissions (or an attacker with root) can read it. Secrets belong in the Android Keystore, which keeps private keys in a hardware-backed secure element on modern devices.

For app data, the right answer in 2026 is EncryptedSharedPreferences, which wraps your preferences with keys held by the Keystore:

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

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

prefs.edit().putString("access_token", token).apply()

The rules that matter: tokens and PII go in EncryptedSharedPreferences or the Keystore, never SharedPreferences; hardware-backed Keystore keys can be configured with setUserAuthenticationRequired(true) for sensitive operations; and never log a token, ever, in any build. The Keystore is the one place on Android where a secret is stored as hardware, not as a file someone can copy.

6. Tamper and Integrity Checks — the Ones That Earn Their Keep

Root detection and integrity checks get a bad name because a naive implementation bans half your user base. The version I recommend is targeted and honest:

  • Signature verification at startup. Confirm your app was signed by your key. Cheap, effective, and it blocks repackaged "modded APK" versions of your app.
  • Basic root detection, but as a warning, not a hard block — legitimate power users exist, and you want to know, not ban.
  • Play Integrity API for anything that matters financially — it is Google's server-side attestation and it is the strongest signal you can get about whether the device and app are genuine.

The warning that has saved me repeatedly: an integrity check that returns a false positive is worse than no check at all, because it locks out paying users. Ship integrity as data (log it, alert on spikes), make blocking a policy decision per check, and never let a root-detection heuristic be the sole gate on a revenue flow.

7. The Supply Chain You Forget: Dependencies and Build Secrets

The cheapest exploit in 2026 is not against your code — it is against your dependencies and your build machine. Most apps pull in hundreds of transitive libraries, and any one of them can be a poisoned package: a typosquat that slipped into a popular repository, or a legitimate library whose maintainer account was hijacked. The mitigations are boring and effective:

  1. Lock your dependencies. Commit gradle.lockfile (or the Gradle dependency locking plugin) so builds are reproducible. An unlocked Gradle build can silently resolve a different version of a dependency a month later — and you will never notice.
  2. Scan every dependency before release. Run an SBOM (software bill of materials) generator on each build and feed it into a vulnerability scanner. The OWASP Dependency-Check plugin is free, runs in Gradle, and flags known CVEs in your dependency tree before you ship them.
  3. Treat your CI runner as a production server. Secrets in CI must come from the platform's secret store, never from the repo. A leaked gradle.properties with a signing password has the same blast radius as a leaked server key.
  4. Ask what a library actually needs. A UI animation library that requests INTERNET and READ_EXTERNAL_STORAGE at runtime should raise an eyebrow. Audit the permissions your dependencies declare in the merged manifest, not just the ones you wrote.

The reason this section exists: I audited one app where the signing keystore password sat in a committed gradle.properties, the dependency tree had an unpinned vulnerable JSON parser, and a third-party SDK was declaring permissions the app never asked for. None of that shows up in a static review of your own code. All of it showed up in a supply-chain pass — and all of it was exploitable.

8. Android 12+ Opt-Outs That Matter for Privacy

Android 12 (API 31) and later give you two controls that users increasingly check before installing:

  • Approximate location. If your app only needs a city-level location, declare the ACCESS_COARSE_LOCATION only and never request precise. Reviewers and privacy-conscious users notice when a calculator wants your GPS.
  • Uninstall attribution. Declare ALLOW_UNINSTALL_AND_UPDATE in your app's admin policy only if you genuinely need it; abusing device admin is a fast route to an unfavorable review.

Privacy hygiene is part of security because a leak is a leak whether the attacker is a hacker or an ad SDK. Every SDK you include is a new party with a copy of your data — prune them as aggressively as you prune code.

The Pitfalls Checklist

Before every release, I run this list:

  • Release keystore generated, backed up in two places, never in the repo
  • Play App Signing enabled with split key model
  • R8 minification + resource shrinking on; release build crash-tested, not just debug
  • Logging stripped from release builds; mapping.txt preserved
  • cleartextTrafficPermitted="false" globally; dev endpoints scoped, not wildcarded
  • Certificate pinning on your own API domains only, with a backup key for rotation
  • Secrets in EncryptedSharedPreferences/Keystore, never SharedPreferences
  • No credentials, tokens, or secrets anywhere in app code or resources
  • Integrity checks returning data, with blocking gated per check
  • mapping.txt uploaded so release crashes are readable
  • Dependency tree locked and SBOM-scanned for CVEs per build
  • CI secrets from the platform secret store, never from the repo
  • Merged manifest audited for unexpected SDK permissions

The Honest Closing

No checklist makes an Android app unhackable — it makes it expensive to hack, and that is the actual goal. The attacks that succeed against most apps are the lazy ones: a plaintext API key, a cleartext endpoint, an unpinned connection, a token in SharedPreferences. Each best practice in this article closes one of those cheap doors. The client app I reviewed had seven of them open. After a hardening pass, it shipped with the key in the Keystore, HTTPS enforced, logging stripped, and an app signing model that meant losing the upload key would not kill the product.

Security on Android is not exotic cryptography; it is a series of boring defaults done deliberately. Do those boring defaults right, in the order above, and the first thing a decompiler finds in your APK will be nothing worth taking.


*Gulshan Yad

Integrating Play Integrity API and Bundle Verification

The Android App Bundle format, introduced in 2020, is now the default packaging method for all new Play Store submissions. It reduces the APK size by delivering only the necessary modules for a device, but it also introduces a new layer of security: the Play Integrity API. This API returns a signed token that confirms the authenticity of the APK, the integrity of its code, and the legitimacy of the installation source. By validating this token on your backend before accepting any sensitive requests, you create a gatekeeper that prevents untrusted or tampered binaries from reaching your services.

Beyond the API, the Play App Signing service keeps signing keys in a Google‑managed secure enclave. Developers never handle the master key, which eliminates the risk of key leakage during build or distribution. Coupled with the Play Integrity API, this two‑tier approach ensures that only the exact binary you built is ever installed on a device. The Play Store also verifies that the bundle’s SHA‑256 matches the published signature, and any deviation triggers an automatic rollback.

When implementing the API, keep the token payload short and store it in a secure database with an expiration window. Rotate the token every few hours if you expose it through a public endpoint. Additionally, consider adding a secondary check on the device itself by verifying the device's integrity level—rooted, compromised, or normal—using the SafetyNet API. This layered defense keeps malicious actors at bay even if they manage to bypass one check.

Hardware‑Backed Key Management with Android Keystore

The Android Keystore system provides a hardware‑backed repository for cryptographic keys. Keys generated here never leave the secure element, which is isolated from the rest of the operating system. This isolation protects against memory‑dump attacks, even on rooted devices. For data at rest, use AES‑256 in GCM mode, leveraging the Keystore to generate and store the symmetric key.

Key rotation is essential to mitigate the risk of key exposure. A practical strategy is to generate a new key alias quarterly, then migrate all encrypted data to the new key. Automate this migration in your CI pipeline: decrypt the data with the old key, re‑encrypt with the new key, and delete the old alias. The Keystore API allows you to mark a key as non‑exportable, ensuring that even if a device is compromised, the key material cannot be extracted.

The Keystore also supports key derivation functions like PBKDF2, which can generate keys from user passwords without storing the password itself. Combine this with a strong salt stored in the Keystore to defend against dictionary attacks. When designing your key‑management strategy, always assume that the user’s device may be physically compromised; rely on hardware isolation and frequent rotation to reduce the window of vulnerability.

Dynamic Permissions and Least‑Privilege Data Access

Android’s runtime permission model encourages developers to request permissions only when they are essential. This reduces the attack surface and gives users clearer context. The most common pitfall is requesting all permissions at app launch, which can trigger a cascade of permission prompts and erode trust.

A structured approach is to group permissions into logical modules: camera, location, contacts, and storage. When a feature is activated—say, a photo‑sharing tool—you prompt for CAMERA and READ_EXTERNAL_STORAGE. If the user denies, you disable the feature gracefully and explain the limitation. This not only follows Google’s best practices but also satisfies privacy regulators that mandate explicit consent.

Beyond permissions, consider using Android’s Content Provider framework to expose only the data your app needs. For example, if you need to share a subset of contacts with a third‑party service, expose a custom provider that filters out sensitive fields. This encapsulation prevents accidental data leakage and aligns with a least‑privilege stance.

Secure Network Communication and Certificate Pinning

TLS 1.3 is now the de‑facto standard for secure network traffic. It reduces handshake latency, eliminates many attack vectors present in TLS 1.2, and mandates forward secrecy. Android 13 enforces TLS 1.3 for all HTTPS connections by default, but you must explicitly enable it in your HTTP client configuration.

Certificate pinning adds an extra layer of protection against man‑in‑the‑middle attacks. Rather than trusting the entire CA chain, you pin the server’s public key or certificate fingerprint in a Network Security Configuration XML file. This file can be updated via the Play Store without rebuilding the APK, ensuring that pinning remains current even when your backend rotates certificates.

Use a robust HTTP client such as OkHttp, which supports both TLS 1.3 and pinning out of the box. Configure retry logic, exponential backoff, and timeouts to prevent denial‑of‑service scenarios. Additionally, log all failed handshake attempts with anonymized metadata so you can detect patterns indicative of an active MITM attack.

Automated Security Testing in CI/CD Pipelines

Security must be baked into every stage of the development lifecycle. Start by integrating static code analysis tools—such as FindBugs, SpotBugs, or Android Lint—into your build process. These tools flag hardcoded secrets, insecure API usage, and potential injection points.

Next, run dependency‑vulnerability scans on every build. Tools like OWASP Dependency‑Check detect known vulnerabilities in third‑party libraries. If a critical issue surfaces, block the merge until the library is patched or replaced. Combine this with a fuzzing framework that sends random inputs to your app’s exposed APIs, revealing edge‑case crashes and memory corruption.

Finally, automate penetration tests against a staging environment that mirrors production. Use tools like Burp Suite or OWASP ZAP to probe authentication bypass, insecure data storage, and insecure background services. Store the results in a central repository and generate a compliance report that can be reviewed by security leads before any release.

Incident Response and Post‑Breaches for Android Apps

Even with rigorous prevention measures, breaches can occur. The first step in a response is to collect forensic evidence: logs, crash reports, and device identifiers. Use a centralized crash‑analytics platform that anonymizes user data but retains enough context to reconstruct the attack path.

Once you confirm a breach, isolate the affected services. Rotate all keys in the Keystore, revoke compromised certificates, and push an immediate update that disables the vulnerable feature. Communicate transparently with users through in‑app notifications and email, explaining the risk and the steps you’re taking.

After remediation, conduct a post‑mortem. Identify the root cause—whether it was a misconfigured permission, an outdated library, or a zero‑day exploit—and update your threat model accordingly. Incorporate lessons learned into the next sprint’s backlog, ensuring that the same vulnerability can’t reappear. Continuous monitoring, automated alerts, and a well‑documented incident‑response playbook are essential for maintaining user trust in a rapidly evolving threat landscape.

Key Takeaways

  • Use the Android App Bundle format and Play Integrity API to confirm that every install originates from the Play Store and that the APK has not been tampered with, preventing unauthorized reverse‑engineering and distribution.
  • Store all sensitive data on the device in the Android Keystore, using hardware‑backed AES‑256 keys that never leave the secure element and rotate them automatically on a quarterly basis.
  • Apply the principle of least privilege by requesting runtime permissions only when needed, grouping them logically, and providing clear rationale to the user so they can make informed choices.
  • Encrypt every network connection with TLS 1.3, enforce certificate pinning via the Network Security Configuration XML, and avoid legacy protocols such as SSL v3 or TLS 1.0/1.1.
  • Leverage Google Play App Signing and Play Protect to keep signing keys secure, enable automatic updates, and monitor for compromised certificates or suspicious install sources.
  • Integrate static analysis, dependency vulnerability checks, and fuzzing into the CI/CD pipeline; run these tests on every build, and treat any critical findings as blockers before deployment.

Frequently Asked Questions

How does the Play Integrity API improve app security compared to older integrity checks?

The Play Integrity API provides a signed token that verifies the app’s installation source, the integrity of the APK, and whether the device is rooted or in a compromised state. Unlike legacy checks, it is validated server‑side, making spoofing nearly impossible and ensuring the app runs only on legitimate devices.

What is the difference between the Android Keystore and a software‑based key store?

The Android Keystore stores cryptographic keys in a hardware‑backed secure element or Trusted Execution Environment. Keys never leave the secure element, providing resistance against memory dumps or key extraction, whereas software‑based stores are vulnerable to root or memory‑based attacks.

When should I request runtime permissions instead of declaring them in the manifest?

Request permissions at the moment you need them—e.g., when opening a camera or accessing contacts. This reduces the attack surface, gives users context, and aligns with Google’s recommendation that permissions be requested on‑demand rather than all at launch.

How can I implement certificate pinning without hardcoding the certificate in the code?

Use the Network Security Configuration XML to declare a set of trusted certificates or public keys. The configuration is external to the codebase, allowing updates through OTA or Play Store releases without rebuilding the APK.

What are the best practices for rotating keys stored in the Keystore?

Generate a new key alias every quarter, retire the old alias, and migrate stored data by decrypting with the old key and re‑encrypting with the new one. Automate this process in your build pipeline to avoid manual errors.

How can I detect a data leak in a third‑party library I use?

Run the library through a dependency‑vulnerability scanner like OWASP Dependency‑Check, and monitor its network traffic in a sandboxed environment. If the library initiates outbound connections to unknown hosts, flag it for review or replacement.

What steps should I take if my app is flagged by Play Protect?

Investigate the reported behaviors—check for suspicious code paths, unexpected network traffic, or unapproved permissions. If a legitimate issue is found, patch it and resubmit. If it’s a false positive, file a support ticket with Play Protect and provide detailed logs.

How do I balance security with user experience when using strict app signing and updates?

Use Google Play’s on‑device automatic update feature, which enforces signature verification before installation. Pair this with a user‑friendly update prompt that explains the security benefit, ensuring users receive patches without manual intervention.

G
Gulshan Yadav

1 followers

AI systems builder · 7 years in production. RAG, self-hosted infra, agent architecture. 📬 Deep-dives → mrgulshanyadav.substack.com

Comments

Sign in to join the conversation

No comments yet. Be the first to share your thoughts!

More from Gulshan Yadav

Recommended for you