Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Cybersecurity

Mobile App Security Best Practices for 2026

Certificate pinning, secure storage, biometric authentication, code obfuscation, and API hardening — the defenses every mobile app needs against real-world attacks.

🔒 Mobile Development February 18, 2026 14 min read

Mobile apps handle sensitive data — payment credentials, health records, personal messages, location history. Yet many apps ship with security gaps that a determined attacker can exploit in minutes using freely available tools. This guide covers the OWASP Mobile Top 10 defenses, practical implementation patterns for both iOS and Android, and the security mistakes we see most often during code reviews at Pillai Infotech.

📋 Table of Contents

OWASP Mobile Top 10 (2024)

The OWASP Mobile Top 10 is the industry-standard list of mobile security risks. Here's the current list with the defenses that matter most:

# Risk Primary Defense
M1Improper Credential UsageKeychain/Keystore, never hardcode secrets
M2Inadequate Supply Chain SecurityDependency scanning, lock files, verified sources
M3Insecure AuthenticationServer-side validation, biometric + token
M4Insufficient Input/Output ValidationValidate all inputs, encode outputs
M5Insecure CommunicationTLS 1.3, certificate pinning
M6Inadequate Privacy ControlsMinimize data collection, encrypt at rest
M7Insufficient Binary ProtectionsObfuscation, tamper detection
M8Security MisconfigurationDisable debug, remove test credentials
M9Insecure Data StorageEncrypted storage, no sensitive data in logs
M10Insufficient CryptographyUse platform crypto APIs, AES-256-GCM

Secure Data Storage

The most common mistake we see: storing sensitive data in plaintext using SharedPreferences (Android) or UserDefaults (iOS). Both are trivially readable on rooted/jailbroken devices. Use the platform's secure storage APIs instead.

Android: EncryptedSharedPreferences + Keystore

// Android — secure token storage
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class SecureTokenStore(context: Context) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val prefs = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )

    fun saveToken(token: String) {
        prefs.edit().putString("auth_token", token).apply()
    }

    fun getToken(): String? = prefs.getString("auth_token", null)

    fun clearToken() {
        prefs.edit().remove("auth_token").apply()
    }
}

iOS: Keychain Services

// iOS — Keychain wrapper for secure storage
import Security

struct KeychainHelper {
    static func save(key: String, data: Data) throws {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        ]

        SecItemDelete(query as CFDictionary)  // Remove existing
        let status = SecItemAdd(query as CFDictionary, nil)

        guard status == errSecSuccess else {
            throw KeychainError.saveFailed(status)
        }
    }

    static func load(key: String) throws -> Data? {
        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 else { return nil }
        return result as? Data
    }
}
What NOT to store on-device: API keys for third-party services (move to your backend), encryption keys for server-side data, user passwords in any form. The device is an untrusted environment — store the minimum needed for the app to function between sessions.

Network Security and Certificate Pinning

TLS alone isn't enough. A man-in-the-middle attack using a rogue CA certificate (trivially installable on a rooted device) can intercept all HTTPS traffic. Certificate pinning ensures your app only trusts your server's certificate.

Android: Network Security Config

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.yourapp.com</domain>
        <pin-set expiration="2027-01-01">
            <!-- Pin the intermediate CA certificate (not leaf) -->
            <pin digest="SHA-256">YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</pin>
            <!-- Backup pin (different CA) for rotation -->
            <pin digest="SHA-256">sRHdihwgkaib1P1gN7akqREHQMk5nOSeltDep3DcZhA=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

iOS: URLSession Pinning

// iOS — Certificate pinning with URLSessionDelegate
class PinnedSessionDelegate: NSObject, URLSessionDelegate {
    private let pinnedHashes: Set<String> = [
        "YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=",
        "sRHdihwgkaib1P1gN7akqREHQMk5nOSeltDep3DcZhA="
    ]

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        guard let serverTrust = challenge.protectionSpace.serverTrust,
              let certificate = SecTrustCopyCertificateChain(serverTrust)?.first
        else {
            return (.cancelAuthenticationChallenge, nil)
        }

