Mobile App Security: Complete Developer Guide
Mobile app security is the practice of protecting mobile applications from threats like data breaches, reverse engineering, and unauthorized access — using secure storage (Keychain/Keystore), certificate pinning, code obfuscation, runtime protection, and encrypted API communication.
What You’ll Learn
You’ll understand the OWASP Mobile Top 10 threats, implement secure storage with iOS Keychain and Android Keystore, configure certificate pinning, obfuscate code with ProGuard/R8, detect rooted/jailbroken devices, and build secure API communication with encrypted data.
Why Mobile Security Matters
Mobile apps handle sensitive data — personal information, payment details, private messages. Durga Antivirus Pro scans for malware on Android devices. Doda Browser stores passwords and browsing data. A security breach in either app would compromise millions of users. Mobile security isn’t optional — it’s a fundamental responsibility.
Mobile Security Learning Path
flowchart LR
A[Mobile Development Overview] --> B[Android Development]
A --> C[iOS Development]
B --> D[Mobile Security]
C --> D
D --> E[Push Notifications]
D:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
OWASP Mobile Top 10 Overview
The OWASP Mobile Top 10 identifies the most critical security risks:
| Rank | Risk | Example |
|---|---|---|
| M1 | Improper Credential Usage | Hardcoded API keys |
| M2 | Inadequate Supply Chain Security | Compromised third-party library |
| M3 | Insecure Authentication/Authorization | Weak password policy |
| M4 | Insufficient Input/Output Validation | SQL injection via search field |
| M5 | Insecure Communication | No certificate pinning |
| M6 | Inadequate Privacy Controls | Collecting location without consent |
| M7 | Insufficient Binary Protections | Unobfuscated code easily reverse-engineered |
| M8 | Security Misconfiguration | Debug mode enabled in release |
| M9 | Insecure Data Storage | Storing passwords in SharedPreferences |
| M10 | Insufficient Cryptography | Using MD5 for password hashing |
We’ll focus on the most actionable: secure storage, certificate pinning, obfuscation, and runtime protection.
Secure Storage: Keychain (iOS) and Keystore (Android)
Never store sensitive data in plain text files, SharedPreferences, or UserDefaults.
iOS Keychain
import Security
struct KeychainManager {
static func save(key: String, data: Data) -> OSStatus {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
// Delete any existing item first
SecItemDelete(query as CFDictionary)
// Add the new item
return SecItemAdd(query as CFDictionary, nil)
}
static func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else { return nil }
return item as? Data
}
static func delete(key: String) -> OSStatus {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
return SecItemDelete(query as CFDictionary)
}
}
// Usage
let token = "user_auth_token_123"
KeychainManager.save(key: "auth_token", data: Data(token.utf8))
if let loaded = KeychainManager.load(key: "auth_token") {
print(String(data: loaded, encoding: .utf8)!) // "user_auth_token_123"
}Android Keystore
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class SecureStorage(context: Context) {
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private val prefs = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
fun saveData(key: String, data: String) {
val secretKey = getOrCreateKey(key)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
// Store IV + encrypted data
prefs.edit().putString(key, android.util.Base64.encodeToString(iv + encrypted,
android.util.Base64.NO_WRAP)).apply()
}
fun getData(key: String): String? {
val encryptedBase64 = prefs.getString(key, null) ?: return null
val encrypted = android.util.Base64.decode(encryptedBase64, android.util.Base64.NO_WRAP)
val secretKey = getOrCreateKey(key)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, encrypted.copyOfRange(0, 12)))
return String(cipher.doFinal(encrypted.copyOfRange(12, encrypted.size)))
}
private fun getOrCreateKey(alias: String): SecretKey {
if (keyStore.containsAlias(alias)) return keyStore.getKey(alias, null) as SecretKey
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()
)
return keyGenerator.generateKey()
}
}Certificate Pinning
Certificate pinning ensures your app only accepts a specific certificate or public key, preventing man-in-the-middle (MITM) attacks even if a CA is compromised.
iOS (URLSession)
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// Load your pinned certificate
guard let pinnedCertData = NSDataAsset(name: "api.dodatech.com")?.data else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
}
}Android (OkHttp)
val certificatePinner = CertificatePinner.Builder()
.add("api.dodatech.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()Code Obfuscation (ProGuard/R8)
Obfuscation renames classes, methods, and fields to short meaningless names, making reverse engineering harder.
Android (R8 / ProGuard)
# proguard-rules.pro
-keep class com.dodatech.** { *; } # Keep our models
-keepattributes Signature
-keepattributes *Annotation*
# Keep Gson serialization classes
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Remove logging in release
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
public static int i(...);
}// build.gradle.kts (app level)
android {
buildTypes {
release {
isMinifyEnabled = true // Enable R8
isShrinkResources = true // Remove unused resources
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}iOS (Swift Compiler Optimizations)
// Build Settings → Optimization Level → Release: Optimize for Speed [-O]
// Strip Swift Symbols: YES
// Deployment Postprocessing: YESiOS doesn’t have ProGuard, but the compiler strips debug symbols and you can use tools like lldb protection.
Root/Jailbreak Detection
Android Root Detection
fun isDeviceRooted(): Boolean {
// Check for known root binaries
val rootPaths = listOf(
"/system/app/Superuser.apk",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
for (path in rootPaths) {
if (File(path).exists()) return true
}
// Check build tags
val buildTags = android.os.Build.TAGS ?: ""
if (buildTags.contains("test-keys")) return true
return false
}iOS Jailbreak Detection
func isJailbroken() -> Bool {
let cydiaPath = FileManager.default.fileExists(atPath: "/Applications/Cydia.app")
let aptPath = FileManager.default.fileExists(atPath: "/private/var/lib/apt/")
// Check if we can write outside app sandbox
let sandboxPath = "/private/" + UUID().uuidString
let canWriteOutsideSandbox = (try? "test".write(toFile: sandboxPath, atomically: true, encoding: .utf8)) != nil
try? FileManager.default.removeItem(atPath: sandboxPath)
return cydiaPath || aptPath || canWriteOutsideSandbox
}Important: Root/jailbreak detection is an arms race — determined users will bypass it. Use it to raise the bar, not as absolute protection.
Secure API Communication
flowchart TD
MobileApp[Mobile App] -->|TLS 1.3 + Certificate Pinning| API[API Server]
MobileApp -->|Encrypted Payload| API
API -->|Encrypted Response| MobileApp
MobileApp -->|"OAuth 2.0 / JWT"| Auth[Auth Server]
Auth -->|Access Token| MobileApp
MobileApp -->|Token in Header| API
// Always use HTTPS — never HTTP
// Never log auth tokens
// Use short-lived tokens (15-60 min) + refresh tokens
// Validate server certificate (see certificate pinning above)
// Example: Secure API client
class SecureApiClient {
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${getToken()}")
.addHeader("X-Request-ID", UUID.randomUUID().toString())
.build()
chain.proceed(request)
}
.certificatePinner(certificatePinner)
.build()
}Common Mobile Security Errors
1. Storing Secrets in Plain Text
SharedPreferences, UserDefaults, or plain files are readable on rooted/jailbroken devices. Always use the platform’s secure storage (Keychain/Keystore).
2. Hardcoding API Keys in Source Code
API keys in source code can be extracted from decompiled APKs/IPAs. Use build config fields, environment variables, or a backend proxy.
3. Disabling SSL/TLS Validation
// 🚨 NEVER do this in production
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(p0: Array<X509Certificate?>, p1: String?) {}
override fun checkServerTrusted(p0: Array<X509Certificate?>, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})This bypasses all certificate validation. Anyone can MITM your app.
4. Insufficient Input Validation
SQL injection, XSS, and command injection affect mobile apps too. Validate and sanitize all input on both client and server.
5. Logging Sensitive Data
// 🚨 Token visible in logcat
Log.d("Auth", "Token: $token")
// ✅ Log metadata only
Log.d("Auth", "Token obtained: ${token.take(4)}...${token.takeLast(4)}")6. Ignoring Third-Party Library Vulnerabilities
Use npm audit, gradle dependencies, or Snyk to scan for vulnerabilities. A compromised dependency = compromised app.
7. Weak Encryption Algorithms
// 🚨 MD5 is not secure for passwords
// ✅ Use bcrypt, scrypt, or Argon2 for password hashing
// ✅ Use AES-256-GCM for encryption
// ❌ Avoid: MD5, SHA1, DES, RC4, ECB modePractice Questions
1. What’s the difference between iOS Keychain and Android Keystore?
Both provide hardware-backed secure storage. Keychain stores key-value data with access policies. Keystore stores cryptographic keys and performs encryption operations within secure hardware.
2. Why is certificate pinning important?
It prevents MITM attacks even if a Certificate Authority is compromised. Without pinning, an attacker with a forged certificate can intercept encrypted traffic.
3. What does ProGuard/R8 do?
ProGuard/R8 optimizes, shrinks, and obfuscates Android bytecode — renaming classes/methods to short names, removing unused code, and making reverse engineering harder.
4. How do you detect a rooted device in Android?
Check for known root binaries (su), test-keys build tags, and attempt to write outside the app sandbox. Combine multiple checks for better accuracy.
5. Challenge: Audit an app’s security.
Given a mobile app that stores a JWT token in SharedPreferences, communicates over HTTP with no certificate validation, and logs API responses to logcat — write a security improvement plan addressing each vulnerability.
Answer:
- Move JWT to EncryptedSharedPreferences or Keystore
- Switch all API calls to HTTPS with certificate pinning
- Remove all logging of sensitive data in release builds
- Add ProGuard/R8 obfuscation
- Add root/jailbreak detection
- Implement OWASP input validation
FAQ
Try It Yourself
Audit a mobile app for security vulnerabilities:
- Install your app on a device
- Proxy traffic through Burp Suite or mitmproxy (does certificate pinning work?)
- Check backup files for plaintext secrets
- Decompile the APK with
apktoolorjadx— is the code obfuscated? - Review
AndroidManifest.xmlforallowBackup=trueor debug mode - Fix each vulnerability found
What’s Next
Mobile security is a continuous process, not a one-time task. Start with the OWASP Mobile Top 10, implement secure storage and certificate pinning first, and add layers (obfuscation, runtime protection) as your security requirements grow.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro