🤖 Unit 15 • Android Mobile Engineering

Mobile Security: Keystore, Cryptography & Biometrics

Defending Android applications against reverse engineering, physical extraction, and network interception: Hardware Security Modules (TEE/StrongBox), the Android Keystore system, EncryptedSharedPreferences (Tink), zero-hardcoded secrets injection, TLS Certificate Pinning, input sanitization against SQL injection, biometric CryptoObject authentication, and SQLCipher Room encryption.

🔐 Storage: Android Keystore & Tink AES-256
🌐 Network: Certificate Pinning & HTTPS
👆 Auth: BiometricPrompt CryptoObject
🗄️ Database: SQLCipher for Encrypted Room
01

The Android Keystore System & Hardware Security Modules (TEE / StrongBox)

🏛️ Hardware-Backed Key Isolation

Software-based encryption keys stored in RAM or app files are vulnerable to memory dump attacks on rooted devices. To provide military-grade security, Android introduces the Android Keystore System, backed by dedicated hardware isolation:

  • Trusted Execution Environment (TEE): A secure, isolated subsystem running alongside the main application processor (ARM TrustZone) with its own dedicated secure operating system.
  • StrongBox Keymaster (Android 9+ / API 28): A physically isolated hardware security module (HSM) with dedicated CPU, true random number generator (TRNG), tamper-resistant packaging, and secure storage (e.g. Google Titan M2 chip on Pixel devices).

The Non-Exportability Guarantee: Cryptographic keys generated inside the Android Keystore can never be extracted from the secure hardware. When an app performs encryption or decryption, the plaintext is passed into the TEE/StrongBox, the hardware chip executes the AES-256-GCM cipher internally, and only the ciphertext or plaintext result is returned to user space. Even if an attacker gains full Linux root access to the device kernel, the private keys remain unextractable!

Visual Architecture: Android Keystore & Hardware TEE / StrongBox Pipeline
App Process (User Space) Sensitive Data / JWT Tokens
➔
KeyStore Provider "AndroidKeyStore" API Bridge
⬇ Cryptographic request dispatched across hardware boundary
ARM TrustZone (TEE) Isolated Secure OS Kernel
⇄
StrongBox HSM Chip Tamper-Resistant Hardware Storage
⬇ Keys NEVER leave hardware chip
AES-256-GCM Encryption Hardware-executed cipher
➔
Encrypted Ciphertext Returned safely to App Sandbox
02

Secure Data Storage: EncryptedSharedPreferences & Token Management

📦 EncryptedSharedPreferences (Jetpack Security / Google Tink)

Standard SharedPreferences write keys and values as unencrypted cleartext XML files at /data/data/<package>/shared_prefs/. Anyone with a rooted device or ADB backup can read stored passwords and authentication tokens directly.

