Mobile App Security

Secure mobile applications against reverse engineering, data theft, network interception, and platform-specific vulnerabilities. Covers certificate pinning, secure storage, biometric authentication, code obfuscation, and the OWASP Mobile Top 10.

Mobile app security is the practice of protecting data, code, and communication across mobile platforms—iOS and Android—from threats that arise during development, deployment, and runtime. It matters when user credentials, session tokens, and sensitive business logic are exposed to attackers through insecure APIs, weak cryptography, untrusted networks, and vulnerable app binaries. This page details the actions engineers take to secure mobile apps, from code and configuration to runtime behavior and monitoring, with concrete tools, exact commands, and real failure modes.

Securing the App Binary

Mobile app binaries (APK for Android, IPA for iOS) are the first line of defense. They must resist reverse engineering, tampering, and dynamic analysis. The primary concerns are integrity, confidentiality, and anti-tampering.

Build-time Obfuscation and Integrity

Obfuscation reduces the visibility of code and assets. For Android, ProGuard and R8 are standard. A typical build.gradle configuration ensures method and class renaming, string encryption, and dead code elimination:

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

The proguard-rules.pro file includes:

-keep class com.example.app.** { *; }
-keepclassmembers class com.example.app.model.* {
    <fields>;
    <init>(...);
}
-keep class * implements com.example.app.security.Cryptographer {
    public <methods>;
}
-keepclasseswithmembers class * {
    @com.example.app.annotations.Encrypted public <fields>;
}

The R8 compiler, enabled by default in Android Studio, applies advanced optimizations and generates a mapping.txt file that maps obfuscated names back to original ones. This file is critical for crash reporting and debugging.

Runtime Integrity Checks

At runtime, the app validates its own integrity. A common pattern is verifying the app’s signature and checking for tampering via a SHA-256 hash of the APK or IPA.

In Android, the SignatureVerifier class reads the APK's META-INF/ directory and compares the CERT.RSA and MANIFEST.MF files against the expected signature. A failure results in:

try {
    PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
    Signature[] signatures = packageInfo.signatures;
    for (Signature signature : signatures) {
        byte[] hash = MessageDigest.getInstance("SHA-256").digest(signature.toByteArray());
        if (!Arrays.equals(hash, expectedHash)) {
            throw new SecurityException("App signature mismatch");
        }
    }
} catch (NoSuchAlgorithmException e) {
    throw new SecurityException("Failed to verify signature", e);
}

For iOS, code signing is enforced via codesign and security tools. A typical check in Swift:

func verifyAppSignature() throws {
    let task = Process()
    task.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
    task.arguments = ["-v", "--verify", "--strict", Bundle.main.bundlePath]
    
    let output = Pipe()
    task.standardOutput = output
    try task.run()
    let result = try output.fileHandle.readToEnd()
    let resultString = String(data: result!, encoding: .utf8)!
    
    if !resultString.contains("succeeded") {
        throw SecurityError.tamperedApp
    }
}

A failure is logged with os_log and triggers a fallback mechanism: the app downloads a fresh binary from a secure server.

Securing Data at Rest and in Transit

Data security is layered across storage and network. Each layer must be independently guarded.

Local Storage Encryption

Android apps store data in SharedPreferences, SQLite, and Room databases. To protect data at rest, developers use SQLCipher with Android Keystore and KeyChain.

For a Room database, the RoomDatabase is initialized with encryption:

val database = Room.databaseBuilder(context, AppDatabase::class.java, "app-db")
    .addMigrations(MIGRATION_1_2)
    .openHelperFactory(
        SupportSQLiteOpenHelper.Factory { db ->
            SQLCipherOpenHelper(
                db,
                "my-secret-key-here",
                10000
            )
        }
    )
    .build()

The SQLCipherOpenHelper uses a key derived from the Android Keystore. The key is protected by hardware-backed KeyStoreEntry:

KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder("app-encryption-key", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .build());

SecretKey key = keyGenerator.generateKey();

A common failure: SQLCipher throws SQLCipherException with message:

Error: 14, SQL error: Failed to open database: database is encrypted but key is not set

This occurs when the key is not loaded from the Keystore before SQLCipher opens the database.

API Communication Security

Mobile apps communicate with backend services over HTTP. Security begins with HTTPS and extends to request signing, token validation, and payload integrity.

An app sends a request using OkHttp with certificate pinning:

val client = OkHttpClient.Builder()
    .addNetworkInterceptor(PinCertificateInterceptor())
    .build()

class PinCertificateInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = chain.proceed(request)

        val certificatePinner = CertificatePinner.Builder()
            .add("api.example.com", "sha256/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
            .add("api.example.com", "sha256/0987654321fedcba0987654321fedcba0987654321fedcba0987654321fedcba")
            .build()

        val certPinner = response.body?.let { it.source().readByteArray() }
        val hostname = request.url.host

        val certificate = response.handshake().peerCertificates[0]
        val certificateHash = MessageDigest.getInstance("SHA-256").digest(certificate.encoded)

        val expectedHash = certificatePinner.pin(hostname)
        if (!Arrays.equals(certificateHash, expectedHash)) {
            throw CertificatePinningException("Certificate mismatch for $hostname")
        }

        return response
    }
}

The CertificatePinner is configured with two SHA-256 hashes of public keys. If a server returns a certificate with a different hash, the app logs:

CertificatePinningException: Certificate mismatch for api.example.com

The app then falls back to a pre-configured list of trusted certificates, stored in the app bundle.

Token-Based Authentication and Session Management

Mobile apps use JWTs for authentication. The app receives a JWT from the server, stores it in SharedPreferences or Keychain, and uses it in subsequent requests.

A typical JWT header and payload:

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516239022,
  "scope": ["read", "write"],
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYXV0aG9yaXR5IjpbInVzZXIiLCJtYW5hZ2VyIl0sImlhdCI6MTUxNjIzOTAyMiwic3RhdHVzIjoiU3RhdHVzIn0.9ZQc4sN1JXtY8FkKv3b8s3r5nB6c4v1a9k1e3n4b5a6c7d8e9f0g1h2i3j4k5l6m7n8o9p0q1r2s3t4u5v6w7x8y9z"
}

The app validates the JWT on each request. A failure occurs if the exp claim is in the past:

JWTValidationException: Token expired at 1516239022, current time is 1516239100

The app then refreshes the token using the refresh_token field. The refresh request includes a Authorization: Bearer header and the refresh_token in the body.

A common edge case: the app runs on a slow network and sends multiple requests while the token expires. The server returns a 401 Unauthorized response with a WWW-Authenticate header:

WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"

The client parses this and triggers a token refresh before retrying the failed request.

Securing the Runtime Environment

The mobile runtime is the final barrier against attacks like dynamic analysis, hooking, and debugging.

Anti-Tampering and Anti-Debugging

The app checks for common tampering indicators: rooted or jailbroken devices, debuggers, and memory patching.

On Android, the RootChecker class verifies root access:

public static boolean isRooted() {
    return new File("/system/app/Superuser.apk").exists()
        || new File("/system/bin/su").exists()
        || new File("/data/local/bin/su").exists();
}

If the device is rooted, the app checks for a debuggable flag in AndroidManifest.xml:

<application
    android:debuggable="true"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

The app logs:

Device is rooted and debuggable; applying enhanced security measures

For iOS, the DYLD.dylib is used to detect debugging. A common check is:

func isDebuggerAttached() -> Bool {
    var info = kinfo_proc()
    var mib = [CTL_PROC, 0, 0, 0]
    var size = MemoryLayout<kinfo_proc>.size
    var result = sysctl(&mib, 4, &info, &size, nil, 0)

    return result == 0 && info.kp_proc.p_flag & (1 << 12) != 0
}

When a debugger is attached, the app disables sensitive operations, such as key derivation or cryptographic operations, and logs:

Debugger detected: disabling key derivation

Secure Key Management

Keys used for encryption, signing, and authentication must be protected. The app uses the platform’s key store and applies key rotation.

On Android, keys are stored in AndroidKeyStore and are backed by hardware. A key is created with:

KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder("crypto-key", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(300)
    .setIsStrongBoxBacked(true)
    .build());

SecretKey key = keyGenerator.generateKey();

The isStrongBoxBacked(true) ensures the key is stored in a hardware security module (HSM), resistant to physical attacks.

A failure occurs when the app tries to access a key that is not available in the StrongBox:

KeyStoreException: Key not found in StrongBox

The app then falls back to software key storage and logs:

Falling back to software key storage

Monitoring and Incident Response

Security is not a one-time configuration. It requires continuous monitoring and response.

The app logs security events using structured JSON:

{
  "timestamp": "2024-04-05T12:34:56.789Z",
  "event": "session_ended",
  "session_id": "abc123",
  "user_id": "123456",
  "device": "iPhone14,3",
  "os_version": "17.3",
  "client_ip": "192.168.1.10",
  "security_level": "high",
  "details": {
    "method": "jwt",
    "token_validity": 3600,
    "refresh_token_used": true
  }
}

These logs are sent via HTTP POST to a central security service. A failure in logging results in:

HTTP 502 Bad Gateway: Failed to send security log to https://logs.example.com/security

The app caches logs locally and retries on startup.

A common edge case: the app detects a JWTValidationException and sends an event with priority: high. The monitoring system triggers an alert if three such events occur within 10 minutes.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.