Mobile App Security: Keys, Obfuscation, and Root Detection
Protect your mobile apps with proven security techniques including secure key storage, code obfuscation with ProGuard/R8, and root/jailbreak detection.
Published on • August 13, 2026
AI Assistant

Mobile applications handle an extraordinary amount of sensitive data—from banking credentials and health records to personal messages and location history. A single vulnerability can expose millions of users to identity theft, financial fraud, or privacy violations. Yet many development teams treat security as an afterthought, bolting on protections only after a breach has already occurred.
This guide covers three foundational pillars of mobile app security: secure key storage, code obfuscation, and root/jailbreak detection. These aren’t theoretical concepts—they’re practical, implementable techniques that significantly raise the bar for attackers. We’ll walk through real code for both Android and iOS, so you can start hardening your apps today.
Prerequisites
Before diving in, make sure you have the following:
- Android Studio (latest stable) or Xcode (latest stable)
- Basic familiarity with Android’s AndroidX libraries and iOS’s Security framework
- An existing mobile project (or willingness to create a new one)
- Understanding of asymmetric/symmetric cryptography concepts
- For Android:
minSdkVersion 23or higher recommended for modern KeyStore APIs - For iOS: iOS 15+ for the latest Keychain and DeviceCheck APIs
Secure Storage: Protecting Secrets at Rest
Hardcoding API keys, tokens, or encryption secrets in source code is one of the most common—and most dangerous—mobile security anti-patterns. Decompiling an APK or reversing an IPA is trivial with the right tools. Secure storage ensures that sensitive values are encrypted at rest and tied to the device’s hardware security module.
Android: EncryptedSharedPreferences and Android Keystore
The Android Keystore system provides cryptographic key storage that keeps keys in a hardware-backed container (on devices with TEE or StrongBox). Combined with EncryptedSharedPreferences, you get a simple API for storing sensitive data securely.
First, add the dependency:
implementation("androidx.security:security-crypto:1.1.0-alpha06")
Then initialize encrypted preferences:
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
fun createEncryptedPrefs(context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
Usage is straightforward:
val prefs = createEncryptedPrefs(context)
prefs.edit().putString("auth_token", "eyJhbGciOiJIUzI1NiIs...").apply()
val token = prefs.getString("auth_token", null)
For raw cryptographic keys (not shared preferences), use the Android Keystore directly:
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
fun generateSecretKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
"app_secret_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
)
return keyGenerator.generateKey()
}
fun loadSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return keyStore.getEntry("app_secret_key", null) as KeyStore.SecretKeyEntry
.secretKey
}
iOS: Keychain Services
Apple’s Keychain is the gold standard for secure storage on iOS. Data stored in the Keychain is encrypted with hardware keys and persists across app reinstalls.
import Security
class KeychainManager {
static func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
static func get(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
For biometric-protected access:
static func saveWithBiometric(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.biometryCurrentSet],
nil
)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessControl as String: accessControl
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
Code Obfuscation with ProGuard and R8
Obfuscation doesn’t prevent reverse engineering entirely, but it dramatically increases the effort required. Renaming classes, methods, and fields to meaningless strings makes decompiled code nearly impossible to understand.
Configuring R8 (Android)
R8 is enabled by default in modern Android Gradle builds. Add ProGuard rules to your build.gradle:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'
), 'proguard-rules.pro'
}
}
}
Example proguard-rules.pro:
# Keep your data models that use reflection
-keep class com.yourapp.models.** { *; }
# Keep Retrofit interfaces
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# Obfuscate everything else
-repackageclasses ''
-allowaccessmodification
For additional obfuscation layers, consider adding string encryption and control flow flattening through commercial tools like DexGuard or open-source alternatives.
Adding String Encryption
Hardcoded strings (API endpoints, encryption keys) are easily readable in decompiled code. Encrypt them:
object StringEncryptor {
private val key = byteArrayOf(
0x3B, 0x0A, 0x1F, 0x2E, 0x4D, 0x5C, 0x6B, 0x7A,
(0x89).toByte(), (0x98).toByte(), 0x07, 0x16, 0x25, 0x34, 0x43, 0x52
)
fun encrypt(input: String): ByteArray {
val bytes = input.toByteArray()
return ByteArray(bytes.size) { i -> (bytes[i].toInt() xor key[i % key.size]).toByte() }
}
fun decrypt(input: ByteArray): String {
val bytes = ByteArray(input.size) { i -> (input[i].toInt() xor key[i % key.size]).toByte() }
return String(bytes)
}
}
Use it at runtime:
val encrypted = StringEncryptor.encrypt("https://api.yourapp.com/v2/")
val endpoint = StringEncryptor.decrypt(encrypted)
Root and Jailbreak Detection
Rooted Android devices and jailbroken iOS devices bypass security sandboxing, giving attackers access to private data, the ability to hook into running processes, and the ability to bypass SSL pinning.
Android Root Detection
Implement multiple detection methods—relying on a single check is easy to bypass:
import java.io.File
object RootDetector {
fun isRooted(): Boolean {
return checkRootBinaries() || checkRootApps() || checkSuExists() || checkRWPaths()
}
private fun checkRootBinaries(): Boolean {
val paths = arrayOf(
"/system/xbin/su",
"/system/bin/su",
"/sbin/su",
"/data/local/xbin/su",
"/data/local/bin/su"
)
return paths.any { File(it).exists() }
}
private fun checkRootApps(): Boolean {
val paths = arrayOf(
"/data/app/com.noshufou.android.su",
"/data/app/eu.chainfire.supersu",
"/data/app/com.koushikdutta.superuser",
"/data/app/com.thirdparty.superuser"
)
return paths.any { File(it).exists() }
}
private fun checkSuExists(): Boolean {
return try {
Runtime.getRuntime().exec(arrayOf("which", "su")).waitFor() == 0
} catch (e: Exception) {
false
}
}
private fun checkRWPaths(): Boolean {
return try {
val process = Runtime.getRuntime().exec(arrayOf("mount"))
val output = process.inputStream.bufferedReader().readText()
process.waitFor()
output.contains("/system") && !output.contains("ro,")
} catch (e: Exception) {
false
}
}
}
iOS Jailbreak Detection
import UIKit
class JailbreakDetector {
static func isJailbroken() -> Bool {
guard !isSimulator() else { return false }
let suspiciousFiles = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/",
"/usr/bin/ssh"
]
return suspiciousFiles.contains { FileManager.default.fileExists(atPath: $0) }
|| canOpenCydiaApp()
|| checksExistenceOfSuspiciousFiles()
}
private static func isSimulator() -> Bool {
return TARGET_OS_SIMULATOR != 0
}
private static func canOpenCydiaApp() -> Bool {
return UIApplication.shared.canOpenURL(URL(string: "cydia://")!)
}
private static func checksExistenceOfSuspiciousFiles() -> Bool {
let path = Bundle.main.infoDictionary?["TEST"] as? String
return false
}
}
Response Strategy
Detection is useless without a proper response. Consider a graduated approach:
fun handleSecurityThreat(context: Context) {
when {
RootDetector.isRooted() -> {
Log.w("Security", "Rooted device detected")
showSecurityWarning()
disableBiometricAuth()
enforceStrongPin()
}
}
}
SSL/TLS Certificate Pinning
SSL pinning prevents man-in-the-middle attacks even when an attacker controls a trusted CA on the device. Implement it using OkHttp’s CertificatePinner:
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
fun createSecureClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add("api.yourapp.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
For iOS, use URLSessionDelegate:
class PinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard let serverTrust = challenge.protectionSpace.serverTrust,
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let remoteCertData = SecCertificateCopyData(certificate) as Data
let localCertData = NSData(contentsOfFile: Bundle.main.path(forResource: "cert", ofType: "der")!)! as Data
if remoteCertData == localCertData {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
Security Checklist
Before shipping your next release, verify these protections are in place:
- Secure storage: No API keys, tokens, or secrets in source code
- Encrypted preferences: Using
EncryptedSharedPreferences(Android) or Keychain (iOS) - ProGuard/R8 enabled:
minifyEnabled trueandshrinkResources truefor release builds - String encryption: Sensitive strings obfuscated or encrypted
- Root detection: Multiple detection methods implemented
- Jailbreak detection: Device integrity checks on iOS
- SSL pinning: Certificate pinning on all API endpoints
- No logging in production: Debug logs disabled for release builds
- Input validation: All user inputs validated server-side
- Dependency audit: Third-party libraries scanned for known vulnerabilities
# Android dependency audit
./gradlew dependencyCheckAnalyze
# iOS dependency audit
pod audit
Conclusion and Next Steps
Mobile app security isn’t a one-time task—it’s an ongoing practice. The techniques covered here—secure key storage, code obfuscation, root detection, and SSL pinning—form a strong defense-in-depth strategy. But they’re just the foundation.
Your next steps should include:
- Penetration testing: Hire a security firm or use tools like MobSF to probe your app for vulnerabilities
- Runtime application self-protection (RASP): Consider commercial solutions that detect tampering, debugging, and hooking in real time
- Secure CI/CD: Integrate security scanning into your build pipeline so vulnerabilities are caught before deployment
- Certificate transparency: Monitor for unauthorized certificate issuance for your domains
- Compliance audits: Ensure your app meets GDPR, HIPAA, or PCI-DSS requirements as applicable
The cost of a breach—financial penalties, reputation damage, user churn—far exceeds the investment in proactive security. Start implementing these protections today. Your users are counting on you.