Mobile App Security: Protecting Data on Untrusted Devices

Implement mobile security that protects user data, API keys, and business logic on devices you do not control. Covers secure storage, certificate pinning, code obfuscation, biometric authentication, runtime integrity checks, and the threat model that makes mobile fundamentally different from server security.

Mobile apps run on untrusted devices: users own their phones, laptops, and tablets, and they may install apps from unknown sources, connect to public Wi-Fi, or sync with multiple accounts. Security is not an afterthought but a continuous process, from initial app installation to data at rest and in transit. This reference page details how to protect app data on such devices, focusing on what engineers must do to harden mobile apps against common attack vectors.

Securing Data in Transit

Data transmitted between app and backend must resist eavesdropping, tampering, and replay attacks. The foundation is TLS, but mobile apps must go beyond default configurations.

Enforce Strong TLS Configuration

Mobile apps should use TLS 1.2 or higher with perfect forward secrecy (PFS) and certificate pinning. The app must validate server certificates against a known public key or hash.

{
  "tls": {
    "minimum_version": "1.2",
    "supported_ciphers": [
      "TLS_AES_256_GCM_SHA512",
      "TLS_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA512",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
    ],
    "certificate_pinning": {
      "public_keys": [
        "sha256:47:18:5c:3d:98:fb:12:3a:8e:01:45:8e:2a:9d:7e:96:21:16:05:8b:0c:0e:55:11:1b:1d:9e:4c:50:9c:85:4b",
        "sha256:32:5a:12:7e:9c:1d:8a:4b:17:2c:54:91:4c:10:2d:1e:74:33:5b:16:44:91:14:8d:6f:28:02:12:13:7b:8c:31"
      ],
      "include_subdomains": true,
      "fail_on_missing_pinning": true
    }
  }
}

When the server certificate does not match the pinned public keys, the app must either fail the connection or fall back to a trusted root CA with a warning. The app logs TLS_PINNING_MISMATCH and TLS_PINNING_FALLBACK events.

Prevent Certificate Pinning Bypass via Certificate Installation

Users may install custom root certificates (e.g., via corporate PKI or enterprise MDM) that trust a server’s certificate. The app must detect and validate that the trusted certificate chain includes at least one of the pinned certificates.

// Swift example: verify certificate chain includes a pinned cert
func validateCertificateChain(_ chain: [SecCertificate], pinnedPublicKeys: [Data]) -> Bool {
    for cert in chain {
        let certData = SecCertificateCopyExternalRepresentation(cert, nil) as Data?
        guard let certData = certData else { continue }
        let certHash = SHA256.hash(data: certData)

        if pinnedPublicKeys.contains(where: { $0 == certHash }) {
            return true
        }
    }
    return false
}

If the chain contains a pinned certificate but not one of the expected public keys, the app logs CERTIFICATE_CHAIN_MISSING_PINNED_CERT and triggers a background check.

Securing Data at Rest

Data stored on the device—locally in SQLite, SharedPreferences, or files—must be protected against extraction, tampering, and theft.

Encrypt Application Data with File-Level Encryption

Use the platform’s native encryption APIs to encrypt the app’s data directory. On iOS, use NSFileProtectionComplete with NSFileProtectionCompleteUntilFirstUserAuthentication. On Android, use EncryptedSharedPreferences with EncryptedFile and MasterKeys.

// Android: configure encrypted shared preferences
MasterKey masterKey = new MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .setKeyGenerator(MasterKey.KeyGenerator.SHA256)
    .build();

EncryptedSharedPreferences.Builder builder = new EncryptedSharedPreferences.Builder(
    context,
    "app_prefs",
    masterKey,
    EncryptedSharedPreferences.AES_GCM_BLOCK_MODE,
    true
);

SharedPreferences prefs = builder.build();
prefs.edit().putString("user_token", "abc123").apply();

The app must encrypt all database files, including SQLiteOpenHelper databases and Room persistent storage.

// Kotlin: encrypt Room database
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
    .addCallback(object : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            db.execSQL("PRAGMA key = 'secret_passphrase_123'")
            db.execSQL("PRAGMA cipher_page_size = 4096")
            db.execSQL("PRAGMA kdf_iter = 4000")
            db.execSQL("PRAGMA cipher_use_hmac = 1")
            db.execSQL("PRAGMA cipher_hmac = 1")
        }
    })
    .build()

The database uses SQLite’s cipher extension with AES-256-GCM. The app must store the encryption key in the keystore and use a dynamic key derivation function (PBKDF2 or scrypt) to derive the database key from the master key.

Protect Sensitive Data with Keychain and Keystore

The app must store secrets—such as API tokens, user passwords, and encryption keys—in the platform’s secure key storage.

On iOS, use SecKeyCreateWithData and SecKeyCopyAttributes to store and retrieve keys.

// Swift: store and retrieve a user session token
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
let tokenData = token.data(using: .utf8)!

let query: [CFString: Any] = [
    kSecClass: kSecClassKey,
    kSecAttrKeyType: kSecAttrKeyTypeRSA,
    kSecAttrLabel: "UserSessionToken",
    kSecAttrApplicationTag: "com.example.app.session_token".data(using: .utf8),
    kSecValueData: tokenData
]

SecItemAdd(query as CFDictionary, nil)

let result = SecItemCopyMatching([
    kSecClass: kSecClassKey,
    kSecAttrLabel: "UserSessionToken"
] as CFDictionary, nil)

if let keyData = result as? Data, let tokenString = String(data: keyData, encoding: .utf8) {
    print("Retrieved token: \(tokenString)")
}

On Android, use KeyStore with AndroidKeyStore and KeyGenerator.