        let serverHash = sha256Hash(of: SecCertificateCopyPublicKey(certificate)!)

        if pinnedHashes.contains(serverHash) {
            return (.useCredential, URLCredential(trust: serverTrust))
        }

        return (.cancelAuthenticationChallenge, nil)
    }
}

Always pin the intermediate CA certificate, not the leaf. Leaf certificates rotate annually; intermediate CAs rotate much less frequently. Include a backup pin from a different CA so certificate rotation doesn't break your app.

Authentication and Biometrics

Modern mobile auth combines short-lived tokens with biometric verification for a balance of security and convenience.

Secure Token Architecture

Biometric Authentication

// Android — BiometricPrompt with Keystore-backed crypto
class BiometricAuthManager(private val activity: FragmentActivity) {

    fun authenticate(onSuccess: (BiometricPrompt.CryptoObject) -> Unit) {
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Verify your identity")
            .setSubtitle("Use fingerprint or face to continue")
            .setNegativeButtonText("Use PIN instead")
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()

        val biometricPrompt = BiometricPrompt(
            activity,
            ContextCompat.getMainExecutor(activity),
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    result.cryptoObject?.let { onSuccess(it) }
                }

                override fun onAuthenticationFailed() {
                    // Biometric didn't match — prompt stays open for retry
                }

                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    // Fatal error — fallback to PIN/password
                }
            }
        )

        // Use Keystore-backed cipher for actual cryptographic guarantee
        val cipher = getCipherForEncryption()  // AES/GCM with Keystore key
        biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
    }
}
Biometric + CryptoObject is essential: Without CryptoObject, biometric auth is just a UI gate — an attacker can bypass it by hooking the callback. With CryptoObject, the biometric verification is tied to a Keystore-backed cryptographic operation that can't be faked.

API Security

Your API is only as secure as the weakest client calling it. Assume every request could be crafted by an attacker, not just your app.

Essential API Defenses

// Request signing — HMAC per request
fun signRequest(
    method: String,
    path: String,
    body: String?,
    sessionKey: ByteArray
): String {
    val timestamp = System.currentTimeMillis() / 1000
    val nonce = UUID.randomUUID().toString()

    val payload = buildString {
        append("$method\n")
        append("$path\n")
        append("$timestamp\n")
        append("$nonce\n")
        append(body?.sha256() ?: "")
    }

    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(sessionKey, "HmacSHA256"))
    val signature = Base64.encodeToString(mac.doFinal(payload.toByteArray()), Base64.NO_WRAP)

    return "ts=$timestamp,nonce=$nonce,sig=$signature"
}

Code Protection and Obfuscation

Any app installed on a device can be reverse-engineered. The goal isn't to make it impossible — it's to make it expensive enough that attackers move to easier targets.

Protection Android iOS
Code obfuscationR8/ProGuard (built-in)Swift compiler strips symbols by default
String encryptionDexGuard or custom encryptionComputed properties, not string literals
Root/jailbreak detectionPlay Integrity API + custom checksApp Attest + file-system checks
Tamper detectionAPK signature verification at runtimeCode signing + App Attest
Debug detectionCheck isDebuggable, detect FridaCheck sysctl for debugger attachment
// Android — R8 ProGuard rules for security
// proguard-rules.pro

# Obfuscate everything by default
-dontoptimize
-keepattributes SourceFile,LineNumberTable  # For crash reports

# Keep only what's needed
-keep class com.yourapp.api.models.** { *; }  # API models (serialization)
-keep class * extends androidx.lifecycle.ViewModel  # ViewModels

# Encrypt strings containing API endpoints
# (Use DexGuard for production, or move to BuildConfig/server)

// Android — basic root detection
fun isDeviceRooted(): Boolean {
    val rootIndicators = listOf(
        "/system/app/Superuser.apk",
        "/system/xbin/su",
        "/system/bin/su",
        "/sbin/su",
        "/data/local/xbin/su"
    )
    return rootIndicators.any { File(it).exists() } ||
           Build.TAGS?.contains("test-keys") == true
}

