Chapter 1: Android Security Fundamentals
Security is not a feature you add at the end of development—it's a mindset that shapes every decision you make from the first line of code. Before diving into specific security implementations, you need to understand how Android protects applications at the system level and where those protections fall short.
This chapter establishes the foundation for everything that follows. You'll learn how Android's security architecture works, how the permission system protects users, and how to think about threats systematically. By the end, you'll understand not just what to secure, but why Android's security model works the way it does.
1.1 Android Security Architecture Overview
Android's security is built on multiple layers, each providing defense against different types of attacks. Understanding these layers helps you identify where your app's security responsibilities begin.
1.1.1 The Security Stack
┌─────────────────────────────────────────────────┐
│ Application Layer │
│ (Your App's Security Logic) │
├─────────────────────────────────────────────────┤
│ Android Framework │
│ (Permissions, Crypto APIs, Keystore) │
├─────────────────────────────────────────────────┤
│ Native Libraries & ART │
│ (Memory Safety, Code Verification) │
├─────────────────────────────────────────────────┤
│ Linux Kernel │
│ (Process Isolation, SELinux, Seccomp) │
├─────────────────────────────────────────────────┤
│ Hardware Security │
│ (TEE, StrongBox, Secure Element, TrustZone) │
└─────────────────────────────────────────────────┘
Each layer builds upon the security guarantees of the layer below it. As an app developer, you primarily work at the Application and Framework layers, but the lower layers provide critical protections that make your security code meaningful.
1.1.2 Linux Kernel Security
At its core, Android is a Linux-based operating system. This provides several fundamental security mechanisms:
Process Isolation
Every Android application runs in its own process with a unique Linux user ID (UID). This isolation means:
- Apps cannot directly access other apps' memory
- File system permissions prevent unauthorized data access
- System resources are protected from unprivileged apps
// Each app gets a unique UID at install time
// Format: u0_a{number} for user apps
// Example: u0_a123
// You can check your app's UID programmatically
val myUid = android.os.Process.myUid()
Log.d("Security", "My UID: $myUid")
SELinux (Security-Enhanced Linux)
Android uses SELinux in enforcing mode to provide Mandatory Access Control (MAC). Unlike traditional Linux permissions (Discretionary Access Control), SELinux policies cannot be overridden by the file owner.
# SELinux denies access even if file permissions allow it
# Example policy (simplified):
# allow untrusted_app app_data_file:file { read write };
# deny untrusted_app system_data_file:file { read write };
SELinux policies define:
- Which processes can access which files
- What system calls an app can make
- How apps can communicate with system services
Seccomp (Secure Computing)
Seccomp filters restrict which system calls an application can make. Android uses seccomp-bpf to block dangerous system calls that could be used for exploitation:
# Example: These system calls are blocked for apps
# - ptrace (process debugging)
# - mount (filesystem manipulation)
# - reboot (system control)
1.1.3 Application Sandbox
The application sandbox is Android's primary security mechanism. Each app operates within its own sandbox with:
| Resource | Isolation Level | Notes |
|---|---|---|
| Process | Complete | Separate Linux process per app |
| User ID | Unique | Each app gets its own UID |
| File Storage | Isolated | /data/data/<package>/ is private |
| Memory | Protected | No direct access to other apps' memory |
| Network | Shared | Requires INTERNET permission |
| Hardware | Mediated | Accessed through system services |
Sandbox Boundaries:
┌──────────────────────────────────────────────────────┐
│ Your App Sandbox │
│ ┌─────────────────────────────────────────────────┐ │
│ │ /data/data/com.yourapp/ │ │
│ │ ├── shared_prefs/ (Private) │ │
│ │ ├── databases/ (Private) │ │
│ │ ├── files/ (Private) │ │
│ │ └── cache/ (Private) │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ System Services (Binder IPC) │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ Camera Service Location Service │
│ (Permission Required) (Permission Required)│
└──────────────────────────────────────────────────────┘
1.1.4 Hardware-Backed Security
Modern Android devices include dedicated hardware for security operations:
Trusted Execution Environment (TEE)
The TEE is an isolated execution environment that runs alongside the main operating system. It provides:
- Secure key storage
- Cryptographic operations
- Biometric data processing
- DRM content protection
// Check if hardware-backed keystore is available
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyInfo = keyStore.getKey("my_key", null)?.let { key ->
val factory = KeyFactory.getInstance(key.algorithm, "AndroidKeyStore")
factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo
}
val isHardwareBacked = keyInfo?.isInsideSecureHardware == true
StrongBox
StrongBox is a tamper-resistant hardware security module found in devices with dedicated secure processors (like Google's Titan M chip):
// Request StrongBox-backed key generation
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"strongbox_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setIsStrongBoxBacked(true) // Request StrongBox
.build()
try {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
} catch (e: StrongBoxUnavailableException) {
// Fall back to TEE-backed key
Log.w("Security", "StrongBox not available, using TEE")
}
1.2 The Android Permission System
Permissions are the primary mechanism for protecting user privacy and controlling access to sensitive resources. Understanding how permissions work—and their limitations—is essential for secure app development.
1.2.1 Permission Architecture
Android permissions are organized into protection levels:
| Protection Level | Description | User Interaction | Example |
|---|---|---|---|
| Normal | Low-risk permissions | Granted automatically | INTERNET, BLUETOOTH |
| Dangerous | Access to sensitive data | Runtime prompt required | CAMERA, LOCATION |
| Signature | Same signing certificate | Automatic if signed same | System-level APIs |
| Privileged | System apps only | Pre-installed apps | MANAGE_USERS |
Permission Declaration:
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.secureapp">
<!-- Normal permissions - granted automatically -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Dangerous permissions - require runtime request -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Permission with max SDK (for backward compatibility) -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
</manifest>
1.2.2 Runtime Permissions (Android 6.0+)
Since Android 6.0 (API 23), dangerous permissions must be requested at runtime:
class MainActivity : AppCompatActivity() {
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
openCamera()
} else {
handlePermissionDenied()
}
}
private val multiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
startLocationTracking()
} else {
handlePartialPermissions(permissions)
}
}
private fun requestCameraPermission() {
when {
// Already granted
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
openCamera()
}
// Should show rationale
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showPermissionRationaleDialog()
}
// Request permission
else -> {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
private fun requestLocationPermissions() {
val permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
// Check if already granted
val allGranted = permissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
if (allGranted) {
startLocationTracking()
} else {
multiplePermissionsLauncher.launch(permissions)
}
}
private fun handlePermissionDenied() {
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
// User selected "Don't ask again" - direct to settings
showSettingsDialog()
} else {
// Show explanation and retry option
showPermissionDeniedMessage()
}
}
private fun showSettingsDialog() {
AlertDialog.Builder(this)
.setTitle("Permission Required")
.setMessage("Camera permission is required. Please enable it in Settings.")
.setPositiveButton("Settings") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
}
1.2.3 Permission Best Practices
Principle of Least Privilege:
Request only the permissions you absolutely need, when you need them:
// ❌ BAD: Requesting all permissions at app launch
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Don't do this - users will deny permissions they don't understand
requestAllPermissions()
}
}
// ✅ GOOD: Request permissions in context
class PhotoActivity : AppCompatActivity() {
private fun onTakePhotoClicked() {
// User understands why camera is needed
if (hasCameraPermission()) {
takePhoto()
} else {
requestCameraPermission()
}
}
}
Permission Groups (Android 11+):
Android groups related permissions together. Granting one permission in a group may automatically grant others:
// Location permission group
// ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION are in the same group
// But ACCESS_BACKGROUND_LOCATION requires separate request
private fun requestBackgroundLocation() {
// First, ensure foreground location is granted
if (hasForegroundLocationPermission()) {
// Then request background location separately
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
backgroundLocationLauncher.launch(
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
}
} else {
// Request foreground first
requestForegroundLocation()
}
}
1.2.4 One-Time Permissions (Android 11+)
Users can grant permissions for a single session only:
// Check permission status for appropriate handling
private fun checkPermissionStatus(permission: String): PermissionStatus {
return when {
ContextCompat.checkSelfPermission(this, permission) ==
PackageManager.PERMISSION_GRANTED -> PermissionStatus.GRANTED
shouldShowRequestPermissionRationale(permission) ->
PermissionStatus.DENIED_CAN_ASK
else -> PermissionStatus.DENIED_PERMANENTLY
}
}
enum class PermissionStatus {
GRANTED,
DENIED_CAN_ASK, // User denied but can ask again
DENIED_PERMANENTLY // User selected "Don't ask again" or one-time expired
}
// Handle one-time permission gracefully
override fun onResume() {
super.onResume()
// Re-check permissions on resume - they may have been revoked
if (!hasCameraPermission()) {
// Camera was a one-time grant that expired
showCameraPermissionNeeded()
}
}
1.2.5 Permission Auto-Reset (Android 11+)
Unused apps automatically have their permissions revoked:
// Check if your app is exempt from auto-reset
private fun checkAutoResetStatus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val packageManager = packageManager
val isExempt = packageManager.isAutoRevokeWhitelisted
if (!isExempt) {
// Permissions may be auto-reset if app is unused
// Consider reminding users to open the app periodically
// Or request exemption if critical (e.g., health monitoring app)
}
}
}
// Request exemption (use sparingly - requires justification)
private fun requestAutoResetExemption() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
startActivity(intent)
// User must manually disable "Pause app activity if unused"
}
}
1.3 Security Changes Across Android Versions
Android's security model evolves with each release. Understanding these changes ensures your app remains secure and functional across versions.
1.3.1 Security Evolution Timeline
| Android Version | API | Key Security Changes |
|---|---|---|
| 6.0 Marshmallow | 23 | Runtime permissions |
| 7.0 Nougat | 24 | File-based encryption, Network Security Config |
| 8.0 Oreo | 26 | Background execution limits, autofill framework |
| 9.0 Pie | 28 | TLS by default, biometric API |
| 10 Q | 29 | Scoped storage, background location limits |
| 11 R | 30 | One-time permissions, package visibility |
| 12 S | 31 | Approximate location, bluetooth permissions |
| 12L | 32 | Extended security for large screens |
| 13 T | 33 | Photo picker, notification permissions |
| 14 U | 34 | Credential manager, partial photo access |
| 15 V | 35 | Enhanced privacy dashboard, file integrity |
1.3.2 Handling Version-Specific Security
object SecurityCompat {
/**
* Check if device supports hardware-backed keystore
*/
fun hasHardwareBackedKeystore(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
/**
* Check if StrongBox is available
*/
fun hasStrongBox(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
context.packageManager.hasSystemFeature(
PackageManager.FEATURE_STRONGBOX_KEYSTORE
)
} else {
false
}
}
/**
* Check if biometric authentication is available
*/
fun hasBiometricCapability(context: Context): BiometricCapability {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
val biometricManager = BiometricManager.from(context)
when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)) {
BiometricManager.BIOMETRIC_SUCCESS ->
BiometricCapability.STRONG
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
BiometricCapability.NOT_ENROLLED
else ->
BiometricCapability.NOT_AVAILABLE
}
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
@Suppress("DEPRECATION")
val fingerprintManager = context.getSystemService(
FingerprintManager::class.java
)
if (fingerprintManager?.isHardwareDetected == true) {
if (fingerprintManager.hasEnrolledFingerprints()) {
BiometricCapability.WEAK
} else {
BiometricCapability.NOT_ENROLLED
}
} else {
BiometricCapability.NOT_AVAILABLE
}
}
else -> BiometricCapability.NOT_AVAILABLE
}
}
/**
* Get appropriate storage location based on Android version
*/
fun getSecureStorageDir(context: Context): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Use credential-encrypted storage
val ceContext = context.createCredentialProtectedStorageContext()
ceContext.filesDir
} else {
context.filesDir
}
}
}
enum class BiometricCapability {
STRONG, // Class 3 biometrics (fingerprint, face with depth)
WEAK, // Class 2 biometrics
NOT_ENROLLED, // Hardware available but not set up
NOT_AVAILABLE // No biometric hardware
}
1.3.3 Network Security Configuration (Android 7.0+)
Control your app's network security behavior declaratively:
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Base configuration for all connections -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Domain-specific configuration -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.yourcompany.com</domain>
<!-- Certificate pinning -->
<pin-set expiration="2025-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- Backup pin (REQUIRED) -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
<!-- Debug-only overrides -->
<debug-overrides>
<trust-anchors>
<!-- Trust user-installed CA certificates for debugging -->
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
1.4 Threat Modeling for Android Applications
Secure development starts with understanding what you're protecting against. Threat modeling helps you identify vulnerabilities before attackers do.
1.4.1 The STRIDE Model
STRIDE is a framework for categorizing security threats:
| Threat | Description | Android Example |
|---|---|---|
| Spoofing | Impersonating a user or system | Fake login screens, stolen tokens |
| Tampering | Modifying data or code | APK modification, man-in-the-middle |
| Repudiation | Denying actions occurred | Fraudulent transactions without logs |
| Information Disclosure | Unauthorized data access | Leaking PII, exposed API keys |
| Denial of Service | Making system unavailable | Resource exhaustion, crashes |
| Elevation of Privilege | Gaining unauthorized access | Root exploits, permission bypass |
1.4.2 Android-Specific Threat Landscape
Attack Surfaces:
┌─────────────────────────────────────────────────────────────┐
│ ATTACK SURFACES │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Network │ │ Storage │ │ IPC │ │
│ │ │ │ │ │ │ │
│ │ • MITM │ │ • Root │ │ • Intent │ │
│ │ • SSL Strip │ │ • Backup │ │ Injection │ │
│ │ • DNS Spoof │ │ • SD Card │ │ • Broadcast │ │
│ │ • API Abuse │ │ • Logs │ │ Sniffing │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Binary │ │ Input │ │ Device │ │
│ │ │ │ │ │ │ │
│ │ • Reverse │ │ • SQL │ │ • Rooted │ │
│ │ Engineer │ │ Injection │ │ • Emulator │ │
│ │ • Tampering │ │ • XSS │ │ • USB Debug │ │
│ │ • Debugging │ │ • Deep Links │ │ • Screen │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.4.3 Creating a Threat Model
Step 1: Identify Assets
/**
* Document what you're protecting
*/
data class SecurityAsset(
val name: String,
val sensitivity: Sensitivity,
val storageLocation: StorageLocation,
val threats: List<String>
)
enum class Sensitivity { LOW, MEDIUM, HIGH, CRITICAL }
enum class StorageLocation { MEMORY, PREFERENCES, DATABASE, FILE, NETWORK }
val securityAssets = listOf(
SecurityAsset(
name = "Authentication Token",
sensitivity = Sensitivity.CRITICAL,
storageLocation = StorageLocation.PREFERENCES,
threats = listOf("Token theft", "Session hijacking", "Replay attacks")
),
SecurityAsset(
name = "User PII",
sensitivity = Sensitivity.HIGH,
storageLocation = StorageLocation.DATABASE,
threats = listOf("Data breach", "Unauthorized access", "Backup extraction")
),
SecurityAsset(
name = "API Keys",
sensitivity = Sensitivity.CRITICAL,
storageLocation = StorageLocation.MEMORY,
threats = listOf("Key extraction", "Reverse engineering", "Traffic analysis")
)
)
Step 2: Identify Threat Actors
| Actor | Capability | Motivation | Example Attack |
|---|---|---|---|
| Script Kiddie | Low | Curiosity, bragging | Public exploit tools |
| Competitor | Medium | Business intelligence | Reverse engineering |
| Cybercriminal | High | Financial gain | Account takeover |
| Nation State | Very High | Espionage | Zero-day exploits |
| Malicious Insider | Varies | Revenge, profit | Direct data access |
Step 3: Document Attack Scenarios
data class ThreatScenario(
val id: String,
val threat: String,
val attackVector: String,
val asset: String,
val likelihood: Int, // 1-5
val impact: Int, // 1-5
val mitigation: String
) {
val riskScore: Int get() = likelihood * impact
}
val threatScenarios = listOf(
ThreatScenario(
id = "T001",
threat = "Token Theft via Backup",
attackVector = "ADB backup extraction on unencrypted storage",
asset = "Authentication Token",
likelihood = 4,
impact = 5,
mitigation = "Use EncryptedSharedPreferences, disable backup"
),
ThreatScenario(
id = "T002",
threat = "Man-in-the-Middle Attack",
attackVector = "SSL interception on public WiFi",
asset = "API Communications",
likelihood = 3,
impact = 4,
mitigation = "Certificate pinning, TLS 1.3 enforcement"
),
ThreatScenario(
id = "T003",
threat = "Intent Injection",
attackVector = "Malicious app sends crafted intents",
asset = "Application Logic",
likelihood = 3,
impact = 3,
mitigation = "Validate all intent data, use explicit intents"
),
ThreatScenario(
id = "T004",
threat = "API Key Extraction",
attackVector = "APK decompilation and static analysis",
asset = "API Keys",
likelihood = 5,
impact = 4,
mitigation = "Server-side key management, obfuscation"
)
)
1.4.4 Risk Assessment Matrix
IMPACT
Low Medium High Critical
┌──────┬────────┬────────┬──────────┐
High │ 4 │ 8 │ 12 │ 16 │ ← Address Immediately
├──────┼────────┼────────┼──────────┤
Medium │ 3 │ 6 │ 9 │ 12 │ ← Plan Mitigation
L ├──────┼────────┼────────┼──────────┤
I Low │ 2 │ 4 │ 6 │ 8 │ ← Monitor
K ├──────┼────────┼────────┼──────────┤
E Rare │ 1 │ 2 │ 3 │ 4 │ ← Accept Risk
L └──────┴────────┴────────┴──────────┘
I
H
O
O
D
1.5 Secure Development Lifecycle
Security must be integrated throughout your development process, not bolted on at the end.
1.5.1 Security Gates
┌─────────────────────────────────────────────────────────────────┐
│ SECURE DEVELOPMENT LIFECYCLE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DESIGN BUILD TEST RELEASE │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Threat│ │Static│ │Dynamic│ │Final │ │
│ │Model │ → │Scan │ → │Testing│ → │Review│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Security Dependency Penetration Security │
│ Requirements Analysis Testing Sign-off │
│ │
└─────────────────────────────────────────────────────────────────┘
1.5.2 Security Requirements Checklist
Use this checklist at the start of every project:
## Security Requirements Checklist
### Data Protection
- [ ] Identify all sensitive data the app will handle
- [ ] Define encryption requirements for data at rest
- [ ] Define encryption requirements for data in transit
- [ ] Plan secure data deletion procedures
### Authentication & Authorization
- [ ] Define authentication mechanism (password, biometric, OAuth)
- [ ] Plan session management strategy
- [ ] Define authorization levels and access controls
- [ ] Plan account recovery procedures
### Network Security
- [ ] Require TLS for all network communication
- [ ] Plan certificate pinning strategy
- [ ] Define API security requirements
- [ ] Plan for network error handling
### Code Security
- [ ] Define obfuscation requirements
- [ ] Plan for secure API key management
- [ ] Define logging policy (no sensitive data)
- [ ] Plan anti-tampering measures
### Platform Security
- [ ] Define minimum Android version
- [ ] List required permissions with justification
- [ ] Define backup policy
- [ ] Plan root/emulator detection if needed
1.5.3 Continuous Security Integration
build.gradle configuration for security checks:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
// ... standard configuration
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Security-focused build config
buildConfigField("Boolean", "ENABLE_LOGGING", "false")
buildConfigField("Boolean", "ENABLE_DEBUG_FEATURES", "false")
}
debug {
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
buildConfigField("Boolean", "ENABLE_DEBUG_FEATURES", "true")
}
}
// Lint checks for security issues
lint {
warningsAsErrors = true
abortOnError = true
// Enable security-related checks
enable += setOf(
"HardcodedDebugMode",
"AllowBackup",
"SetWorldReadable",
"SetWorldWritable",
"GrantAllUriPermissions",
"WorldReadableFiles",
"WorldWriteableFiles"
)
}
}
dependencies {
// Security libraries
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("net.zetetic:android-database-sqlcipher:4.5.4")
// Security testing
testImplementation("org.owasp:dependency-check-gradle:8.4.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
Security-focused ProGuard rules:
# proguard-rules.pro
# Keep security-critical annotations
-keepattributes *Annotation*
# Obfuscate everything by default
-repackageclasses 'a'
-allowaccessmodification
# Remove logging in release
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
public static int i(...);
public static int w(...);
public static int e(...);
}
# Remove debug code
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
# Protect sensitive classes from reflection attacks
-keepclassmembers class * {
@androidx.annotation.Keep *;
}
1.6 Common Security Mistakes
Learning from common mistakes helps you avoid them in your own code.
1.6.1 Top 10 Android Security Mistakes
// ❌ MISTAKE 1: Logging sensitive data
Log.d(TAG, "User password: $password")
Log.d(TAG, "Auth token: $token")
Log.d(TAG, "API Response: $sensitiveResponse")
// ✅ FIX: Never log sensitive data
Log.d(TAG, "Login attempt for user: ${userId.hashCode()}")
// ❌ MISTAKE 2: Hardcoded secrets
const val API_KEY = "sk_live_abc123xyz789"
const val ENCRYPTION_KEY = "my_secret_key_123"
// ✅ FIX: Use secure key management
val apiKey = BuildConfig.API_KEY // From local.properties (not committed)
val encryptionKey = keyStore.getKey("alias", null)
// ❌ MISTAKE 3: Allowing cleartext traffic
// Default in Android 8.1 and below - cleartext allowed
// ✅ FIX: Enforce TLS
// android:usesCleartextTraffic="false" in manifest
// Or use network_security_config.xml
// ❌ MISTAKE 4: Trusting all certificates
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
// ✅ FIX: Use system trust store or certificate pinning
// ❌ MISTAKE 5: Exported components without protection
<activity android:name=".AdminActivity" android:exported="true" />
// ✅ FIX: Protect sensitive components
<activity
android:name=".AdminActivity"
android:exported="false" />
<activity
android:name=".DeepLinkActivity"
android:exported="true"
android:permission="com.example.ADMIN_PERMISSION" />
// ❌ MISTAKE 6: SQL injection vulnerability
val query = "SELECT * FROM users WHERE name = '$userInput'"
database.rawQuery(query, null)
// ✅ FIX: Use parameterized queries
val query = "SELECT * FROM users WHERE name = ?"
database.rawQuery(query, arrayOf(userInput))
// ❌ MISTAKE 7: Storing passwords instead of hashes
sharedPrefs.putString("password", plainTextPassword)
// ✅ FIX: Never store passwords
// Use proper authentication (OAuth, tokens) or hash with bcrypt/Argon2
// ❌ MISTAKE 8: Using deprecated crypto
val cipher = Cipher.getInstance("DES/ECB/PKCS5Padding") // Weak!
val md = MessageDigest.getInstance("MD5") // Broken!
val md2 = MessageDigest.getInstance("SHA1") // Weak!
// ✅ FIX: Use modern algorithms
val cipher = Cipher.getInstance("AES/GCM/NoPadding") // Strong
val md = MessageDigest.getInstance("SHA-256") // Secure
// ❌ MISTAKE 9: Ignoring backup security
<application android:allowBackup="true"> // Default!
// ✅ FIX: Disable or control backup
<application
android:allowBackup="false"
android:fullBackupContent="@xml/backup_rules">
// ❌ MISTAKE 10: Implicit intents for sensitive data
val intent = Intent("com.example.SEND_DATA")
intent.putExtra("secret", sensitiveData)
sendBroadcast(intent) // Any app can receive!
// ✅ FIX: Use explicit intents and local broadcasts
val intent = Intent(this, DataReceiver::class.java)
intent.putExtra("secret", sensitiveData)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
1.6.2 Security Code Review Checklist
When reviewing code for security issues, check:
## Code Review Security Checklist
### Data Handling
- [ ] No sensitive data in logs
- [ ] No hardcoded secrets
- [ ] Sensitive data encrypted at rest
- [ ] Secure random number generation used
- [ ] Memory cleared after handling secrets
### Network
- [ ] TLS enforced for all connections
- [ ] Certificate pinning implemented
- [ ] No custom TrustManager that trusts all
- [ ] Proper error handling (no info leakage)
### Storage
- [ ] Sensitive data in encrypted storage
- [ ] Proper file permissions (MODE_PRIVATE)
- [ ] Backup rules configured
- [ ] No sensitive data on external storage
### Components
- [ ] No unnecessary exported components
- [ ] Intent data validated
- [ ] Deep links validated
- [ ] Content providers protected
### Cryptography
- [ ] Modern algorithms used (AES-GCM, SHA-256+)
- [ ] Keys stored in Android Keystore
- [ ] No hardcoded IVs or keys
- [ ] Proper key derivation for passwords
1.7 Summary
In this chapter, we established the foundation for Android security:
Android Security Architecture:
- Multiple layers of defense from hardware to application
- Linux kernel provides process isolation and SELinux
- Hardware-backed security through TEE and StrongBox
- Application sandbox isolates each app
Permission System:
- Protection levels: normal, dangerous, signature, privileged
- Runtime permissions for dangerous permissions
- One-time permissions and auto-reset features
- Principle of least privilege
Security Evolution:
- Each Android version adds security improvements
- Network Security Configuration for declarative security
- Scoped storage for improved privacy
- Always test on your minimum supported API level
Threat Modeling:
- STRIDE framework for categorizing threats
- Identify assets, threat actors, and attack vectors
- Risk assessment prioritizes mitigation efforts
- Document and review threat models regularly
Secure Development:
- Integrate security throughout the development lifecycle
- Use security gates at design, build, test, and release
- Automate security checks in CI/CD
- Learn from common security mistakes
The security mechanisms we discussed in this chapter protect your application at the system level. However, they are not sufficient on their own. In the next chapter, we'll implement secure data storage that protects user information even when these system-level protections are bypassed.
1.8 Key Takeaways
Defense in Depth: Never rely on a single security mechanism. Layer multiple protections.
Least Privilege: Request only the permissions you need, when you need them.
Assume Breach: Design your app as if the device is already compromised.
Stay Updated: Android security improves with each release. Support the latest versions.
Think Like an Attacker: Threat modeling helps you find vulnerabilities before others do.