// Android: generate and retrieve a key for encrypting user data
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGenerator.init(new KeyGenParameterSpec.Builder("user_data_key", 
    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(300)
    .setKeySize(256)
    .build());

keyGenerator.generateKey();

SecretKey key = keyStore.getKey("user_data_key", null);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);

The app must check if the key is available and prompt the user to re-authenticate if the key is locked (e.g., after device reboot or sleep).

Securing App Execution

The app must protect its own code and execution environment against tampering, dynamic analysis, and debugging.

Detect and Respond to Runtime Tampering

The app must verify its integrity at startup and during execution. This includes checking the app binary, loaded libraries, and runtime state.

// C: verify app binary integrity using SHA-256
int verify_app_binary(const char* app_path, const char* expected_hash) {
    FILE* file = fopen(app_path, "rb");
    if (!file) return 0;

    unsigned char hash[32];
    SHA256_CTX ctx;
    SHA256_Init(&ctx);

    unsigned char buffer[4096];
    size_t len;
    while ((len = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        SHA256_Update(&ctx, buffer, len);
    }
    SHA256_Final(hash, &ctx);
    fclose(file);

    return memcmp(hash, expected_hash, 32) == 0;
}

On Android, use ApplicationInfo to read the android:extractNativeLibs and android:fullBackupContent flags and validate the APK’s classes.dex and lib/ folders.

The app must also detect common tampering tools:

When tampering is detected, the app logs RUNTIME_TAMPERING_DETECTED and can trigger actions like:

Secure the App’s Execution Environment

The app must ensure that the runtime environment is trusted.

{
  "runtime": {
    "trusted_environments": [
      "device_is_jailbroken",
      "app_is_debuggable",
      "device_is_emulator",
      "network_is_public",
      "background_sync_enabled"
    ],
    "checks": {
      "device_is_jailbroken": {
        "method": "file_exists",
        "path": "/private/var/lib/apt",
        "expected": true
      },
      "app_is_debuggable": {
        "method": "android_manifest",
        "key": "android:debuggable",
        "expected": true
      },
      "device_is_emulator": {
        "method": "property_exists",
        "property": "ro.build.fingerprint",
        "values": [
          "generic_x86",
          "google/sdk_google_phone_x86",
          "samsung/klteatt/klte:5.0.1/LRX21M/1718933:userdebug/test-keys"
        ]
      },
      "network_is_public": {
        "method": "dhcp_config",
        "check": [
          "gateway",
          "dns"
        ],
        "expected": {
          "gateway": "192.168.1.1",
          "dns": [
            "8.8.8.8",
            "1.1.1.1"
          ]
        }
      }
    },
    "actions": {
      "on_tampering": [
        "invalidate_session_tokens",
        "enable_biometric_challenge",
        "enable_hardened_mode"
      ]
    }
  }
}

When any check fails, the app updates its runtime_state and records the failure reason in the logs. The app can re-check these conditions periodically in the background.

Handling Security Failures

When a security failure occurs, the app must not only detect it but also respond in a predictable way.

Log and Report Security Events

The app logs security events in a structured format using a standard schema.

{
  "event": "TLS_PINNING_MISMATCH",
  "timestamp": "2023-10-05T12:34:56.789Z",
  "details": {
    "expected_pinned_keys": [
      "sha256:47:18:5c:3d:98:fb:12:3a:8e:01:45:8e:2a:9d:7e:96:21:16:05:8b:0c:0e:55:11:1b:1d:9e:4c:50:9c:85:4b",
      "sha256:32:5a:12:7e:9c:1d:8a:4b:17:2c:54:91:4c:10:2d:1e:74:33:5b:16:44:91:14:8d:6f:28:02:12:13:7b:8c:31"
    ],
    "actual_certificate_chain": [
      {
        "subject": "CN=api.example.com",
        "issuer": "CN=Let's Encrypt Authority X3",
        "public_key": "sha256:47:18:5c:3d:98:fb:12:3a:8e:01:45:8e:2a:9d:7e:96:21:16:05:8b:0c:0e:55:11:1b:1d:9e:4c:50:9c:85:4b"
      },
      {
        "subject": "CN=Let's Encrypt Authority X3",
        "issuer": "CN=ISRG Root X1",
        "public_key": "sha256:32:5a:12:7e:9c:1d:8a:4b:17:2c:54:91:4c:10:2d:1e:74:33:5b:16:44:91:14:8d:6f:28:02:12:13:7b:8c:31"
      }
    ],
    "server_name": "api.example.com",
    "network_type": "wifi",
    "app_version": "1.4.2"
  }
}

The app uploads these events to a central logging service via a background HTTP request.

Respond to Critical Security Failures

When the app detects a critical failure—such as missing certificate pinning, tampered binary, or failed key retrieval—it must act immediately.

The app must be resilient to network loss, battery drain, and user interruptions during these responses.

Handle Key Rotation and Recovery

The app must support key rotation for encryption and authentication.

When a key is rotated (e.g., due to compromise), the app updates the master_key_version and encryption_key_version fields in its configuration.

{
  "keys": {
    "master_key_version": 3,
    "encryption_key_version": 2,
    "rotation": {
      "current": {
        "version": 2,
        "created_at": "2023-10-04T08:00:00Z",
        "key": "1234567890abcdef1234567890abcdef"
      },
      "previous": [
        {
          "version": 1,
          "created_at": "2023-09-25T10:15:30Z",
          "key": "fedcba9876543210fedcba9876543210"
        }
      ]
    }
  }
}

The app can decrypt data with any key in the previous list, allowing smooth transition during rotation. The app logs KEY_ROTATION_SUCCESS and KEY_ROTATION_FAILED events.

Summary of Key Patterns

These patterns ensure that mobile apps remain secure even when deployed on devices beyond the control of the app’s developers.

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.