App Security

Android Development โ€” Production & Publishing
Course 3 ยท Chapter 5 ยท App Security

๐Ÿ” App Security

An installed APK sits on the user's device, fully extractable and inspectable โ€” a fundamentally different threat model from a web server, which an attacker never gets to hold a copy of. This chapter covers four practical defenses: shrinking/obfuscating release code, pinning certificates against compromised CAs, storing secrets encrypted rather than plain, and biometric authentication for sensitive actions.

๐Ÿ—œ๏ธ ProGuard/R8 โ€” Shrinking and Obfuscating Release Builds

R8 (Android's modern successor to ProGuard, using the same rule syntax) strips unused code and renames classes/methods to short, meaningless names in release builds โ€” smaller APKs, and a real (though not absolute) speed bump against casual reverse engineering:

// app/build.gradle.kts buildTypes { release { isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } }

R8's aggressive renaming breaks anything relying on reflection to find a class or method by its original name โ€” Room, Retrofit/Moshi's JSON mapping, and Kotlin Intermediate Chapter 6's reflection all fall into this category. A proguard-rules.pro keep rule tells R8 to leave specific classes untouched:

// proguard-rules.pro โ€” protects classes that reflection needs by real name -keep class com.philip.myapp.data.** { *; } # Entities, API response models โ€” Moshi/Room need real field names
โš  Test the Release Build, Not Just Debug

Debug builds have isMinifyEnabled = false by default, so a missing keep rule can go completely unnoticed until a release build crashes โ€” often with a confusing ClassNotFoundException or a JSON field silently mapping to null, since Moshi/Room can no longer find the (renamed) class or property it expects. Genuinely testing a release build before shipping is what catches this class of bug.

๐Ÿ“Œ Certificate Pinning

The HTTPS/TLS course covered the certificate authority chain of trust โ€” a client trusts any certificate signed by a CA in its trust store. Certificate pinning narrows that further: the app hardcodes exactly which certificate (or public key) it expects from a specific server, rejecting connections even to a certificate that's technically CA-valid but doesn't match the pin:

val certificatePinner = CertificatePinner.Builder() .add("api.myapp.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build() val client = OkHttpClient.Builder() .certificatePinner(certificatePinner) .build() val retrofit = Retrofit.Builder() .baseUrl("https://api.myapp.com/") .client(client) // Retrofit (Course 2, Chapter 4) uses OkHttp underneath โ€” this is where it's configured .addConverterFactory(MoshiConverterFactory.create()) .build()

This defends against a specific, narrow threat: a compromised or coerced CA issuing a fraudulent-but-technically-valid certificate for the app's API domain, enabling a man-in-the-middle attack that a normal HTTPS connection wouldn't detect. Pinning is a deliberate trade-off, not a free upgrade โ€” a legitimate certificate rotation on the server that isn't matched by an app update locks out every installed copy of the app until it's updated, which is why pinning is reserved for apps handling genuinely sensitive data, not applied by default everywhere.

๐Ÿ”’ Secure Storage โ€” Beyond Plain DataStore

Course 2 Chapter 7's DataStore is unencrypted on disk โ€” fine for a theme preference, wrong for an auth token or API key. The Jetpack Security library provides an encrypted alternative, backed by the Android Keystore system (hardware-backed key storage on supported devices):

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("auth_token", token).apply()

The API deliberately looks like ordinary SharedPreferences โ€” the encryption is entirely handled underneath, with keys managed by the Android Keystore rather than stored alongside the encrypted data itself (which would defeat the purpose). This connects directly to the Auth course's coverage of session/token storage: on the web, an httpOnly cookie protects a token from JavaScript access; on Android, encrypted storage protects it from being read as plain text if the device's filesystem is ever accessed directly (a rooted device, physical access, a backup extraction).

๐Ÿ‘† Biometric Authentication

BiometricPrompt gates access to a specific action or screen behind the device's fingerprint/face unlock โ€” an additional local authentication factor, layered on top of (not replacing) whatever server-side auth the Auth course covered:

val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { // Proceed โ€” e.g. reveal a sensitive screen, or decrypt something locally } override fun onAuthenticationFailed() { // Wrong fingerprint/face โ€” the prompt itself already showed an error } }) val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Unlock to view account") .setNegativeButtonText("Cancel") .build() biometricPrompt.authenticate(promptInfo)

Mobile Security vs the Web Security Courses

Web Security Course ConceptAndroid Equivalent
CA chain of trust (HTTPS course)Certificate pinning narrows trust further, per-app
httpOnly cookie protecting a session token (Auth course)EncryptedSharedPreferences / Keystore-backed storage
MFA (Auth course, Chapter 6)BiometricPrompt as a local, device-level factor
Minified/obfuscated JS as weak security-by-obscurityR8 โ€” similarly not a substitute for real access control, but raises the bar

๐Ÿ’ป Coding Challenges

Challenge 1: A Keep Rule for a Reflection-Dependent Class

Given a data class ApiUser(val id: Int, val name: String) used as a Retrofit/Moshi response model, write the proguard-rules.pro keep rule that would protect it from R8's renaming, and explain in a comment specifically what would break (and how it would likely manifest) if this rule were missing in a release build.

Goal: Practice writing and reasoning about a ProGuard/R8 keep rule for a realistic scenario.

โ†’ Solution

Challenge 2: Encrypted Token Storage

Write a TokenStorage class wrapping EncryptedSharedPreferences (using MasterKey with AES256_GCM), with functions saveToken(token: String) and getToken(): String? for storing/retrieving an auth token securely.

Goal: Practice the EncryptedSharedPreferences setup for a genuinely sensitive value.

โ†’ Solution

Challenge 3: Biometric Gate for a Sensitive Screen

Write a function showBiometricPrompt(activity: FragmentActivity, onSuccess: () -> Unit) using BiometricPrompt to gate access, calling onSuccess() only on onAuthenticationSucceeded. Sketch (in a comment) how you'd use it in a composable to conditionally show an "Account Details" screen only after successful authentication.

Goal: Practice wiring BiometricPrompt behind a callback usable from the rest of the app.

โ†’ Solution

๐Ÿ’ก None of This Replaces Server-Side Security

Every technique in this chapter protects the client โ€” the app installed on someone's device. It's not a substitute for anything covered in the security courses: proper authentication, authorization, input validation, and parameterized queries still have to be correct on the server, regardless of how well-defended the Android client is. A perfectly pinned, encrypted, obfuscated app talking to a server with a SQL injection vulnerability is still a broken system.

๐ŸŽฏ What's Next

Next chapter: Performance & Optimisation โ€” profiling, memory leaks, ANR prevention, and baseline profiles.