Runtime Protection

Beyond build-time protections, your app should defend itself at runtime:

// Android — screenshot prevention + background blur
class SecureActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Prevent screenshots and screen recording
        window.setFlags(
            WindowManager.LayoutParams.FLAG_SECURE,
            WindowManager.LayoutParams.FLAG_SECURE
        )
    }
}

// iOS — background snapshot protection
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    private var blurView: UIVisualEffectView?

    func sceneWillResignActive(_ scene: UIScene) {
        // Add blur when entering background
        let blur = UIBlurEffect(style: .light)
        blurView = UIVisualEffectView(effect: blur)
        blurView?.frame = window?.bounds ?? .zero
        window?.addSubview(blurView!)
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        blurView?.removeFromSuperview()
        blurView = nil
    }
}

Security Checklist

Use this checklist before every release. At Pillai Infotech, we run through this during every mobile app security review:

Pre-Release Security Checklist

  • ☐ No hardcoded API keys, secrets, or credentials in source code
  • ☐ Sensitive data stored in Keychain (iOS) / EncryptedSharedPreferences (Android)
  • ☐ TLS 1.2+ enforced, cleartext traffic disabled
  • ☐ Certificate pinning implemented with backup pins
  • ☐ Auth tokens are short-lived with refresh rotation
  • ☐ Biometric auth uses CryptoObject (not just UI gate)
  • ☐ All inputs validated server-side (not just client-side)
  • ☐ R8/ProGuard obfuscation enabled (Android release builds)
  • ☐ Debug logging disabled in production builds
  • ☐ No sensitive data in log output (grep for tokens, passwords, PII)
  • ☐ Screenshot prevention on sensitive screens
  • ☐ Background snapshot blurred for sensitive views
  • ☐ Root/jailbreak detection implemented (at minimum, warning)
  • ☐ Dependencies scanned for known vulnerabilities
  • ☐ App Attest (iOS) / Play Integrity (Android) for API verification
  • ☐ Deep links validated — no open redirect vulnerabilities

Frequently Asked Questions

Should I block rooted/jailbroken devices?

Depends on your app. Banking and healthcare apps should at minimum warn users and disable sensitive features. For general apps, detection is more useful for adjusting trust levels (e.g., requiring re-authentication) than outright blocking, which frustrates legitimate power users.

Is certificate pinning worth the operational complexity?

Yes, for apps handling financial data, healthcare records, or authentication flows. The key is pinning intermediate CA certificates (not leaf) and always having backup pins. Without pinning, any compromised CA can intercept your traffic.

How do I handle API keys for third-party services?

Never embed them in the app binary — they will be extracted. Proxy third-party API calls through your own backend server. The mobile app authenticates with your server; your server makes the third-party call with the API key stored server-side.

What about React Native and Flutter security?

Cross-platform frameworks face the same security challenges plus additional ones — JavaScript bundles in React Native are easier to reverse-engineer than compiled code. Apply all the same defenses, plus Hermes bytecode compilation (RN) and code obfuscation (Flutter).

How often should I do security audits?

At minimum: before initial launch, after major features, and annually. For apps handling sensitive data, quarterly penetration testing is recommended. Automated dependency scanning should run on every CI build.

What are the most common security mistakes you see?

Logging sensitive data in production, storing tokens in plaintext SharedPreferences/UserDefaults, not validating deep link parameters, using biometrics as a UI-only gate without CryptoObject, and trusting client-side validation without server-side checks.

🔒

Pillai Infotech LLP

We build secure mobile apps and conduct security reviews for iOS and Android applications. Get a security assessment for your mobile app.

Related Articles

iOS App Development with Swift: What's New in 2026 → Android App Development with Kotlin: 2026 Best Practices → React Native vs Native Development: The 2026 Decision Guide →