EncryptedSharedPreferences (part of Jetpack Security, powered by Google's audited cryptographic library Tink) wraps standard SharedPreferences with automatic hardware encryption:

  • Keys: Deterministically encrypted using AES-256-SIV (Synthetic Initialization Vector), meaning keys cannot be read, but lookups remain O(1) fast.
  • Values: Authenticated and encrypted using AES-256-GCM (Galois/Counter Mode), preventing both eavesdropping and unauthorized tampering.
SecureStorageManager.kt Kotlin
object SecureStorageManager {
    private const val PREFS_FILE = "devascent_secure_prefs"

    fun getEncryptedPrefs(context: Context): SharedPreferences {
        // 1. Generate or retrieve hardware-backed MasterKey in Android Keystore
        val masterKey = MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
            .build()

        // 2. Instantiate EncryptedSharedPreferences
        return EncryptedSharedPreferences.create(
            context,
            PREFS_FILE,
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )
    }

    fun saveAuthToken(context: Context, token: String) {
        getEncryptedPrefs(context).edit()
            .putString("auth_jwt_token", token)
            .apply()
    }
}
03

Protecting API Keys & Eliminating Hard-Coded Secrets

⚠️ The Risk of Hard-Coded Secrets

Hardcoding API keys, AWS credentials, or encryption keys directly into Kotlin source files (e.g. val API_KEY = "sk_live_12345") is an immediate critical vulnerability. When an APK is compiled, strings are packed into the classes.dex bytecode. Anyone can decompile the APK using tools like jadx or run strings classes.dex to extract your secrets in under 5 seconds!

🛡️ Defense-in-Depth for Mobile Secrets
  1. local.properties & Gradle BuildConfig: Store secrets in local.properties (which is automatically excluded in .gitignore). Read these values inside build.gradle.kts and inject them into BuildConfig:
    build.gradle.kts Gradle Kotlin DSL
    val localProperties = Properties().apply {
        val propFile = rootProject.file("local.properties")
        if (propFile.exists()) load(propFile.inputStream())
    }
    
    android {
        defaultConfig {
            val apiKey = localProperties.getProperty("DEVASCENT_API_KEY", "")
            buildConfigField("String", "API_KEY", "\"$apiKey\"")
        }
    }
  2. Backend Proxy Architecture (The Gold Standard): True enterprise applications never embed master secrets in mobile clients. Mobile clients authenticate with a backend proxy using short-lived JWT tokens, and the backend server injects third-party secret keys securely.
04

Network Security: TLS, Network Security Config & Certificate Pinning

🌐 Network Security Config XML

Introduced in Android 7.0 (API 24), the Network Security Config file lets apps customize network security declarations without modifying Kotlin networking code:

res/xml/network_security_config.xml XML
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- Prohibit cleartext HTTP across the entire application -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <!-- Trust ONLY official system CAs; ignore user-installed proxy certs -->
            <certificates src="system" />
        </trust-anchors>
    </base-config>
</network-security-config>
📌 Man-in-the-Middle (MitM) Attacks & Certificate Pinning

Standard HTTPS relies on trusting public Certificate Authorities (CAs). If an attacker compromises a CA or installs a custom root CA on a corporate Wi-Fi network (or uses tools like Charles Proxy / Burp Suite), they can decrypt and inspect all API traffic.

Certificate Pinning: The mobile app hardcodes the cryptographic SHA-256 public key hash (Subject Public Key Info - SPKI) of the server's TLS certificate. During the TLS handshake, OkHttp verifies that the server's certificate matches the hardcoded pin. If it does not match, the connection is terminated immediately!

PinnedHttpClient.kt Kotlin / OkHttp
val certificatePinner = CertificatePinner.Builder()
    .add(
        "api.devascentacademy.me",
        "sha256/k2v657xUM4Mm80Afj5gf2140V12V73nU4qFpX8=", // Primary Pin
        "sha256/JSMzqOOrtynOT1kmau6zKhgT60NkGgYZUcw="  // Backup Pin for cert rotation!
    )
    .build()

val okHttpClient = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()
05

Biometric Authentication & CryptoObject Binding

👆 BiometricPrompt & Hardware Cryptographic Unlocking

Many applications simply ask the biometric scanner for a boolean: "Did the user authenticate? Yes/No". On a rooted device, an attacker using instrumentation frameworks like Frida can hook the callback function and force-return true to bypass authentication entirely!

The Secure Solution: BiometricPrompt.CryptoObject:
Instead of relying on a naive boolean check, the Android Keystore key is configured to require user authentication (setUserAuthenticationRequired(true)). The app instantiates an uninitialized Cipher and wraps it inside a BiometricPrompt.CryptoObject. The Android hardware chip will physically refuse to unlock the cryptographic key until the fingerprint or face authentication succeeds in the TEE:

BiometricCryptoAuth.kt Kotlin
fun authenticateWithBiometrics(
    activity: FragmentActivity,
    cryptoObject: BiometricPrompt.CryptoObject,
    onSuccess: (Cipher) -> Unit
) {
    val executor = ContextCompat.getMainExecutor(activity)

    val prompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            // Cipher is now physically unlocked by the TEE hardware
            result.cryptoObject?.cipher?.let(onSuccess)
        }
    })

    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric Security Verification")
        .setSubtitle("Confirm your identity to decrypt sensitive credentials.")
        .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
        .setNegativeButtonText("Cancel")
        .build()

    prompt.authenticate(promptInfo, cryptoObject)
}
06

