Security is not a feature you add at the end. It is a set of decisions made throughout the development process — what data you store, where you store it, how you transmit it, and what you trust. A security vulnerability in a shipped app can leak user data, enable account takeovers, and destroy user trust permanently.
Data Storage Security
Never store sensitive data in plaintext. The right storage choice depends on the sensitivity and size of the data.
| Data Type | Recommended Storage |
|---|---|
| Auth tokens, passwords | EncryptedSharedPreferences |
| User preferences (non-sensitive) | DataStore |
| Structured sensitive data | Room with SQLCipher |
| Credentials that survive uninstall | Android Keystore (not for storing data, for key management) |
| Nothing sensitive | SharedPreferences (plaintext, readable on rooted devices) |
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("auth_token", token).apply()
EncryptedSharedPreferences encrypts both keys and values. The encryption key is stored in the Android Keystore, which is hardware-backed on modern devices.
Network Security
TLS Only
Android enforces cleartext traffic blocking by default since API 28. Ensure your network_security_config.xml does not re-enable it:
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>
Reference it in the manifest:
<application
android:networkSecurityConfig="@xml/network_security_config" />
Certificate Pinning
Certificate pinning rejects connections to your server if the certificate does not match a known hash — even if the certificate is signed by a trusted CA. This blocks man-in-the-middle attacks.
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup pin
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
Always pin at least two hashes — the current certificate and a backup — so you can rotate without an app update. Get the hash with:
openssl s_client -connect api.example.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
Biometric Authentication
Use the BiometricPrompt API for authentication gates — protecting sensitive screens, confirming payments, or unlocking settings.
val biometricPrompt = BiometricPrompt(
activity,
ContextCompat.getMainExecutor(activity),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
proceedToSensitiveScreen()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
showError(errString.toString())
}
override fun onAuthenticationFailed() {
showError("Authentication failed")
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Confirm your identity")
.setSubtitle("Authentication required to continue")
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
Check capability before showing the prompt:
val biometricManager = BiometricManager.from(context)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> showBiometricPrompt()
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> showPinFallback()
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> promptEnrollment()
}
Sensitive Data in Logs
Development logs regularly include tokens, user IDs, and API responses. Strip them from release builds.
object Logger {
fun d(tag: String, message: String) {
if (BuildConfig.DEBUG) {
Log.d(tag, message)
}
}
}
Add a ProGuard rule to remove all Log.d and Log.v calls from release builds:
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
Never log passwords, tokens, credit card numbers, or PII — even in debug builds that might be captured by device analytics.
Exported Components
Every Activity, Service, BroadcastReceiver, and ContentProvider with android:exported="true" is accessible to other apps. Only export what other apps genuinely need to reach.
<!-- Bad: accidentally exported -->
<activity android:name=".SettingsActivity" android:exported="true" />
<!-- Correct: internal only -->
<activity android:name=".SettingsActivity" android:exported="false" />
Since API 31, Android requires explicit android:exported on all components with intent filters. The build will fail without it — which forces the decision.
Root and Emulator Detection
For high-security apps (banking, payment), detect environments where the device may be compromised. Libraries like RootBeer check for common root indicators:
val rootBeer = RootBeer(context)
if (rootBeer.isRooted) {
// Show warning or restrict functionality
}
Root detection is not foolproof — it can be bypassed on rooted devices — but it raises the cost of attack.
Permissions
Request only the permissions you need and at the latest possible moment. Declare the permission in the manifest:
<uses-permission android:name="android.permission.CAMERA" />
Request it in code when the user triggers the feature:
val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) openCamera() else showPermissionRationale()
}
// Triggered by a button click, not at startup
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
Practice
Audit an existing app for these specific issues: plaintext token storage in SharedPreferences, missing certificate pinning on the API client, any Log.d calls that print tokens or user data, and exported components that should be internal. Fix the plaintext storage issue first.
Summary
Encrypt sensitive data with EncryptedSharedPreferences. Enforce TLS and consider certificate pinning for high-value APIs. Gate sensitive screens with BiometricPrompt. Strip debug logs from release builds. Audit exported components and minimize permissions. Security is a continuous audit, not a one-time fix.