androidengineers.Book a session

Security Architecture

ProGuard/R8 & Secure Coding

article20 minHard

R8 (Android's minifier/obfuscator, replacing ProGuard) does three things: removes unused code (shrinking), renames classes/methods to short names (obfuscation), and optimizes bytecode. Together they reduce APK size and make reverse engineering harder.

R8 vs ProGuard

R8 is enabled by default in Android Gradle Plugin 3.4+. It performs all three tasks in a single step, is faster than ProGuard, and produces smaller output.

// build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true          // enable R8 shrinking/obfuscation
            isShrinkResources = true        // also shrink unused resources
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Essential ProGuard Rules

# Keep data classes used with Gson/Kotlin Serialization (reflection-based)
-keep class com.example.data.** { *; }

# Keep enum classes (R8 can break them)
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# Keep Parcelable implementations
-keepclassmembers class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator CREATOR;
}

# Keep custom exceptions
-keep public class * extends java.lang.Exception

# Keep classes annotated with @Keep
-keep @androidx.annotation.Keep class * { *; }
-keepclassmembers class * {
    @androidx.annotation.Keep *;
}

The @Keep Annotation

Instead of blanket rules, annotate specific classes or methods that must not be obfuscated:

@Keep  // R8 won't rename or remove this class
data class ApiResponse(
    val id: String,
    val title: String,
    val body: String
)

class MyReflection {
    @Keep  // R8 won't rename this method
    fun callFromNativeCode() { ... }
}

Debugging Obfuscation Issues

When a release build crashes with obfuscated stack traces, use the mapping file to de-obfuscate:

# De-obfuscate a stack trace
java -jar retrace.jar \
  app/build/outputs/mapping/release/mapping.txt \
  obfuscated-stacktrace.txt

Firebase Crashlytics automatically de-obfuscates if you upload the mapping file:

// build.gradle.kts — upload mapping file automatically
plugins {
    id("com.google.firebase.crashlytics")
}
// Crashlytics uploads mapping.txt during release build

Secure Coding Practices

Don't Store Secrets in Code

// ❌ Secrets in source code — visible in APK even with obfuscation
const val API_KEY = "sk-live-abc123xyz"  // extractable from APK via strings command

// ✅ Fetch from server; store in encrypted prefs
class SecretManager(private val api: SecretsApi) {
    suspend fun getApiKey(): String = api.fetchApiKey(deviceToken)
}

// ✅ Or use BuildConfig for non-sensitive config, stored in server-side
// build.gradle.kts — for non-secret values
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")

Input Validation

// Validate all user input at system boundaries
fun parseUserId(input: String): String? {
    // Allow only alphanumeric + hyphens, max 36 chars (UUID format)
    val regex = Regex("^[a-zA-Z0-9\\-]{1,36}$")
    return if (regex.matches(input)) input else null
}

// SQL injection: Room prevents this with parameterized queries — use @Query, not raw execSQL
@Query("SELECT * FROM articles WHERE id = :id")  // parameterized — safe
suspend fun getById(id: String): ArticleEntity?

// ❌ Never build SQL by concatenation:
// db.execSQL("SELECT * FROM articles WHERE id = '$id'")  // SQL injection!

Secure Logging

// ❌ Logging sensitive data — visible in adb logcat and device logs
Log.d("Auth", "Token: $authToken")
Log.d("User", "Email: $userEmail")

// ✅ Use Timber with a tree that strips sensitive logs in release
class ReleaseTree : Timber.Tree() {
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (priority < Log.WARN) return  // only log warnings and errors in release
        // Send to Crashlytics or similar — no PII
    }
}

// In Application.onCreate:
if (BuildConfig.DEBUG) {
    Timber.plant(Timber.DebugTree())
} else {
    Timber.plant(ReleaseTree())
}

Key Takeaways

ConceptRule
R8 enabledEnable for all release builds; never disable
Mapping fileUpload to Crashlytics; commit the location; never delete
@KeepAnnotate classes used via reflection; prefer over blanket rules
Secrets in codeNever — extract, fetch from secure source, or use Keystore
SQL injectionRoom's @Query with parameters prevents it; never concatenate SQL
Log sanitizationTimber + release tree that strips sensitive data

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
ProGuard/R8 & Secure Coding | Android System Design | Android Engineers