Secure Database Practices: SQLCipher for Room & Input Validation

🗄️ Full-Disk SQLite Database Encryption with SQLCipher

Standard Room database SQLite files reside as unencrypted binary records at /data/data/<package>/databases/app.db. SQLCipher is an extension providing transparent, 256-bit AES full-database file encryption:

EncryptedRoomDatabase.kt Kotlin / Room
val passphrase = SecureStorageManager.getDatabasePassphrase()
val factory = SupportFactory(passphrase) // SQLCipher OpenHelperFactory

val encryptedDb = Room.databaseBuilder(
    context,
    DevAscentDatabase::class.java,
    "secure_devascent.db"
)
    .openHelperFactory(factory) // Injects SQLCipher transparent 256-bit AES encryption
    .build()
🛡️ Input Validation & Defense in Depth
  • SQL Injection Prevention: Always use parameterized Room DAO queries (e.g. @Query("SELECT * FROM courses WHERE title = :searchQuery")). Never concatenate raw strings into RawQuery.
  • Exported Component Vulnerabilities: Any Activity, Service, or BroadcastReceiver with an <intent-filter> is automatically exported to other apps on the device unless explicitly set to android:exported="false". Always explicitly specify this tag.
07

Production Capstone: Unified Hardware-Backed Security Vault

The production-grade component below provides an end-to-end Hardware Security Vault, integrating Android Keystore AES-256-GCM key generation, biometric CryptoObject binding, and authenticated token encryption:

DevAscentSecurityVault.kt Kotlin / Android Keystore
package me.devascent.academy.security

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 DevAscentSecurityVault {

    companion object {
        private const val ANDROID_KEYSTORE = "AndroidKeyStore"
        private const val KEY_ALIAS = "devascent_vault_key"
        private const val TRANSFORMATION = "AES/GCM/NoPadding"
        private const val GCM_TAG_LENGTH = 128
    }

    private val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
        load(null)
    }

    init {
        generateHardwareBackedKey()
    }

    private fun generateHardwareBackedKey() {
        if (!keyStore.containsAlias(KEY_ALIAS)) {
            val keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                ANDROID_KEYSTORE
            )

            val spec = KeyGenParameterSpec.Builder(
                KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setKeySize(256)
                .setUserAuthenticationRequired(false) // Can be set to true for biometric gating
                .build()

            keyGenerator.init(spec)
            keyGenerator.generateKey()
        }
    }

    private fun getKey(): SecretKey {
        return (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey
    }

    // Encrypts plaintext and prepends the Initialization Vector (IV)
    fun encryptData(plaintext: ByteArray): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getKey())

        val iv = cipher.iv // 12-byte GCM initialization vector
        val ciphertext = cipher.doFinal(plaintext)

        // Prepend 12-byte IV to ciphertext: [IV (12 bytes)] + [Ciphertext + AuthTag]
        return iv + ciphertext
    }

    // Extracts IV and decrypts ciphertext using hardware chip
    fun decryptData(encryptedPayload: ByteArray): ByteArray {
        val iv = encryptedPayload.copyOfRange(0, 12)
        val ciphertext = encryptedPayload.copyOfRange(12, encryptedPayload.size)

        val cipher = Cipher.getInstance(TRANSFORMATION)
        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)

        return cipher.doFinal(ciphertext)
    }
}
Simulated Android Studio Logcat Security Audit Output
10:05:01.120 I/DevAscentSecurity: Initializing AndroidKeyStore provider [Hardware: StrongBox HSM] 10:05:01.145 D/Keystore2: Generated Key: alias=devascent_vault_key, algorithm=AES-256-GCM, TEE=true 10:05:02.302 I/DevAscentSecurity: Encrypted token payload [Length: 128 bytes, IV: 12 bytes] 10:05:03.114 D/OkHttp: CertificatePinner: Verified pin for api.devascentacademy.me [Match: sha256/k2v657xU...] 10:05:03.450 I/RoomDatabase: SQLCipher database unlocked with Keystore passphrase. SQLite version 3.39.2
08

University Exam & Technical Interview Viva Guide (8 Master Q&As)

Q1. Why is the Android Keystore system superior to storing cryptographic keys in SharedPreferences?
Standard SharedPreferences stores data in plaintext on the internal file system, where any attacker with root access or physical extraction tools can read the keys. The Android Keystore system generates and stores keys inside dedicated hardware isolation (Trusted Execution Environment (TEE) or StrongBox HSM). Keys are non-exportable: the encryption and decryption operations occur directly inside the hardware chip, ensuring that private keys cannot be extracted even if the OS kernel is compromised.
Q2. What is Certificate Pinning and why is it necessary if an app already uses HTTPS?
HTTPS relies on the device trusting several hundred public Certificate Authorities (CAs). If an attacker compromises a CA, operates a malicious proxy on a corporate Wi-Fi network, or installs a custom root CA certificate on the user's phone, they can decrypt and intercept all SSL traffic via a Man-in-the-Middle (MitM) attack. Certificate Pinning forces the mobile client to accept only specific, hardcoded public key hashes (SPKI pins), rejecting any certificate signed by an untrusted or compromised CA.
Q3. Why is using BiometricPrompt.CryptoObject more secure than a standard boolean biometric callback?
In a naive biometric implementation, the app checks if onAuthenticationSucceeded was called and proceeds. On rooted devices, attackers can use dynamic instrumentation frameworks like Frida to hook the callback and force it to return true. With CryptoObject, the cryptographic key in the Keystore is locked by the hardware chip. The TEE hardware will only release the decrypted key if the physical fingerprint or face match actually succeeded in the hardware sensor, making software hook bypasses impossible.
Q4. How does EncryptedSharedPreferences encrypt both keys and values?
EncryptedSharedPreferences utilizes two distinct encryption schemes powered by Google's Tink library:
  1. Keys: Encrypted using AES-256-SIV (Synthetic Initialization Vector). This is deterministic encryption, ensuring that looking up a key (e.g. prefs.getString("auth_token")) is fast while keeping the key name completely unreadable.
  2. Values: Encrypted using AES-256-GCM (Galois/Counter Mode). This provides authenticated encryption that guarantees both confidentiality and tamper-resistance.
Q5. Why is committing API keys to source control dangerous and what is the recommended architecture?
Any string compiled into an Android APK can be extracted within seconds using tools like jadx or strings classes.dex. Storing secrets in Git repositories also risks public exposure. The recommended architecture is a Backend Proxy Service: the mobile client authenticates with the developer's server using short-lived JWT tokens, and the backend server securely calls third-party APIs (Stripe, OpenAI, AWS) without exposing the master credentials on mobile devices.
Q6. What does android:exported="false" protect against in the Android Manifest?
Setting android:exported="false" ensures that an Activity, Service, or BroadcastReceiver can only be launched by components within the same application or by apps with the exact same Linux User ID. If left as true, malicious third-party apps installed on the device can send explicit intents to launch internal payment screens, trigger unauthorized services, or inject malicious broadcast data.
Q7. What is SQLCipher and how does it protect Room database data?
SQLCipher is an open-source extension that provides transparent, 256-bit AES encryption of SQLite database files. When used with Room (via SupportFactory), every page of the database on disk is encrypted with AES-256-CBC. If someone steals the physical device or extracts the app.db file from storage, the database cannot be opened without the master passphrase.
Q8. How does Android protect against cleartext HTTP traffic by default?
Starting in Android 9 (API 28), the Android OS enforces android:usesCleartextTraffic="false" by default. Any attempt by an application to connect to an unencrypted http:// endpoint is blocked at the socket level by the OS, throwing an IOException: Cleartext HTTP traffic not permitted. This protects users from eavesdropping and packet sniffing on unencrypted public Wi-Fi networks.