Skip to content
Mobile App Security: Complete Developer Guide

Mobile App Security: Complete Developer Guide

DodaTech Updated Jun 20, 2026 9 min read

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
  
Prerequisites: Basic knowledge of Android and/or iOS development. Understanding of REST APIs and HTTPS.

OWASP Mobile Top 10 Overview

The OWASP Mobile Top 10 identifies the most critical security risks:

RankRiskExample
M1Improper Credential UsageHardcoded API keys
M2Inadequate Supply Chain SecurityCompromised third-party library
M3Insecure Authentication/AuthorizationWeak password policy
M4Insufficient Input/Output ValidationSQL injection via search field
M5Insecure CommunicationNo certificate pinning
M6Inadequate Privacy ControlsCollecting location without consent
M7Insufficient Binary ProtectionsUnobfuscated code easily reverse-engineered
M8Security MisconfigurationDebug mode enabled in release
M9Insecure Data StorageStoring passwords in SharedPreferences
M10Insufficient CryptographyUsing 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: YES

iOS 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 mode

Practice 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:

  1. Move JWT to EncryptedSharedPreferences or Keystore
  2. Switch all API calls to HTTPS with certificate pinning
  3. Remove all logging of sensitive data in release builds
  4. Add ProGuard/R8 obfuscation
  5. Add root/jailbreak detection
  6. Implement OWASP input validation

FAQ

Do I need to implement all OWASP Mobile Top 10 controls?
Start with M9 (secure storage), M5 (secure communication), and M7 (binary protection). These have the highest impact for the least effort. Address the others based on your app’s risk profile.
Can certificate pinning cause app breakage?
Yes, when the server certificate rotates. Always pin the public key (not the certificate) so certificate renewal doesn’t break your app. Have a backup pin and a remote config toggle to disable pinning in emergencies.
Is biometric authentication (Face ID / fingerprint) secure for mobile apps?
Yes, when combined with the platform’s secure Keystore/Keychain. Use BiometricPrompt (Android) or LAContext (iOS) to gate access to stored secrets. Biometric data never leaves the device.
How do I handle API keys securely in mobile apps?
The best approach: don’t put them in the app at all. Use a backend proxy that holds the API key. Client authenticates with your backend, which forwards requests to the third-party API.

Try It Yourself

Audit a mobile app for security vulnerabilities:

  1. Install your app on a device
  2. Proxy traffic through Burp Suite or mitmproxy (does certificate pinning work?)
  3. Check backup files for plaintext secrets
  4. Decompile the APK with apktool or jadx — is the code obfuscated?
  5. Review AndroidManifest.xml for allowBackup=true or debug mode
  6. 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