Skip to content
All posts
July 24, 20264 min read

ProGuard and R8 for Security: Obfuscating Sensitive Code

R8's obfuscation won't stop a determined reverse engineer, but it removes the easy wins and raises the cost of attacking your app. Here's what it actually protects, its limits, and how to configure it without breaking your build.

SecurityR8ProGuardAndroidObfuscation
Share:

There's a persistent myth that obfuscation makes your app secure. It doesn't, and believing it does is dangerous because it leads people to put secrets in client code thinking R8 will hide them. What obfuscation actually does is more modest and still worthwhile: it raises the cost of understanding and attacking your app. Knowing exactly what it buys — and what it doesn't — is the point.

What R8 Actually Does

R8 is the default shrinker and obfuscator in modern Android builds. With minification on, it strips unused code and resources, then renames classes, methods, and fields to short meaningless names. The decompiled output goes from readable, well-named logic to a maze of

code
a.b.c()
calls. That doesn't make the code unreadable, but it makes it tedious to follow, which deters casual inspection and slows down a serious one.

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

The shrinking is a real bonus: a smaller APK is a side effect of turning this on, so you get size and a security speed bump together.

Understand the Hard Limit

Here's the part people skip: obfuscation is not encryption. Every string constant in your code — including any API key or secret you embedded — is still right there in the binary, fully readable, renaming or not. A

code
const val API_KEY
survives R8 perfectly intact; the variable name changes, the value doesn't. So obfuscation does nothing to protect secrets, and anything that must stay secret cannot live in the client at all. It belongs behind a backend you control. Treat R8 as raising the cost of understanding your logic, never as hiding your data.

Configure Keep Rules Carefully

The practical pain with R8 is that it can break things by removing or renaming code that's referenced reflectively — serialization, some libraries, anything that looks up classes by name at runtime. The fix is keep rules that tell R8 to leave specific code alone.

text
-keep class com.example.model.** { *; }
-keepclassmembers class * { @kotlinx.serialization.Serializable *; }

The discipline is to keep as little as possible. Every broad keep rule is code you've opted out of shrinking and obfuscating, so over-keeping quietly defeats the purpose. Add narrow rules for exactly what breaks, test, and resist the urge to keep whole packages to make an error go away.

Test the Release Build, Not Just Debug

The classic R8 bug is an app that works perfectly in debug and crashes only in release, because minification removed something. This is why I always run the full app from a release build before shipping, not just the debug variant. Crashlytics with mapping files uploaded is essential here, because obfuscated stack traces are unreadable until de-obfuscated with the mapping file R8 produces. Upload that mapping file with every release or your production crash reports become a wall of

code
a.b.c
.

Use It, But Know Its Place

R8 belongs on every release build — the shrinking alone justifies it, and the obfuscation is a free speed bump against casual reverse engineering. Just hold the right mental model: it makes your app smaller and modestly harder to understand, and it does nothing to protect secrets. Pair it with keeping secrets server-side and the real security work elsewhere, and it does its modest job well. Mistake it for a security boundary, and it'll lull you into shipping the very secrets it can't hide.

The most damaging misconception I want to leave you with is the one that turns a useful tool into a liability: believing obfuscation protects secrets. It doesn't, and the danger is subtle, because the belief leads developers to embed an API key or a license check in client code and feel safe about it, when in reality the renamed code still hands over every string constant to anyone who unzips the APK. Hold the accurate model — R8 shrinks your app and makes your logic tedious to read, full stop — and you'll use it correctly: on every release for the size and the speed bump, while keeping anything that must stay secret on a server R8 has nothing to do with. The tool is good; the myth around it is what gets people hurt.

Key Takeaways

  • Enable R8 minification and resource shrinking on release builds — you get a smaller APK and a reverse-engineering speed bump together.
  • Obfuscation renames code but leaves string constants fully readable; it cannot protect embedded secrets.
  • Keep secrets server-side; nothing that must stay secret can live in client code, R8 or not.
  • Write narrow keep rules for reflectively-accessed code and keep as little as possible to preserve the benefit.
  • Always test a release build before shipping, and upload R8 mapping files so production crash traces are readable.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus