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.
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:
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!
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:
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()
}
}
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!
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:
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\"")
}
}
Introduced in Android 7.0 (API 24), the Network Security Config file lets apps customize network security declarations without modifying Kotlin networking code:
<?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>
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!
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()
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:
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)
}
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:
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()
@Query("SELECT * FROM courses WHERE title = :searchQuery")). Never concatenate raw strings into RawQuery.
<intent-filter> is automatically exported to other apps on the device unless explicitly set to android:exported="false". Always explicitly specify this tag.
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:
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)
}
}
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.
BiometricPrompt.CryptoObject more secure than a standard boolean biometric callback?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.
prefs.getString("auth_token")) is fast while keeping the key name completely unreadable.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.
android:exported="false" protect against in the Android Manifest?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.
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.
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.