Mobile applications handle some of the most sensitive data in people’s digital lives. Banking credentials, personal messages, health records, location history, and authentication tokens all pass through apps on devices that can be lost, stolen, or compromised. Yet security often receives less attention than features during development, treated as a checkbox rather than a fundamental design concern. This guide provides Android developers with comprehensive knowledge of mobile security threats and practical defenses to protect users and their data.
Understanding the Android Security Model
Android’s security architecture provides multiple layers of protection that developers should understand and leverage. At the foundation, Linux kernel security isolates applications from each other and from the system. Each application runs in its own sandbox with a unique user ID, unable to access other applications’ files or memory without explicit permission.
The application sandbox is remarkably effective. Even if an attacker compromises your application, they cannot automatically access data from banking apps, read SMS messages, or modify system settings. They are confined to your application’s sandbox unless they find a way to escape it, which typically requires exploiting operating system vulnerabilities.
Above the kernel, Android’s permission system controls access to sensitive capabilities. Applications must declare permissions in their manifest, and users must grant runtime permissions for dangerous capabilities like camera access, location, and contacts. The principle of least privilege applies: request only the permissions your application actually needs, and request them only when needed.
SELinux provides mandatory access control that restricts what even privileged processes can do. Android’s SELinux policies prevent many categories of attacks by limiting what operations are possible regardless of traditional Unix permissions. Developers benefit from this protection automatically but should avoid practices that require disabling SELinux protections.
Secure Data Storage
How you store data significantly impacts your application’s security posture. Android provides several storage options with different security characteristics.
Internal storage places files in your application’s private directory, accessible only to your application by default. This is the most secure option for sensitive data that does not need to be shared. Files here are deleted when the application is uninstalled, and other applications cannot access them without root privileges.
However, internal storage is not encrypted by default on all devices. On devices without file-based encryption or full-disk encryption, someone with physical access to the device could potentially extract data. For highly sensitive information, add an additional layer of encryption.
SharedPreferences stores key-value pairs in XML files within internal storage. While convenient, standard SharedPreferences provides no encryption. The EncryptedSharedPreferences class from the AndroidX Security library encrypts both keys and values using the Android Keystore system, providing strong protection with minimal code changes.
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// Use exactly like regular SharedPreferences
securePrefs.edit().putString("auth_token", token).apply()
Room databases can be encrypted using SQLCipher, an open-source extension that provides transparent AES-256 encryption of database files. The encryption key should be derived from user input or stored in the Android Keystore, never hardcoded or stored in plain text.
External storage is readable by any application with the appropriate permission and should never contain sensitive data. Even with scoped storage restrictions in recent Android versions, treat external storage as public for security purposes.
The Android Keystore System
The Android Keystore provides hardware-backed or TEE-backed storage for cryptographic keys. Keys stored in the Keystore cannot be extracted from the device, even by applications that created them. Cryptographic operations using these keys happen within secure hardware, protecting the key material from software attacks.
Keystore keys can be bound to security conditions. You can require that the device be unlocked, that the user authenticate with biometrics, or that the key only be used within a certain time after authentication. These bindings are enforced by hardware and cannot be bypassed by software.
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val keySpec = KeyGenParameterSpec.Builder(
"my_secure_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(300)
.build()
keyGenerator.init(keySpec)
keyGenerator.generateKey()
For maximum security, check whether the device supports hardware-backed Keystore and adjust your security posture accordingly. Some devices implement Keystore in software only, which provides less protection against sophisticated attacks.
Network Security
Network communication is a primary attack surface for mobile applications. Man-in-the-middle attacks can intercept sensitive data, and malicious servers can send crafted responses to exploit parsing vulnerabilities.
HTTPS is mandatory for all network communication in modern Android. The platform blocks cleartext HTTP traffic by default, and applications must explicitly opt out of this protection. Never opt out. Even for seemingly innocuous data, cleartext traffic reveals information to network observers and can be modified in transit.
Certificate pinning adds an additional layer of protection against man-in-the-middle attacks. Standard HTTPS trusts any certificate signed by any certificate authority in the device’s trust store. If an attacker compromises a certificate authority or installs a malicious CA certificate on the device, they can intercept HTTPS traffic. Certificate pinning restricts which certificates your application accepts, typically pinning to your server’s specific certificate or public key.
The Network Security Configuration file provides a declarative way to configure network security settings including certificate pinning, cleartext traffic permissions, and custom trust anchors for debugging.
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.yourapp.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">base64EncodedSHA256PublicKeyHash=</pin>
<pin digest="SHA-256">backupPinInCaseOfKeyRotation=</pin>
</pin-set>
</domain-config>
</network-security-config>
Always include backup pins for key rotation. If your only pinned certificate expires or is compromised, applications with expired pins will be unable to connect to your servers at all. Plan for certificate rotation from the beginning.
Authentication and Session Management
Authentication verifies user identity, and session management maintains that verified state across application usage. Both are critical security functions that are frequently implemented incorrectly.
Never store passwords on the device. Use your server’s authentication endpoint to verify credentials and receive a session token. If you must support offline authentication, store a securely hashed representation that cannot reveal the original password.
Session tokens should be random, unpredictable, and sufficiently long to prevent brute-force guessing. Tokens should expire after a reasonable period of inactivity and should be invalidated server-side when users log out. Store tokens using EncryptedSharedPreferences or the Keystore, never in plain SharedPreferences or files.
Implement token refresh flows to maintain sessions without requiring repeated password entry. Short-lived access tokens combined with longer-lived refresh tokens balance security against user convenience. When a refresh token is used, invalidate the old one and issue new tokens to limit the window of exposure if tokens are compromised.
Biometric authentication provides convenient and secure local authentication. The BiometricPrompt API handles the complexity of different biometric types and integrates with the Keystore for cryptographic operations bound to biometric authentication.
val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
// Access Keystore keys or perform authenticated operations
val cipher = result.cryptoObject?.cipher
// Use cipher for decryption operations
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
// Handle authentication failure
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Verify your identity to access secure data")
.setNegativeButtonText("Use password")
.build()
biometricPrompt.authenticate(promptInfo, cryptoObject)
Input Validation and Injection Attacks
All input from external sources must be validated and sanitized. Users, servers, intents, content providers, and files can all provide malicious input designed to exploit your application.
SQL injection remains a threat if you construct SQL queries using string concatenation with user input. Always use parameterized queries or Room’s query builder, which handles escaping automatically. Never interpolate user input directly into SQL strings.
// DANGEROUS - SQL Injection vulnerability
val query = "SELECT * FROM users WHERE name = '$userInput'"
// SAFE - Parameterized query
@Query("SELECT * FROM users WHERE name = :name")
fun getUserByName(name: String): User
WebView introduces web security concerns into your application. If you load untrusted content or enable JavaScript, your application becomes vulnerable to cross-site scripting and other web attacks. Disable JavaScript unless absolutely required. Use HTTPS for all WebView content. Consider using setAllowFileAccess(false) and setAllowContentAccess(false) unless you need these capabilities.
Intent validation is frequently overlooked. If your application exports components that receive intents from other applications, validate all intent data before using it. Malicious applications can send crafted intents to exploit parsing vulnerabilities, cause denial of service, or access functionality they should not reach.
Deep links and app links present similar risks. Validate that incoming URLs match expected patterns before processing them. A malicious link could attempt to access sensitive functionality or inject malicious data into your application.
Code Protection and Reverse Engineering
Android applications can be decompiled and analyzed. Attackers can extract hardcoded secrets, understand your application’s logic, find vulnerabilities, and create modified versions. While you cannot prevent reverse engineering entirely, you can make it more difficult and time-consuming.
Never hardcode secrets like API keys, encryption keys, or passwords in your application code. They will be found. Use server-side secrets where possible. For client-side secrets that must exist, obfuscate them and consider splitting them across multiple locations to complicate extraction.
ProGuard or R8 obfuscation renames classes, methods, and fields to meaningless identifiers, making decompiled code harder to understand. Enable minification and obfuscation for release builds. Customize the rules to maximize obfuscation while preserving functionality.
# proguard-rules.pro
-optimizationpasses 5
-overloadaggressively
-repackageclasses ''
-allowaccessmodification
# Keep necessary classes for your frameworks
-keep class com.yourapp.api.models.** { *; }
-keepattributes Signature, *Annotation*
Root detection and tamper detection can identify compromised environments and modified applications. SafetyNet Attestation (now Play Integrity API) provides server-verifiable attestation of device integrity. However, determined attackers can bypass these checks. Treat them as speed bumps that increase attack cost, not as impenetrable barriers.
Secure Development Practices
Security is not a feature you add at the end but a practice woven throughout development. Adopt secure coding practices and incorporate security testing into your development process.
Conduct threat modeling early in development. Identify what data your application handles, who might want to access it maliciously, and what attack vectors exist. This analysis guides security decisions and helps prioritize where to invest protection efforts.
Keep dependencies updated. Vulnerabilities are regularly discovered in libraries your application uses. Monitor for security advisories and update promptly. Automated dependency scanning tools can alert you to known vulnerabilities in your dependency tree.
Use static analysis tools to find security issues automatically. Android Lint includes security-focused checks. Commercial tools like Checkmarx or Veracode provide deeper analysis. Integrate these tools into your CI/CD pipeline to catch issues before they reach production.
Perform penetration testing on release candidates. Automated tools catch common issues, but skilled testers find subtle vulnerabilities that tools miss. Consider engaging security professionals for critical applications.
Conclusion
Mobile security requires attention across every layer of your application. Store sensitive data encrypted using platform facilities. Communicate over HTTPS with certificate pinning. Validate all external input. Protect your code from easy reverse engineering. And build security into your development process rather than treating it as an afterthought.
The techniques covered here represent baseline security for Android applications. High-security applications like banking or healthcare may require additional measures including on-device intrusion detection, runtime application self-protection, and regular third-party security audits.
At RyuPy, security is a core consideration in everything we build. We understand that users trust applications with their data, and we take that trust seriously. Every developer building Android applications should share this commitment to protecting users.

0 Comments