Skip to main content
Blog

Mobile

App integrity and obfuscation: R8 and Dart --obfuscate

Mobile security expertise checks how well an app resists tampering and reverse engineering. How to set up R8, Dart obfuscation and an integrity check.

· 7 min read

Photo: Profpcde, CC0 · Wikimedia Commons

A mobile app lands on the user's device in its entirety. Anyone can download the APK or IPA, unpack it, modify it and rebuild it. That is why a cybersecurity expertise looks not only at how a mobile app talks to the server, but also at how well the binary itself is protected. OWASP Mobile Top 10 (2024) calls this M7: Insufficient Binary Protections.

When the mobile app of our own product (Flutter, Android and iOS) went through expertise, several high-severity findings fell into this area: there was no integrity (tamper) check, and the code was not sufficiently obfuscated. Below is a practical account of how we closed them.

Two different protections: obfuscation and integrity

These two ideas are often confused, but they do different jobs:

  • Obfuscation makes code harder to read. Class, method and variable names are replaced with short meaningless ones, and unused code is removed. The goal is to increase the time reverse engineering takes.
  • Integrity checking means the app verifies at startup that it has not been modified. If someone unpacked the APK, changed the code and re-signed it with a different key, the app should detect this and stop.

Obfuscation is not a way to keep secrets. It only slows analysis down. That is why the two are used together.

Android: shrinking code with R8

On Android, the R8 compiler shrinks and obfuscates the Java/Kotlin layer. In a Flutter project it is usually enabled for release builds, but it is better to state it explicitly in android/app/build.gradle (or build.gradle.kts) — the expertise examines the release build:

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

Keep only the -keep rules you genuinely need in proguard-rules.pro. Keeping an entire package with -keep class com.example.** { *; } effectively disables obfuscation — a common mistake.

Flutter: building Dart code with --obfuscate

The core logic of a Flutter app is written in Dart, and R8 does not touch it. Dart code needs its own flag:

flutter build apk --release --obfuscate --split-debug-info=build/symbols
flutter build ipa --release --obfuscate --split-debug-info=build/symbols

--split-debug-info moves symbols out of the app into a separate folder. Do not publish that folder, but keep it: you need those files to read stack traces from crash reports with flutter symbolize.

Obfuscation does not encrypt string literals. If API addresses, keys or secrets sit in the code as plain strings, the strings command will still show them after obfuscation.

So in our app we stopped storing endpoints as plain strings, and we did not put secret keys inside the app at all — they stay on the server.

Integrity (tamper) checking

The main way to detect modification is to check the signing certificate. Every release build is signed with your key. Anyone who modifies the app has to re-sign it with their own key, because they do not have yours. At startup the app compares the hash of the current signature with the expected value.

You can write this by hand, but we used a ready-made RASP (Runtime Application Self-Protection) library, freeRASP. It detects, all at once:

  • a changed signature (tamper) and installation from an unofficial source;
  • a rooted or jailbroken device;
  • an emulator;
  • hooking tools such as Frida and an attached debugger.

The configuration takes the signing certificate hash (for Android) and the Team ID (for iOS). When a threat was detected, we did not simply close the app; we showed the user a clear blocking screen explaining why the app would not run and what to do.

One important detail: take the signature hash from the release key. If the app is distributed with Google Play App Signing, the store copy is re-signed with Google's key. In that case, also add the hash of the "App signing key" certificate from Play Console, otherwise the copy installed from the store will be treated as "modified".

Server-side confirmation

Any on-device check can be bypassed by someone with enough time and skill. For important operations it therefore helps to ask for confirmation on the server as well: Play Integrity API on Android, App Attest on iOS. This lets the server itself verify that the request comes from a genuine, unmodified copy of the app. It is not mandatory for every project, but it is worth considering for apps that handle financial or personal data.

Pre-expertise checklist

  1. R8 is enabled in the release build (isMinifyEnabled = true), with no unnecessary -keep rules.
  2. Dart code is built with --obfuscate --split-debug-info, and symbols are stored somewhere safe.
  3. Open the APK in jadx or a similar tool and confirm that class names are really obfuscated.
  4. Use strings to confirm that no keys, passwords or internal addresses are left in the binary.
  5. The signature (tamper) check works: re-sign the app with another key, install it and confirm the blocking screen appears.
  6. Root/jailbreak, emulator and hook detection have been tried on a test device.
  7. The file submitted for expertise is exactly the release file built with these settings.

These measures do not make an app unbreakable. They make reverse engineering and modification considerably more expensive — and that is what the expertise checks: that the protection layers exist and work.

Security scan

Want to know where your app stands on binary protection? Send your APK or IPA for a security scan. The Claude AI agent and our specialist will review obfuscation, signature checks and RASP measures and tell you what needs closing before the expertise.